Skip to content

Commit 81db72c

Browse files
committed
Attrs code generator
1 parent 49b5f26 commit 81db72c

6 files changed

Lines changed: 116 additions & 15 deletions

File tree

rest_client_gen/dynamic_typing/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import Iterable, List, Tuple, Union
22

3-
ImportPathList = List[Tuple[str, Union[Iterable[str], str]]]
3+
ImportPathList = List[Tuple[str, Union[Iterable[str], str, None]]]
44

55

66
class BaseType:

rest_client_gen/dynamic_typing/typing.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,31 @@ def compile_imports(imports: ImportPathList) -> str:
2828
"""
2929
Merge list of imports path and convert them into list code (string)
3030
"""
31-
imports_map: Dict[str, Set[str]] = OrderedDict()
31+
class_imports_map: Dict[str, Set[str]] = OrderedDict()
32+
package_imports_set: Set[str] = set()
3233
for module, classes in filter(None, imports):
33-
classes_set = imports_map.get(module, set())
34-
if isinstance(classes, str):
35-
classes_set.add(classes)
34+
if classes is None:
35+
package_imports_set.add(module)
3636
else:
37-
classes_set.update(classes)
38-
imports_map[module] = classes_set
37+
classes_set = class_imports_map.get(module, set())
38+
if isinstance(classes, str):
39+
classes_set.add(classes)
40+
else:
41+
classes_set.update(classes)
42+
class_imports_map[module] = classes_set
3943

4044
# Sort imports by package name and sort class names of each import
41-
imports_map = OrderedDict(sorted(
42-
((module, sorted(classes)) for module, classes in imports_map.items()),
45+
class_imports_map = OrderedDict(sorted(
46+
((module, sorted(classes)) for module, classes in class_imports_map.items()),
4347
key=operator.itemgetter(0)
4448
))
4549

46-
return "\n".join(f"from {module} import {', '.join(classes)}" for module, classes in imports_map.items())
50+
class_imports = "\n".join(
51+
f"from {module} import {', '.join(classes)}"
52+
for module, classes in class_imports_map.items()
53+
)
54+
package_imports = "\n".join(
55+
f"import {module}"
56+
for module in sorted(package_imports_set)
57+
)
58+
return "\n".join(filter(None, (package_imports, class_imports)))

rest_client_gen/models/attr.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import operator
2+
from inspect import isclass
3+
from typing import List, Tuple
4+
5+
from .base import GenericModelCodeGenerator, template
6+
from ..dynamic_typing import DList, DOptional, ImportPathList, MetaData, ModelMeta, StringSerializable
7+
8+
KWAGRS_TEMPLATE = "{% for key, value in kwargs.items() %}" \
9+
"{{ key }}={{ value }}" \
10+
"{% if not loop.last %}, {% endif %}" \
11+
"{% endfor %}"
12+
13+
14+
def sort_kwargs(kwargs: dict) -> dict:
15+
return dict(sorted(kwargs.items(), key=operator.itemgetter(0)))
16+
17+
18+
class AttrsModelCodeGenerator(GenericModelCodeGenerator):
19+
ATTRS = template("attr.s"
20+
"{% if kwargs %}"
21+
f"({KWAGRS_TEMPLATE})"
22+
"{% endif %}")
23+
ATTRIB = template(f"attr.ib({KWAGRS_TEMPLATE})")
24+
25+
def __init__(self, model: ModelMeta, attrs_kwargs: dict = None, **kwargs):
26+
"""
27+
:param model: ModelMeta instance
28+
:param attrs_kwargs: kwargs for @attr.s() decorators
29+
:param kwargs:
30+
"""
31+
super().__init__(model, **kwargs)
32+
self.attrs_kwargs = attrs_kwargs or {}
33+
34+
def generate(self, nested_classes: List[str] = None) -> Tuple[ImportPathList, str]:
35+
"""
36+
:param nested_classes: list of strings that contains classes code
37+
:return: list of import data, class code
38+
"""
39+
imports, code = super().generate(nested_classes)
40+
imports.append(('attr', None))
41+
return imports, code
42+
43+
@property
44+
def decorators(self) -> List[str]:
45+
"""
46+
:return: List of decorators code (without @)
47+
"""
48+
return [self.ATTRS.render(kwargs=sort_kwargs(self.attrs_kwargs))]
49+
50+
def field_data(self, name: str, meta: MetaData, optional: bool) -> Tuple[ImportPathList, dict]:
51+
"""
52+
Form field data for template
53+
54+
:param name: Field name
55+
:param meta: Field metadata
56+
:param optional: Is field optional
57+
:return: imports, field data
58+
"""
59+
imports, data = super().field_data(name, meta, optional)
60+
body_kwargs = {}
61+
if optional:
62+
meta: DOptional
63+
if isinstance(meta.type, DList):
64+
body_kwargs["factory"] = "list"
65+
else:
66+
body_kwargs["default"] = "None"
67+
if isclass(meta) and issubclass(meta, StringSerializable):
68+
body_kwargs["converter"] = meta.__name__
69+
data["body"] = self.ATTRIB.render(kwargs=sort_kwargs(body_kwargs))
70+
return imports, data

test/test_code_generation/test_typing.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,24 @@ def test_metadata_to_typing_with_dict():
2323
"from pytest import param\n"
2424
"from typing import Any, List, Tuple",
2525
id="basic"
26-
)
26+
),
27+
pytest.param(
28+
[
29+
('typing', ('List', 'Any')),
30+
[],
31+
('typing', None),
32+
[],
33+
('pytest', 'param'),
34+
('typing', ('List', 'Tuple')),
35+
('attr', None),
36+
('typing', None),
37+
],
38+
"import attr\n"
39+
"import typing\n"
40+
"from pytest import param\n"
41+
"from typing import Any, List, Tuple",
42+
id="basic"
43+
),
2744
]
2845

