Skip to content

Commit 59ee7d5

Browse files
committed
Merge branch 'master' into optimization
2 parents 257eac1 + e66932e commit 59ee7d5

File tree

4 files changed

+208
-13
lines changed

4 files changed

+208
-13
lines changed

TODO.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
- [ ] Save meta-models as python code
1111
- [X] typing code generation
1212
- [ ] (Maybe in future) Extract to another module (by serializers for each dynamic typing class)
13-
- [ ] attrs
13+
- [X] attrs
1414
- [ ] dataclasses
1515
- [ ] generate from_json/to_json converters
1616
- [ ] Model class -> Meta format converter
@@ -21,7 +21,6 @@
2121
- [ ] dataclasses
2222
- [ ] Decorator to mark class as exclude from models merge
2323
- Other features
24-
- [ ] Keys separator style
2524
- [ ] Decode unicode in keys
2625
- [ ] Nesting models generation
2726
- [X] Cascade (default)
@@ -54,7 +53,7 @@
5453
- [X] Merge meta-models and extract common ones
5554
- [ ] Save meta-models as python code
5655
- [X] typing code generation
57-
- [ ] attrs
56+
- [X] attrs
5857
- [ ] dataclasses
5958
- [ ] generate from_json/to_json converters
6059
- [ ] Model class -> Meta format converter

rest_client_gen/models/attr.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import operator
21
from inspect import isclass
3-
from typing import List, Tuple
2+
from typing import Iterable, List, Tuple
43

54
from .base import GenericModelCodeGenerator, template
65
from ..dynamic_typing import DList, DOptional, ImportPathList, MetaData, ModelMeta, StringSerializable
@@ -11,13 +10,28 @@
1110
"{% if not loop.last %}, {% endif %}" \
1211
"{% endfor %}"
1312

13+
DEFAULT_ORDER = (
14+
("default", "converter", "factory"),
15+
"*",
16+
("metadata",)
17+
)
1418

15-
def sort_kwargs(kwargs: dict) -> dict:
16-
# TODO: Unify this function
17-
meta = kwargs.pop("metadata", {})
18-
sorted_dict = dict(sorted(kwargs.items(), key=operator.itemgetter(0)))
19-
if meta:
20-
sorted_dict["metadata"] = meta
19+
20+
def sort_kwargs(kwargs: dict, ordering: Iterable[Iterable[str]] = DEFAULT_ORDER) -> dict:
21+
sorted_dict_1 = {}
22+
sorted_dict_2 = {}
23+
current = sorted_dict_1
24+
for group in ordering:
25+
if isinstance(group, str):
26+
if group != "*":
27+
raise ValueError(f"Unknown kwarg group: {group}")
28+
current = sorted_dict_2
29+
else:
30+
for item in group:
31+
if item in kwargs:
32+
value = kwargs.pop(item)
33+
current[item] = value
34+
sorted_dict = {**sorted_dict_1, **kwargs, **sorted_dict_2}
2135
return sorted_dict
2236

2337

@@ -53,7 +67,7 @@ def decorators(self) -> List[str]:
5367
"""
5468
:return: List of decorators code (without @)
5569
"""
56-
return [self.ATTRS.render(kwargs=sort_kwargs(self.attrs_kwargs))]
70+
return [self.ATTRS.render(kwargs=self.attrs_kwargs)]
5771

