Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 3 additions & 16 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
- [X] typing code generation
- [ ] (Maybe in future) Extract to another module (by serializers for each dynamic typing class)
- [X] attrs
- [ ] dataclasses
- [X] dataclasses
- [ ] post_init converters for StringSerializable types
- [ ] generate from_json/to_json converters
- [ ] Model class -> Meta format converter
- [ ] attrs
Expand All @@ -36,24 +37,10 @@
- [X] ISO date
- [X] ISO time
- [X] ISO datetime
- [ ] Don't create metadata (RCG_ORIGINAL_FIELD) if original_field == generated_field
- [X] Cli tool

- Testing
- Models layer
- [X] Create and register models
- [X] Test pointers in the models registry
- [ ] Test whats going on with strict/non-strict merging
- [ ] Save meta-models as python code
- [X] typing code generation
- [X] attrs
- [ ] dataclasses
- [ ] generate from_json/to_json converters
- [ ] Model class -> Meta format converter
- [ ] attrs
- [ ] dataclasses
- [ ] Implement existing models registration
- [ ] attrs
- [ ] dataclasses

- Build, Deploy, CI
- [X] setup.py
Expand Down
4 changes: 2 additions & 2 deletions json_to_models/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from json_to_models.models import ModelsStructureType, compose_models
from json_to_models.models.attr import AttrsModelCodeGenerator
from json_to_models.models.base import GenericModelCodeGenerator, generate_code
from json_to_models.models.dataclasses import DataclassModelCodeGenerator
from json_to_models.registry import (
ModelCmp, ModelFieldsEquals, ModelFieldsNumberMatch, ModelFieldsPercentMatch, ModelRegistry
)
Expand All @@ -40,8 +41,7 @@ class Cli:
MODEL_GENERATOR_MAPPING: Dict[str, Type[GenericModelCodeGenerator]] = {
"base": convert_args(GenericModelCodeGenerator),
"attrs": convert_args(AttrsModelCodeGenerator, meta=bool_js_style),
# TODO: vvvv
"dataclasses": None
"dataclasses": convert_args(DataclassModelCodeGenerator, meta=bool_js_style, post_init_converters=bool_js_style)
}

def __init__(self):
Expand Down
36 changes: 7 additions & 29 deletions json_to_models/models/attr.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
from inspect import isclass
from typing import Iterable, List, Tuple
from typing import List, Tuple

from .base import GenericModelCodeGenerator, template
from ..dynamic_typing import DList, DOptional, ImportPathList, MetaData, ModelMeta, StringSerializable

METADATA_FIELD_NAME = "RCG_ORIGINAL_FIELD"
KWAGRS_TEMPLATE = "{% for key, value in kwargs.items() %}" \
"{{ key }}={{ value }}" \
"{% if not loop.last %}, {% endif %}" \
"{% endfor %}"
from .base import GenericModelCodeGenerator, KWAGRS_TEMPLATE, METADATA_FIELD_NAME, sort_kwargs, template
from ..dynamic_typing import DDict, DList, DOptional, ImportPathList, MetaData, ModelMeta, StringSerializable

DEFAULT_ORDER = (
("default", "converter", "factory"),
Expand All @@ -17,24 +11,6 @@
)


def sort_kwargs(kwargs: dict, ordering: Iterable[Iterable[str]] = DEFAULT_ORDER) -> dict:
sorted_dict_1 = {}
sorted_dict_2 = {}
current = sorted_dict_1
for group in ordering:
if isinstance(group, str):
if group != "*":
raise ValueError(f"Unknown kwarg group: {group}")
current = sorted_dict_2
else:
for item in group:
if item in kwargs:
value = kwargs.pop(item)
current[item] = value
sorted_dict = {**sorted_dict_1, **kwargs, **sorted_dict_2}
return sorted_dict


