Skip to content

Commit d5ce672

Browse files
committed
Generate post_init converters for StringSerializable types in dataclasses;
Change GenericModelCodeGenerator.decorators signature
1 parent 49b6a38 commit d5ce672

6 files changed

Lines changed: 157 additions & 42 deletions

File tree

json_to_models/models/attr.py

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,9 @@ def __init__(self, model: ModelMeta, meta=False, attrs_kwargs: dict = None, **kw
2929
self.no_meta = not meta
3030
self.attrs_kwargs = attrs_kwargs or {}
3131

32-
def generate(self, nested_classes: List[str] = None) -> Tuple[ImportPathList, str]:
33-
"""
34-
:param nested_classes: list of strings that contains classes code
35-
:return: list of import data, class code
36-
"""
37-
imports, code = super().generate(nested_classes)
38-
imports.append(('attr', None))
39-
return imports, code
40-
4132
@property
42-
def decorators(self) -> List[str]:
43-
"""
44-
:return: List of decorators code (without @)
45-
"""
46-
return [self.ATTRS.render(kwargs=self.attrs_kwargs)]
33+
def decorators(self) -> Tuple[ImportPathList, List[str]]:
34+
return [('attr', None)], [self.ATTRS.render(kwargs=self.attrs_kwargs)]
4735

4836
def field_data(self, name: str, meta: MetaData, optional: bool) -> Tuple[ImportPathList, dict]:
4937
"""

json_to_models/models/base.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from . import INDENT, ModelsStructureType, OBJECTS_DELIMITER, indent, sort_fields
1010
from ..dynamic_typing import AbsoluteModelRef, ImportPathList, MetaData, ModelMeta, compile_imports, metadata_to_typing
11+
from ..utils import cached_classmethod
1112

1213
METADATA_FIELD_NAME = "J2M_ORIGINAL_FIELD"
1314
KWAGRS_TEMPLATE = "{% for key, value in kwargs.items() %}" \
@@ -18,6 +19,7 @@
1819
keywords_set = set(keyword.kwlist)
1920
ones = ['', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine']
2021

22+
2123
def template(pattern: str, indent: str = INDENT) -> Template:
2224
"""
2325
Remove indent from triple-quotes string and return jinja2.Template instance
@@ -56,36 +58,41 @@ class {{ name }}:
5658
{%- else %}
5759
pass
5860
{%- endif -%}
61+
{%- if extra %}
62+
{{ extra }}
63+
{%- endif -%}
5964
""")
6065

6166
FIELD: Template = template("{{name}}: {{type}}{% if body %} = {{ body }}{% endif %}")
6267

6368
def __init__(self, model: ModelMeta, **kwargs):
6469
self.model = model
6570

66-
def generate(self, nested_classes: List[str] = None) -> Tuple[ImportPathList, str]:
71+
def generate(self, nested_classes: List[str] = None, extra: str = "") -> Tuple[ImportPathList, str]:
6772
"""
6873
:param nested_classes: list of strings that contains classes code
6974
:return: list of import data, class code
7075
"""
7176
imports, fields = self.fields
77+
decorator_imports, decorators = self.decorators
7278
data = {
73-
"decorators": self.decorators,
79+
"decorators": decorators,
7480
"name": self.model.name,
75-
"fields": fields
81+
"fields": fields,
82+
"extra": extra
7683
}
7784
if nested_classes:
7885
data["nested"] = [indent(s) for s in nested_classes]
7986
return imports, self.BODY.render(**data)
8087

8188
@property
82-
def decorators(self) -> List[str]:
89+
def decorators(self) -> Tuple[ImportPathList, List[str]]:
8390
"""
84-
:return: List of decorators code (without @)
91+
:return: List of imports and List of decorators code (without @)
8592
"""
86-
return []
93+
return [], []
8794

88-
@classmethod
95+
@cached_classmethod
8996
def convert_field_name(cls, name):
9097
if name in keywords_set:
9198
name += "_"

json_to_models/models/dataclasses.py

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,60 @@
1111
)
1212

1313

14+
def f(self):
15+
for name in ('',):
16+
t = self.__annotations__[name]
17+
setattr(self, name, t.to_internal_value(getattr(self, name)))
18+
19+
20+
def dataclass_post_init_converters(str_fields: List[str]):
21+
"""
22+
Method factory. Return post_init method to convert string into StringSerializable types
23+
To override generated __post_init__ you can call it directly:
24+
25+
>>> def __post_init__(self):
26+
... dataclass_post_init_converters(['a', 'b'])(self)
27+
28+
:param str_fields: names of StringSerializable fields
29+
:return: __post_init__ method
30+
"""
31+
32+
def __post_init__(self):
33+
for name in (str_fields):
34+
t = self.__annotations__[name]
35+
setattr(self, name, t.to_internal_value(getattr(self, name)))
36+
37+
return __post_init__
38+
39+
40+
def convert_strings(str_fields: List[str]):
41+
"""
42+
Decorator factory. Set up `__post_init__` method to convert strings fields values into StringSerializable types
43+
44+
:param str_fields: names of StringSerializable fields
45+
:return: Class decorator
46+
"""
47+
48+
def decorator(cls):
49+
if hasattr(cls, '__post__init__'):
50+
old_fn = cls.__post__init__
51+
52+
def __post__init__(self, *args, **kwargs):
53+
dataclass_post_init_converters(str_fields)(self)
54+
old_fn(self, *args, **kwargs)
55+
56+
setattr(cls, '__post_init__', __post__init__)
57+
else:
58+
setattr(cls, '__post_init__', dataclass_post_init_converters(str_fields))
59+
60+
return cls
61+
62+
return decorator
63+
64+
1465
class DataclassModelCodeGenerator(GenericModelCodeGenerator):
15-
DC_DECORATOR = template("dataclass"
16-
"{% if kwargs %}"
17-
f"({KWAGRS_TEMPLATE})"
18-
"{% endif %}")
66+
DC_DECORATOR = template(f"dataclass{{% if kwargs %}}({KWAGRS_TEMPLATE}){{% endif %}}")
67+
DC_CONVERT_DECORATOR = template("convert_strings({{ str_fields }})")
1968
DC_FIELD = template(f"field({KWAGRS_TEMPLATE})")
2069

2170
def __init__(self, model: ModelMeta, meta=False, post_init_converters=False, dataclass_kwargs: dict = None,
@@ -32,21 +81,17 @@ def __init__(self, model: ModelMeta, meta=False, post_init_converters=False, dat
3281
self.no_meta = not meta
3382
self.dataclass_kwargs = dataclass_kwargs or {}
3483

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-
4484
@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)]
85+
def decorators(self) -> Tuple[ImportPathList, List[str]]:
86+
imports = [('dataclasses', ['dataclass', 'field'])]
87+
decorators = [self.DC_DECORATOR.render(kwargs=self.dataclass_kwargs)]
88+
if self.post_init_converters:
89+
str_fields = [self.convert_field_name(name) for name, t in self.model.type.items()
90+
if isclass(t) and issubclass(t, StringSerializable)]
91+
if str_fields:
92+
imports.append(('json_to_models.models.dataclasses', ['dataclass_post_init_converters']))
93+
decorators.append(self.DC_CONVERT_DECORATOR.render(str_fields=str_fields))
94+
return imports, decorators
5095

5196
def field_data(self, name: str, meta: MetaData, optional: bool) -> Tuple[ImportPathList, dict]:
5297
"""

json_to_models/utils.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,40 @@ def decorator(fn):
9595
return convert_args(fn, *args_converters, **kwargs_converters)
9696

9797
return decorator
98+
99+
100+
def cached_method(func: Callable):
101+
"""
102+
Decorator to cache method return values
103+
"""
104+
null = object()
105+
106+
@wraps(func)
107+
def cached_fn(self, *args):
108+
if getattr(self, '__cache__', None) is None:
109+
setattr(self, '__cache__', {})
110+
value = self.__cache__.get(args, null)
111+
if value is null:
112+
value = func(self, *args)
113+
self.__cache__[args] = value
114+
return value
115+
116+
return cached_fn
117+
118+
119+
def cached_classmethod(func: Callable):
120+
"""
121+
Decorator to cache classmethod return values
122+
"""
123+
cache = {}
124+
null = object()
125+
126+
@wraps(func)
127+
def cached_fn(cls, *args):
128+
value = cache.get(args, null)
129+
if value is null:
130+
value = func(cls, *args)
131+
cache[args] = value
132+
return value
133+
134+
return classmethod(cached_fn)

test/test_etc.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
import pytest
44
from inflection import singularize
55

6-
from json_to_models.utils import Index, convert_args_decorator, distinct_words, json_format
6+
from json_to_models.utils import (Index, cached_classmethod, cached_method, convert_args_decorator, distinct_words,
7+
json_format)
78

89
test_distinct_words_data = [
910
pytest.param(['test', 'foo', 'bar'], {'test', 'foo', 'bar'}),
@@ -63,3 +64,40 @@ def test_convert_args_decorator():
6364
assert f('1', b='1.5') == 2.5
6465
a = A("2.3", "7.5")
6566
assert a.value == 9.8
67+
68+
69+
def test_cached_methods():
70+
class A:
71+
x = []
72+
73+
def __init__(self):
74+
self.y = []
75+
76+
@cached_method
77+
def f(self, a):
78+
self.y.append(a)
79+
return a
80+
81+
@cached_classmethod
82+
def g(cls, a):
83+
cls.x.append(a)
84+
return a
85+
86+
A.g('a')
87+
A.g('a')
88+
a = A()
89+
A.g('b')
90+
A.g('a')
91+
a.f('b')
92+
a.f('b')
93+
a.f('a')
94+
a.f('a')
95+
b = A()
96+
a.f('b')
97+
b.f('a')
98+
b.g('c')
99+
100+
assert A.x == ['a', 'b', 'c']
101+
assert a.y == ['b', 'a']
102+
assert b.y == ['a']
103+
assert a.x == ['a', 'b', 'c']

testing_tools/real_apis/f1.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
from json_to_models.dynamic_typing import register_datetime_classes
99
from json_to_models.generator import MetadataGenerator
1010
from json_to_models.models import compose_models
11-
from json_to_models.models.attr import AttrsModelCodeGenerator
1211
from json_to_models.models.base import generate_code
12+
from json_to_models.models.dataclasses import DataclassModelCodeGenerator
1313
from json_to_models.registry import ModelRegistry
1414
from json_to_models.utils import json_format
1515
from testing_tools.pprint_meta_data import pretty_format_meta
@@ -61,7 +61,7 @@ def main():
6161
print('\n', json_format([structure[0], {str(a): str(b) for a, b in structure[1].items()}]))
6262
print("=" * 20)
6363

64-
print(generate_code(structure, AttrsModelCodeGenerator))
64+
print(generate_code(structure, DataclassModelCodeGenerator, class_generator_kwargs={"post_init_converters": True}))
6565

6666

6767
if __name__ == '__main__':

0 commit comments

Comments
 (0)