5872
def field_data(self, name: str, meta: MetaData, optional: bool) -> Tuple[ImportPathList, dict]:
5973
"""
@@ -72,8 +86,12 @@ def field_data(self, name: str, meta: MetaData, optional: bool) -> Tuple[ImportP
7286
body_kwargs["factory"] = "list"
7387
else:
7488
body_kwargs["default"] = "None"
75-
if isclass(meta) and issubclass(meta, StringSerializable):
89+
if isclass(meta.type) and issubclass(meta.type, StringSerializable):
90+
body_kwargs["converter"] = f"optional({meta.type.__name__})"
91+
imports.append(("attr.converter", "optional"))
92+
elif isclass(meta) and issubclass(meta, StringSerializable):
7693
body_kwargs["converter"] = meta.__name__
94+
7795
if not self.no_meta:
7896
body_kwargs["metadata"] = {METADATA_FIELD_NAME: name}
7997
data["body"] = self.ATTRIB.render(kwargs=sort_kwargs(body_kwargs))
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
from typing import Dict, List
2+
3+
import pytest
4+
5+
from rest_client_gen.dynamic_typing import (DList, DOptional, FloatString, IntString, ModelMeta, compile_imports)
6+
from rest_client_gen.models import sort_fields
7+
from rest_client_gen.models.attr import AttrsModelCodeGenerator, METADATA_FIELD_NAME, sort_kwargs
8+
from rest_client_gen.models.base import generate_code
9+
from test.test_code_generation.test_models_code_generator import model_factory, trim
10+
11+
12+
def test_attrib_kwargs_sort():
13+
sorted_kwargs = sort_kwargs(dict(
14+
y=2,
15+
metadata='b',
16+
converter='a',
17+
default=None,
18+
x=1,
19+
))
20+
expected = ['default', 'converter', 'y', 'x', 'metadata']
21+
for k1, k2 in zip(sorted_kwargs.keys(), expected):
22+
assert k1 == k2
23+
try:
24+
sort_kwargs({}, ['wrong_char'])
25+
except ValueError as e:
26+
assert e.args[0].endswith('wrong_char')
27+
else:
28+
assert 0, "XPass"
29+
30+
31+
32+
def field_meta(original_name):
33+
return f"metadata={{'{METADATA_FIELD_NAME}': '{original_name}'}}"
34+
35+
36+
# Data structure:
37+
# pytest.param id -> {
38+
# "model" -> (model_name, model_metadata),
39+
# test_name -> expected, ...
40+
# }
41+
test_data = {
42+
"base": {
43+
"model": ("Test", {
44+
"foo": int,
45+
"bar": int,
46+
"baz": float
47+
}),
48+
"fields_data": {
49+
"foo": {
50+
"name": "foo",
51+
"type": "int",
52+
"body": f"attr.ib({field_meta('foo')})"
53+
},
54+
"bar": {
55+
"name": "bar",
56+
"type": "int",
57+
"body": f"attr.ib({field_meta('bar')})"
58+
},
59+
"baz": {
60+
"name": "baz",
61+
"type": "float",
62+
"body": f"attr.ib({field_meta('baz')})"
63+
}
64+
},
65+
"fields": {
66+
"imports": "",
67+
"fields": [
68+
f"foo: int = attr.ib({field_meta('foo')})",
69+
f"bar: int = attr.ib({field_meta('bar')})",
70+
f"baz: float = attr.ib({field_meta('baz')})",
71+
]
72+
},
73+
"generated": trim(f"""
74+
import attr
75+
76+
77+
@attr.s
78+
class Test:
79+
foo: int = attr.ib({field_meta('foo')})
80+
bar: int = attr.ib({field_meta('bar')})
81+
baz: float = attr.ib({field_meta('baz')})
82+
""")
83+
},
84+
"complex": {
85+
"model": ("Test", {
86+
"foo": int,
87+
"baz": DOptional(DList(DList(str))),
88+
"bar": DOptional(IntString),
89+
"qwerty": FloatString,
90+
"asdfg": DOptional(int)
91+
}),
92+
"fields_data": {
93+
"foo": {
94+
"name": "foo",
95+
"type": "int",
96+
"body": f"attr.ib({field_meta('foo')})"
97+
},
98+
"baz": {
99+
"name": "baz",
100+
"type": "Optional[List[List[str]]]",
101+
"body": f"attr.ib(factory=list, {field_meta('baz')})"
102+
},
103+
"bar": {
104+
"name": "bar",
105+
"type": "Optional[IntString]",
106+
"body": f"attr.ib(default=None, converter=optional(IntString), {field_meta('bar')})"
107+
},
108+
"qwerty": {
109+
"name": "qwerty",
110+
"type": "FloatString",
111+
"body": f"attr.ib(converter=FloatString, {field_meta('qwerty')})"
112+
},
113+
"asdfg": {
114+
"name": "asdfg",
115+
"type": "Optional[int]",
116+
"body": f"attr.ib(default=None, {field_meta('asdfg')})"
117+
}
118+
},
119+
"generated": trim(f"""
120+
import attr
121+
from attr.converter import optional
122+
from rest_client_gen.dynamic_typing.string_serializable import FloatString, IntString
123+
from typing import List, Optional
124+
125+
126+
@attr.s
127+
class Test:
128+
foo: int = attr.ib({field_meta('foo')})
129+
qwerty: FloatString = attr.ib(converter=FloatString, {field_meta('qwerty')})
130+
baz: Optional[List[List[str]]] = attr.ib(factory=list, {field_meta('baz')})
131+
bar: Optional[IntString] = attr.ib(default=None, converter=optional(IntString), {field_meta('bar')})
132+
asdfg: Optional[int] = attr.ib(default=None, {field_meta('asdfg')})
133+
""")
134+
}
135+
}
136+
137+
test_data_unzip = {
138+
test: [
139+
pytest.param(
140+
model_factory(*data["model"]),
141+
data[test],
142+
id=id
143+
)
144+
for id, data in test_data.items()
145+
if test in data
146+
]
147+
for test in ("fields_data", "fields", "generated")
148+
}
149+
150+
151+
@pytest.mark.parametrize("value,expected", test_data_unzip["fields_data"])
152+
def test_fields_data_attr(value: ModelMeta, expected: Dict[str, dict]):
153+
gen = AttrsModelCodeGenerator(value)
154+
required, optional = sort_fields(value)
155+
for is_optional, fields in enumerate((required, optional)):
156+
for field in fields:
157+
field_imports, data = gen.field_data(field, value.type[field], bool(is_optional))
158+
assert data == expected[field]
159+
160+
161+
@pytest.mark.parametrize("value,expected", test_data_unzip["fields"])
162+
def test_fields_attr(value: ModelMeta, expected: dict):
163+
expected_imports: str = expected["imports"]
164+
expected_fields: List[str] = expected["fields"]
165+
gen = AttrsModelCodeGenerator(value)
166+
imports, fields = gen.fields
167+
imports = compile_imports(imports)
168+
assert imports == expected_imports
169+
assert fields == expected_fields
170+
171+
172+
@pytest.mark.parametrize("value,expected", test_data_unzip["generated"])
173+
def test_generated_attr(value: ModelMeta, expected: str):
174+
generated = generate_code(([{"model": value, "nested": []}], {}), AttrsModelCodeGenerator)
175+
assert generated.rstrip() == expected, generated

test/test_code_generation/test_models_code_generator.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
from rest_client_gen.models import indent, sort_fields
88
from rest_client_gen.models.base import GenericModelCodeGenerator, generate_code
99

10+
# Data structure:
11+
# (string, indent lvl, indent string)
12+
# result
1013
test_indent_data = [
1114
pytest.param(
1215
("1", 1, " " * 4),

0 commit comments

Comments
 (0)