2946

testing_tools/real_apis/f1.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77

88
from rest_client_gen.generator import MetadataGenerator
99
from rest_client_gen.models import compose_models
10-
from rest_client_gen.models.base import GenericModelCodeGenerator, generate_code
10+
from rest_client_gen.models.attr import AttrsModelCodeGenerator
11+
from rest_client_gen.models.base import generate_code
1112
from rest_client_gen.registry import ModelRegistry
1213
from rest_client_gen.utils import json_format
1314
from testing_tools.pprint_meta_data import pretty_format_meta
@@ -58,7 +59,7 @@ def main():
5859
print('\n', json_format([structure[0], {str(a): str(b) for a, b in structure[1].items()}]))
5960
print("=" * 20)
6061

61-
print(generate_code(structure, GenericModelCodeGenerator))
62+
print(generate_code(structure, AttrsModelCodeGenerator))
6263

6364

6465
if __name__ == '__main__':

testing_tools/real_apis/pathofexile.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
from rest_client_gen.generator import MetadataGenerator
77
from rest_client_gen.models import compose_models
8-
from rest_client_gen.models.base import GenericModelCodeGenerator, generate_code
8+
from rest_client_gen.models.attr import AttrsModelCodeGenerator
9+
from rest_client_gen.models.base import generate_code
910
from rest_client_gen.registry import ModelRegistry
1011
from rest_client_gen.utils import json_format
1112
from testing_tools.pprint_meta_data import pretty_format_meta
@@ -36,7 +37,7 @@ def main():
3637
print('\n', json_format([structure[0], {str(a): str(b) for a, b in structure[1].items()}]))
3738
print("=" * 20)
3839

39-
print(generate_code(structure, GenericModelCodeGenerator))
40+
print(generate_code(structure, AttrsModelCodeGenerator))
4041

4142

4243
if __name__ == '__main__':

0 commit comments

Comments
 (0)