Skip to content

Commit 39a09aa

Browse files
authored
Merge pull request #14 from bogdandm/dataclasses
Dataclasses code generation
2 parents 66a25c8 + 76c9ed6 commit 39a09aa

File tree

9 files changed

+295
-53
lines changed

9 files changed

+295
-53
lines changed

TODO.md

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
- [X] typing code generation
1212
- [ ] (Maybe in future) Extract to another module (by serializers for each dynamic typing class)
1313
- [X] attrs
14-
- [ ] dataclasses
14+
- [X] dataclasses
15+
- [ ] post_init converters for StringSerializable types
1516
- [ ] generate from_json/to_json converters
1617
- [ ] Model class -> Meta format converter
1718
- [ ] attrs
@@ -36,24 +37,10 @@
3637
- [X] ISO date
3738
- [X] ISO time
3839
- [X] ISO datetime
40+
- [ ] Don't create metadata (RCG_ORIGINAL_FIELD) if original_field == generated_field
3941
- [X] Cli tool
4042

4143
- Testing
42-
- Models layer
43-
- [X] Create and register models
44-
- [X] Test pointers in the models registry
45-
- [ ] Test whats going on with strict/non-strict merging
46-
- [ ] Save meta-models as python code
47-
- [X] typing code generation
48-
- [X] attrs
49-
- [ ] dataclasses
50-
- [ ] generate from_json/to_json converters
51-
- [ ] Model class -> Meta format converter
52-
- [ ] attrs
53-
- [ ] dataclasses
54-
- [ ] Implement existing models registration
55-
- [ ] attrs
56-
- [ ] dataclasses
5744

5845
- Build, Deploy, CI
5946
- [X] setup.py

json_to_models/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from json_to_models.models import ModelsStructureType, compose_models
1616
from json_to_models.models.attr import AttrsModelCodeGenerator
1717
from json_to_models.models.base import GenericModelCodeGenerator, generate_code
18+
from json_to_models.models.dataclasses import DataclassModelCodeGenerator
1819
from json_to_models.registry import (
1920
ModelCmp, ModelFieldsEquals, ModelFieldsNumberMatch, ModelFieldsPercentMatch, ModelRegistry
2021
)
@@ -40,8 +41,7 @@ class Cli:
4041
MODEL_GENERATOR_MAPPING: Dict[str, Type[GenericModelCodeGenerator]] = {
4142
"base": convert_args(GenericModelCodeGenerator),
4243
"attrs": convert_args(AttrsModelCodeGenerator, meta=bool_js_style),
43-
# TODO: vvvv
44-
"dataclasses": None
44+
"dataclasses": convert_args(DataclassModelCodeGenerator, meta=bool_js_style, post_init_converters=bool_js_style)
4545
}
4646

4747
def __init__(self):

json_to_models/models/attr.py

Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
11
from inspect import isclass
2-
from typing import Iterable, List, Tuple
2+
from typing import List, Tuple
33

4-
from .base import GenericModelCodeGenerator, template
5-
from ..dynamic_typing import DList, DOptional, ImportPathList, MetaData, ModelMeta, StringSerializable
6-
7-
METADATA_FIELD_NAME = "RCG_ORIGINAL_FIELD"
8-
KWAGRS_TEMPLATE = "{% for key, value in kwargs.items() %}" \
9-
"{{ key }}={{ value }}" \
10-
"{% if not loop.last %}, {% endif %}" \
11-
"{% endfor %}"
4+
from .base import GenericModelCodeGenerator, KWAGRS_TEMPLATE, METADATA_FIELD_NAME, sort_kwargs, template
5+
from ..dynamic_typing import DDict, DList, DOptional, ImportPathList, MetaData, ModelMeta, StringSerializable
126

137
DEFAULT_ORDER = (
148
("default", "converter", "factory"),
@@ -17,24 +11,6 @@
1711
)
1812

