Skip to content

Commit 2dea771

Browse files
authored
Merge pull request #5 from bogdandm/optimization
Optimization
2 parents e66932e + 41c6ff1 commit 2dea771

File tree

7 files changed

+108
-20
lines changed

7 files changed

+108
-20
lines changed

rest_client_gen/dynamic_typing/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from .base import (
2-
BaseType, ImportPathList, MetaData, NoneType, Unknown, UnknownType
2+
BaseType, ImportPathList, MetaData, NoneType, Unknown, UnknownType, get_hash_string
33
)
44
from .complex import ComplexType, DList, DOptional, DTuple, DUnion, SingleType
55
from .models_meta import AbsoluteModelRef, ModelMeta, ModelPtr

rest_client_gen/dynamic_typing/base.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from inspect import isclass
12
from typing import Iterable, List, Tuple, Union
23

34
ImportPathList = List[Tuple[str, Union[Iterable[str], str, None]]]
@@ -28,6 +29,27 @@ def to_typing_code(self) -> Tuple[ImportPathList, str]:
2829
"""
2930
raise NotImplementedError()
3031

32+
def to_hash_string(self) -> str:
33+
"""
34+
Return unique string that can be used to generate hash of type instance.
35+
Caches hash value by default. If subclass can mutate (by default it always can)
36+
then it should define setters to safely invalidate cached value.
37+
38+
:return: hash string
39+
"""
40+
# NOTE: Do not override __hash__ function because BaseType instances isn't immutable
41+
if not self._hash:
42+
self._hash = self._to_hash_string()
43+
return self._hash
44+
45+
def _to_hash_string(self) -> str:
46+
"""
47+
Hash getter method to override
48+
49+
:return:
50+
"""
51+
raise NotImplementedError()
52+
3153

3254
class UnknownType(BaseType):
3355
__slots__ = []
@@ -44,7 +66,19 @@ def replace(self, t: 'MetaData', **kwargs) -> 'UnknownType':
4466
def to_typing_code(self) -> Tuple[ImportPathList, str]:
4567
return ([('typing', 'Any')], 'Any')
4668

69+
def to_hash_string(self) -> str:
70+
return "Unknown"
71+
4772

4873
Unknown = UnknownType()
4974
NoneType = type(None)
5075
MetaData = Union[type, dict, BaseType]
76+
77+
78+
def get_hash_string(t: MetaData):
79+
if isinstance(t, dict):
80+
return str(hash(tuple((k, get_hash_string(v)) for k, v in t.items())))
81+
elif isclass(t):
82+
return str(t)
83+
elif isinstance(t, BaseType):
84+
return t.to_hash_string()

rest_client_gen/dynamic_typing/complex.py

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,53 @@
11
from itertools import chain
22
from typing import Iterable, List, Tuple, Union
33

4-
from .base import BaseType, ImportPathList, MetaData
4+
from .base import BaseType, ImportPathList, MetaData, get_hash_string
55
from .typing import metadata_to_typing
66

77

88
class SingleType(BaseType):
9-
__slots__ = ["type"]
9+
__slots__ = ["_type", "_hash"]
1010

1111
def __init__(self, t: MetaData):
12-
self.type = t
12+
self._type = t
13+
self._hash = None
14+
15+
@property
16+
def type(self):
17+
return self._type
18+
19+
@type.setter
20+
def type(self, t: MetaData):
21+
self._type = t
22+
self._hash = None
1323

1424
def __str__(self):
15-
return f"{self.__class__.__name__}[{self.type}]"
25+
return f"{type(self).__name__}[{self.type}]"
1626

1727
def __repr__(self):
18-
return f"<{self.__class__.__name__} [{self.type}]>"
28+
return f"<{type(self).__name__} [{self.type}]>"
1929

2030
def __iter__(self) -> Iterable['MetaData']:
2131
yield self.type
2232

2333
def __eq__(self, other):
24-
return isinstance(other, self.__class__) and self.type == other.type
34+
return type(other) is type(self) and self.type == other.type
2535

2636
def replace(self, t: 'MetaData', **kwargs) -> 'SingleType':
2737
self.type = t
2838
return self
2939

40+
def _to_hash_string(self) -> str:
41+
return f"{type(self).__name__}/{get_hash_string(self.type)}"
42+
3043

3144
class ComplexType(BaseType):
32-
__slots__ = ["_types"]
45+
__slots__ = ["_types", "_sorted", "_hash"]
3346

3447
def __init__(self, *types: MetaData):
3548
self._types = list(types)
49+
self._sorted = None
50+
self._hash = None
3651

3752
@property
3853
def types(self):
@@ -42,6 +57,7 @@ def types(self):
4257
def types(self, value):
4358
self._types = value
4459
self._sorted = None
60+
self._hash = None
4561

4662
@property
4763
def sorted(self):
@@ -62,17 +78,17 @@ def _sort_key(self, item):
6278

6379
def __str__(self):
6480
items = ', '.join(map(str, self.types))
65-
return f"{self.__class__.__name__}[{items}]"
81+
return f"{type(self).__name__}[{items}]"
6682

6783
def __repr__(self):
6884
items = ', '.join(map(str, self.types))
69-
return f"<{self.__class__.__name__} [{items}]>"
85+
return f"<{type(self).__name__} [{items}]>"
7086

7187
def __iter__(self) -> Iterable['MetaData']:
7288
yield from self.types
7389

7490
def __eq__(self, other):
75-
return isinstance(other, self.__class__) and self.sorted == other.sorted
91+
return type(other) is type(self) and self.sorted == other.sorted
7692

7793
def __len__(self):
7894
return len(self.types)
@@ -97,6 +113,9 @@ def to_typing_code(self) -> Tuple[ImportPathList, str]:
97113
f"[{nested}]"
98114
)
99115

116+
def _to_hash_string(self) -> str:
117+
return type(self).__name__ + "/" + ",".join(map(get_hash_string, self.types))
118+
100119

101120
class DOptional(SingleType):
102121
"""
@@ -117,16 +136,22 @@ class DUnion(ComplexType):
117136
"""
118137

119138
def __init__(self, *types: Union[type, BaseType, dict]):
139+
hashes = set()
120140
unique_types = []
141+
# Ensure that types in union are unique
121142
for t in types:
122143
if isinstance(t, DUnion):
123144
# Merging nested DUnions
124145
for t2 in list(t._extract_nested_types()):
125-
if t2 not in unique_types:
146+
h = get_hash_string(t2)
147+
if h not in hashes:
126148
unique_types.append(t2)
127-
elif t not in unique_types:
128-
# Ensure that types in union are unique
129-
unique_types.append(t)
149+
hashes.add(h)
150+
else:
151+
h = get_hash_string(t)
152+
if h not in hashes:
153+
hashes.add(h)
154+
unique_types.append(t)
130155
super().__init__(*unique_types)
131156

132157
def _extract_nested_types(self):

rest_client_gen/dynamic_typing/models_meta.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ def replace(self, t: ModelMeta, **kwargs) -> 'ModelPtr':
121121
return self
122122

123123
def replace_parent(self, t: ModelMeta, **kwargs) -> 'ModelPtr':
124+
self._hash = None
124125
self.parent.remove_child_ref(self)
125126
self.parent = t
126127
self.parent.add_child_ref(self)
@@ -129,6 +130,9 @@ def replace_parent(self, t: ModelMeta, **kwargs) -> 'ModelPtr':
129130
def to_typing_code(self) -> Tuple[ImportPathList, str]:
130131
return AbsoluteModelRef(self.type).to_typing_code()
131132

133+
def _to_hash_string(self) -> str:
134+
return f"{type(self).__name__}_#{self.type.index}"
135+
132136

133137
ContextInjectionType = Dict[ModelMeta, Union[ModelMeta, str]]
134138

@@ -150,6 +154,11 @@ class NestedModel:
150154
This information is only available at the models code generation stage
151155
while typing code is generated from raw metadata and passing this absolute path as argument
152156
to each ModelPtr would be annoying.
157+
158+
Usage:
159+
160+
with AbsoluteModelRef.inject({TestModel: "ParentModelName"}):
161+
<some code generation>
153162
"""
154163

