Skip to content

Commit

Permalink
Merge 76c9ed6 into 66a25c8
Browse files Browse the repository at this point in the history
  • Loading branch information
bogdandm committed Nov 28, 2018
2 parents 66a25c8 + 76c9ed6 commit 4c30a55
Show file tree
Hide file tree
Showing 9 changed files with 295 additions and 53 deletions.
19 changes: 3 additions & 16 deletions TODO.md
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
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
@@ -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
@@ -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
@@ -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
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
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

0 comments on commit 4c30a55

Please sign in to comment.