class AttrsModelCodeGenerator(GenericModelCodeGenerator):
ATTRS = template("attr.s"
"{% if kwargs %}"
Expand All @@ -45,7 +21,7 @@ class AttrsModelCodeGenerator(GenericModelCodeGenerator):
def __init__(self, model: ModelMeta, meta=False, attrs_kwargs: dict = None, **kwargs):
"""
:param model: ModelMeta instance
:param no_meta: Disable generation of metadata as attrib argument
:param meta: Enable generation of metadata as attrib argument
:param attrs_kwargs: kwargs for @attr.s() decorators
:param kwargs:
"""
Expand Down Expand Up @@ -84,6 +60,8 @@ def field_data(self, name: str, meta: MetaData, optional: bool) -> Tuple[ImportP
meta: DOptional
if isinstance(meta.type, DList):
body_kwargs["factory"] = "list"
elif isinstance(meta.type, DDict):
body_kwargs["factory"] = "dict"
else:
body_kwargs["default"] = "None"
if isclass(meta.type) and issubclass(meta.type, StringSerializable):
Expand All @@ -94,5 +72,5 @@ def field_data(self, name: str, meta: MetaData, optional: bool) -> Tuple[ImportP

if not self.no_meta:
body_kwargs["metadata"] = {METADATA_FIELD_NAME: name}
data["body"] = self.ATTRIB.render(kwargs=sort_kwargs(body_kwargs))
data["body"] = self.ATTRIB.render(kwargs=sort_kwargs(body_kwargs, DEFAULT_ORDER))
return imports, data
26 changes: 25 additions & 1 deletion json_to_models/models/base.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
from typing import List, Tuple, Type
from typing import Iterable, List, Tuple, Type

import inflection
from jinja2 import Template

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

METADATA_FIELD_NAME = "RCG_ORIGINAL_FIELD"
KWAGRS_TEMPLATE = "{% for key, value in kwargs.items() %}" \
"{{ key }}={{ value }}" \
"{% if not loop.last %}, {% endif %}" \
"{% endfor %}"


def template(pattern: str, indent: str = INDENT) -> Template:
"""
Expand Down Expand Up @@ -159,3 +165,21 @@ def generate_code(structure: ModelsStructureType, class_generator: Type[GenericM
else:
imports_str = ""
return imports_str + objects_delimiter.join(classes) + "\n"


def sort_kwargs(kwargs: dict, ordering: Iterable[Iterable[str]]) -> dict:
sorted_dict_1 = {}
sorted_dict_2 = {}
current = sorted_dict_1
for group in ordering:
if isinstance(group, str):
if group != "*":
raise ValueError(f"Unknown kwarg group: {group}")
current = sorted_dict_2
else:
for item in group:
if item in kwargs:
value = kwargs.pop(item)
current[item] = value
sorted_dict = {**sorted_dict_1, **kwargs, **sorted_dict_2}
return sorted_dict
Empty file removed json_to_models/models/dataclass.py
Empty file.
81 changes: 81 additions & 0 deletions json_to_models/models/dataclasses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from inspect import isclass
from typing import List, Tuple

from .base import GenericModelCodeGenerator, KWAGRS_TEMPLATE, METADATA_FIELD_NAME, sort_kwargs, template
from ..dynamic_typing import DDict, DList, DOptional, ImportPathList, MetaData, ModelMeta, StringSerializable

DEFAULT_ORDER = (
("default", "default_factory"),
"*",
("metadata",)
)


class DataclassModelCodeGenerator(GenericModelCodeGenerator):
DC_DECORATOR = template("dataclass"
"{% if kwargs %}"
f"({KWAGRS_TEMPLATE})"
"{% endif %}")
DC_FIELD = template(f"field({KWAGRS_TEMPLATE})")

def __init__(self, model: ModelMeta, meta=False, post_init_converters=False, dataclass_kwargs: dict = None,
**kwargs):
"""
:param model: ModelMeta instance
:param meta: Enable generation of metadata as attrib argument
:param post_init_converters: Enable generation of type converters in __post_init__ methods
:param dataclass_kwargs: kwargs for @dataclass() decorators
:param kwargs:
"""
super().__init__(model, **kwargs)
self.post_init_converters = post_init_converters
self.no_meta = not meta
self.dataclass_kwargs = dataclass_kwargs or {}

def generate(self, nested_classes: List[str] = None) -> Tuple[ImportPathList, str]:
"""
:param nested_classes: list of strings that contains classes code
:return: list of import data, class code
"""
imports, code = super().generate(nested_classes)
imports.append(('dataclasses', ['dataclass, field']))
return imports, code

@property
def decorators(self) -> List[str]:
"""
:return: List of decorators code (without @)
"""
return [self.DC_DECORATOR.render(kwargs=self.dataclass_kwargs)]

def field_data(self, name: str, meta: MetaData, optional: bool) -> Tuple[ImportPathList, dict]:
"""
Form field data for template

:param name: Field name
:param meta: Field metadata
:param optional: Is field optional
:return: imports, field data
"""
imports, data = super().field_data(name, meta, optional)
body_kwargs = {}
if optional:
meta: DOptional
if isinstance(meta.type, DList):
body_kwargs["default_factory"] = "list"
elif isinstance(meta.type, DDict):
body_kwargs["default_factory"] = "dict"
else:
body_kwargs["default"] = "None"
if isclass(meta.type) and issubclass(meta.type, StringSerializable):
pass
elif isclass(meta) and issubclass(meta, StringSerializable):
pass

if not self.no_meta:
body_kwargs["metadata"] = {METADATA_FIELD_NAME: name}
if len(body_kwargs) == 1 and next(iter(body_kwargs.keys())) == "default":
data["body"] = body_kwargs["default"]
elif body_kwargs:
data["body"] = self.DC_FIELD.render(kwargs=sort_kwargs(body_kwargs, DEFAULT_ORDER))
return imports, data
14 changes: 12 additions & 2 deletions test/test_cli/test_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,9 @@ def _validate_result(proc: subprocess.Popen) -> Tuple[str, str]:
assert stdout, stdout
assert proc.returncode == 0
# Note: imp package is deprecated but I can't find a way to create dummy module using importlib
module = imp.new_module("model")
exec(compile(stdout, "model.py", "exec"), module.__dict__)
module = imp.new_module("test_model")
sys.modules["test_model"] = module
exec(compile(stdout, "test_model.py", "exec"), module.__dict__)
return stdout, stderr


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


@pytest.mark.parametrize("command", test_commands)
def test_script_dataclasses(command):
command += " -f dataclasses"
proc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = _validate_result(proc)
assert "@dataclass" in stdout
print(stdout)


@pytest.mark.parametrize("command", test_commands)
def test_script_custom(command):
command += " -f custom --code-generator json_to_models.models.attr.AttrsModelCodeGenerator"
Expand Down
6 changes: 3 additions & 3 deletions test/test_code_generation/test_attrs_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

from json_to_models.dynamic_typing import (DDict, DList, DOptional, FloatString, IntString, ModelMeta, compile_imports)
from json_to_models.models import sort_fields
from json_to_models.models.attr import AttrsModelCodeGenerator, METADATA_FIELD_NAME, sort_kwargs
from json_to_models.models.base import generate_code
from json_to_models.models.attr import AttrsModelCodeGenerator, DEFAULT_ORDER
from json_to_models.models.base import METADATA_FIELD_NAME, generate_code, sort_kwargs
from test.test_code_generation.test_models_code_generator import model_factory, trim


Expand All @@ -16,7 +16,7 @@ def test_attrib_kwargs_sort():
converter='a',
default=None,
x=1,
))
), DEFAULT_ORDER)
expected = ['default', 'converter', 'y', 'x', 'metadata']
for k1, k2 in zip(sorted_kwargs.keys(), expected):
assert k1 == k2
Expand Down
Loading