155164
class Context:

test/test_dynamic_typing/test_dynamic_typing.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
from builtins import complex
2+
13
import pytest
24

3-
from rest_client_gen.dynamic_typing import DUnion
5+
from rest_client_gen.dynamic_typing import DUnion, get_hash_string
46

57
# *args | MetaData
6-
test_dunion= [
8+
test_dunion = [
79
pytest.param(
810
[int, int],
911
DUnion(int),
@@ -21,7 +23,21 @@
2123
)
2224
]
2325

26+
2427
@pytest.mark.parametrize("value,expected", test_dunion)
2528
def test_dunion_creation(value, expected):
2629
result = DUnion(*value)
27-
assert result == expected
30+
assert result == expected
31+
32+
33+
def test_hash_string():
34+
a = {'a': int}
35+
b = {'b': int}
36+
c = {'a': float}
37+
assert len(set(map(get_hash_string, (a, b, c)))) == 3
38+
39+
union = DUnion(str, float)
40+
h1 = union.to_hash_string()
41+
union.replace(complex, index=0)
42+
h2 = union.to_hash_string()
43+
assert h1 != h2, f"{h1}, {h2}"

testing_tools/pprint_meta_data.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@ def _pprint_gen(value, key=None, lvl=0, empty_line=True, ignore_ptr=False):
3737
yield " <empty>"
3838

3939
elif isinstance(value, SingleType):
40-
yield f"{value.__class__.__name__}:"
40+
yield f"{type(value).__name__}:"
4141
yield from _pprint_gen(value.type, lvl=lvl, empty_line=False, ignore_ptr=ignore_ptr)
4242

4343
elif isinstance(value, ComplexType):
44-
yield f"{value.__class__.__name__}:"
44+
yield f"{type(value).__name__}:"
4545

4646
for t in value.types:
4747
yield from _pprint_gen(t, lvl=lvl + 1, ignore_ptr=ignore_ptr)

testing_tools/real_apis/pathofexile.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""
22
Path of Exile API http://www.pathofexile.com/developer/docs/api-resource-public-stash-tabs
33
"""
4+
from datetime import datetime
5+
46
import requests
57

68
from rest_client_gen.generator import MetadataGenerator
@@ -23,6 +25,7 @@ def main():
2325
tabs = tabs['stashes']
2426

2527
print(f"Start model generation (data len = {len(tabs)})")
28+
start_t = datetime.now()
2629
gen = MetadataGenerator()
2730
reg = ModelRegistry()
2831
fields = gen.generate(*tabs)
@@ -38,6 +41,7 @@ def main():
3841
print("=" * 20)
3942

4043
print(generate_code(structure, AttrsModelCodeGenerator))
44+
print(f"{(datetime.now() - start_t).total_seconds():.4f} seconds")
4145

4246

4347
if __name__ == '__main__':

0 commit comments

Comments
 (0)