1913

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}
35-
return sorted_dict
36-
37-
3814
class AttrsModelCodeGenerator(GenericModelCodeGenerator):
3915
ATTRS = template("attr.s"
4016
"{% if kwargs %}"
@@ -45,7 +21,7 @@ class AttrsModelCodeGenerator(GenericModelCodeGenerator):
4521
def __init__(self, model: ModelMeta, meta=False, attrs_kwargs: dict = None, **kwargs):
4622
"""
4723
:param model: ModelMeta instance
48-
:param no_meta: Disable generation of metadata as attrib argument
24+
:param meta: Enable generation of metadata as attrib argument
4925
:param attrs_kwargs: kwargs for @attr.s() decorators
5026
:param kwargs:
5127
"""
@@ -84,6 +60,8 @@ def field_data(self, name: str, meta: MetaData, optional: bool) -> Tuple[ImportP
8460
meta: DOptional
8561
if isinstance(meta.type, DList):
8662
body_kwargs["factory"] = "list"
63+
elif isinstance(meta.type, DDict):
64+
body_kwargs["factory"] = "dict"
8765
else:
8866
body_kwargs["default"] = "None"
8967
if isclass(meta.type) and issubclass(meta.type, StringSerializable):
@@ -94,5 +72,5 @@ def field_data(self, name: str, meta: MetaData, optional: bool) -> Tuple[ImportP
9472

9573
if not self.no_meta:
9674
body_kwargs["metadata"] = {METADATA_FIELD_NAME: name}
97-
data["body"] = self.ATTRIB.render(kwargs=sort_kwargs(body_kwargs))
75+
data["body"] = self.ATTRIB.render(kwargs=sort_kwargs(body_kwargs, DEFAULT_ORDER))
9876
return imports, data

json_to_models/models/base.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1-
from typing import List, Tuple, Type
1+
from typing import Iterable, List, Tuple, Type
22

33
import inflection
44
from jinja2 import Template
55

66
from . import INDENT, ModelsStructureType, OBJECTS_DELIMITER, indent, sort_fields
77
from ..dynamic_typing import AbsoluteModelRef, ImportPathList, MetaData, ModelMeta, compile_imports, metadata_to_typing
88

9+
METADATA_FIELD_NAME = "RCG_ORIGINAL_FIELD"
10+
KWAGRS_TEMPLATE = "{% for key, value in kwargs.items() %}" \
11+
"{{ key }}={{ value }}" \
12+
"{% if not loop.last %}, {% endif %}" \
13+
"{% endfor %}"
14+
915

1016
def template(pattern: str, indent: str = INDENT) -> Template:
1117
"""
@@ -159,3 +165,21 @@ def generate_code(structure: ModelsStructureType, class_generator: Type[GenericM
159165
else:
160166
imports_str = ""
161167
return imports_str + objects_delimiter.join(classes) + "\n"
168+
169+
170+
def sort_kwargs(kwargs: dict, ordering: Iterable[Iterable[str]]) -> dict:
171+
sorted_dict_1 = {}
172+
sorted_dict_2 = {}
173+
current = sorted_dict_1
174+
for group in ordering:
175+
if isinstance(group, str):
176+
if group != "*":
177+
raise ValueError(f"Unknown kwarg group: {group}")
178+
current = sorted_dict_2
179+
else:
180+
for item in group:
181+
if item in kwargs:
182+
value = kwargs.pop(item)
183+
current[item] = value
184+
sorted_dict = {**sorted_dict_1, **kwargs, **sorted_dict_2}
185+
return sorted_dict

json_to_models/models/dataclass.py

Whitespace-only changes.
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from inspect import isclass
2+
from typing import List, Tuple
3+
4+
from .base import GenericModelCodeGenerator, KWAGRS_TEMPLATE, METADATA_FIELD_NAME, sort_kwargs, template
5+
from ..dynamic_typing import DDict, DList, DOptional, ImportPathList, MetaData, ModelMeta, StringSerializable
6+
7+
DEFAULT_ORDER = (
8+
("default", "default_factory"),
9+
"*",
10+
("metadata",)
11+
)
12+
13+
14+
class DataclassModelCodeGenerator(GenericModelCodeGenerator):
15+
DC_DECORATOR = template("dataclass"
16+
"{% if kwargs %}"
17+
f"({KWAGRS_TEMPLATE})"
18+
"{% endif %}")
19+
DC_FIELD = template(f"field({KWAGRS_TEMPLATE})")
20+
21+
def __init__(self, model: ModelMeta, meta=False, post_init_converters=False, dataclass_kwargs: dict = None,
22+
**kwargs):
23+
"""
24+
:param model: ModelMeta instance
25+
:param meta: Enable generation of metadata as attrib argument
26+
:param post_init_converters: Enable generation of type converters in __post_init__ methods
27+
:param dataclass_kwargs: kwargs for @dataclass() decorators
28+
:param kwargs:
29+
"""
30+
super().__init__(model, **kwargs)
31+
self.post_init_converters = post_init_converters
32+
self.no_meta = not meta
33+
self.dataclass_kwargs = dataclass_kwargs or {}
34+
35+
def generate(self, nested_classes: List[str] = None) -> Tuple[ImportPathList, str]:
36+
"""
37+
:param nested_classes: list of strings that contains classes code
38+
:return: list of import data, class code
39+
"""
40+
imports, code = super().generate(nested_classes)
41+
imports.append(('dataclasses', ['dataclass, field']))
42+
return imports, code
43+
44+
@property
45+
def decorators(self) -> List[str]:
46+
"""
47+
:return: List of decorators code (without @)
48+
"""
49+
return [self.DC_DECORATOR.render(kwargs=self.dataclass_kwargs)]
50+
51+
def field_data(self, name: str, meta: MetaData, optional: bool) -> Tuple[ImportPathList, dict]:
52+
"""
53+
Form field data for template
54+
55+
:param name: Field name
56+
:param meta: Field metadata
57+
:param optional: Is field optional
58+
:return: imports, field data
59+
"""
60+
imports, data = super().field_data(name, meta, optional)
61+
body_kwargs = {}
62+
if optional:
63+
meta: DOptional
64+
if isinstance(meta.type, DList):
65+
body_kwargs["default_factory"] = "list"
66+
elif isinstance(meta.type, DDict):
67+
body_kwargs["default_factory"] = "dict"
68+
else:
69+
body_kwargs["default"] = "None"
70+
if isclass(meta.type) and issubclass(meta.type, StringSerializable):
71+
pass
72+
elif isclass(meta) and issubclass(meta, StringSerializable):
73+
pass
74+
75+
if not self.no_meta:
76+
body_kwargs["metadata"] = {METADATA_FIELD_NAME: name}
77+
if len(body_kwargs) == 1 and next(iter(body_kwargs.keys())) == "default":
78+
data["body"] = body_kwargs["default"]
79+
elif body_kwargs:
80+
data["body"] = self.DC_FIELD.render(kwargs=sort_kwargs(body_kwargs, DEFAULT_ORDER))
81+
return imports, data

test/test_cli/test_script.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,9 @@ def _validate_result(proc: subprocess.Popen) -> Tuple[str, str]:
7979
assert stdout, stdout
8080
assert proc.returncode == 0
8181
# Note: imp package is deprecated but I can't find a way to create dummy module using importlib
82-
module = imp.new_module("model")
83-
exec(compile(stdout, "model.py", "exec"), module.__dict__)
82+
module = imp.new_module("test_model")
83+
sys.modules["test_model"] = module
84+
exec(compile(stdout, "test_model.py", "exec"), module.__dict__)
8485
return stdout, stderr
8586

8687

@@ -100,6 +101,15 @@ def test_script_attrs(command):
100101
print(stdout)
101102

102103

104+
@pytest.mark.parametrize("command", test_commands)
105+
def test_script_dataclasses(command):
106+
command += " -f dataclasses"
107+
proc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
108+
stdout, stderr = _validate_result(proc)
109+
assert "@dataclass" in stdout
110+
print(stdout)
111+
112+
103113
@pytest.mark.parametrize("command", test_commands)
104114
def test_script_custom(command):
105115
command += " -f custom --code-generator json_to_models.models.attr.AttrsModelCodeGenerator"

test/test_code_generation/test_attrs_generation.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
from json_to_models.dynamic_typing import (DDict, DList, DOptional, FloatString, IntString, ModelMeta, compile_imports)
66
from json_to_models.models import sort_fields
7-
from json_to_models.models.attr import AttrsModelCodeGenerator, METADATA_FIELD_NAME, sort_kwargs
8-
from json_to_models.models.base import generate_code
7+
from json_to_models.models.attr import AttrsModelCodeGenerator, DEFAULT_ORDER
8+
from json_to_models.models.base import METADATA_FIELD_NAME, generate_code, sort_kwargs
99
from test.test_code_generation.test_models_code_generator import model_factory, trim
1010

1111

@@ -16,7 +16,7 @@ def test_attrib_kwargs_sort():
1616
converter='a',
1717
default=None,
1818
x=1,
19-
))
19+
), DEFAULT_ORDER)
2020
expected = ['default', 'converter', 'y', 'x', 'metadata']
2121
for k1, k2 in zip(sorted_kwargs.keys(), expected):
2222
assert k1 == k2

0 commit comments

Comments
 (0)