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
1 change: 1 addition & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
- (!) README.md
- Remove OrderedDict (dictionaries in Python 3.7 are now ordered)
- Features
- Models layer
- [X] Data variant converting
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
python-dateutil==2.7.*
inflection==0.3.*
unidecode==1.0.*
ordered-set==3.0.*
ordered-set==3.0.*
Jinja2==2.10.*
48 changes: 41 additions & 7 deletions rest_client_gen/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Dict, Generic, Iterable, List, Set, TypeVar
from typing import Dict, Generic, Iterable, List, Set, Tuple, TypeVar

from rest_client_gen.dynamic_typing import DOptional
from ..dynamic_typing import ModelMeta, ModelPtr

Index = str
Expand Down Expand Up @@ -59,7 +60,10 @@ def extract_root(model: ModelMeta) -> Set[Index]:
return roots


def compose_models(models_map: Dict[str, ModelMeta]):
def compose_models(models_map: Dict[str, ModelMeta]) -> List[dict]:
"""
Generate nested sorted models structure for internal usage.
"""
root_models = ListEx()
root_nested_ix = 0
structure_hash_table: Dict[Index, dict] = {
Expand All @@ -81,8 +85,10 @@ def compose_models(models_map: Dict[str, ModelMeta]):
else:
parents = {ptr.parent.index for ptr in pointers}
struct = structure_hash_table[key]
# FIXME: "Model is using by single root model" case for the time being will be disabled
# until solution to make typing ref such as 'Parent.Child' will be found
# Model is using by other models
if has_root_pointers or len(parents) > 1 and len(struct["roots"]) > 1:
if has_root_pointers or len(parents) > 1: # and len(struct["roots"]) > 1
# Model is using by different root models
try:
root_models.insert_before(
Expand All @@ -92,14 +98,42 @@ def compose_models(models_map: Dict[str, ModelMeta]):
except ValueError:
root_models.insert(root_nested_ix, struct)
root_nested_ix += 1
elif len(parents) > 1 and len(struct["roots"]) == 1:
# Model is using by single root model
parent = structure_hash_table[struct["roots"][0]]
parent["nested"].insert(0, struct)
# elif len(parents) > 1 and len(struct["roots"]) == 1:
# # Model is using by single root model
# parent = structure_hash_table[struct["roots"][0]]
# parent["nested"].insert(0, struct)
else:
# Model is using by only one model
parent = structure_hash_table[next(iter(parents))]
struct = structure_hash_table[key]
parent["nested"].append(struct)

return root_models


def sort_fields(model_meta: ModelMeta) -> Tuple[List[str], List[str]]:
"""
Split fields into required and optional groups

:return: two list of fields names: required fields, optional fields
"""
fields = model_meta.type
required = []
optional = []
for key, meta in fields.items():
if isinstance(meta, DOptional):
optional.append(key)
else:
required.append(key)
return required, optional


INDENT = " " * 4
OBJECTS_DELIMITER = "\n" * 3 # 2 blank lines


def indent(string: str, lvl: int = 1, indent: str = INDENT) -> str:
"""
Indent all lines of string by ``indent * lvl``
"""
return "\n".join(indent * lvl + line for line in string.split("\n"))
157 changes: 157 additions & 0 deletions rest_client_gen/models/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
from typing import List, Tuple, Type

from jinja2 import Template

from rest_client_gen.dynamic_typing import compile_imports
from rest_client_gen.models import INDENT, OBJECTS_DELIMITER
from . import indent, sort_fields
from ..dynamic_typing import ImportPathList, MetaData, ModelMeta, metadata_to_typing


def template(pattern: str, indent: str = INDENT) -> Template:
"""
Remove indent from triple-quotes string and return jinja2.Template instance
"""
if "\n" in pattern:
n = len(indent)
lines = pattern.split("\n")
for i in (0, -1):
if not lines[i].strip():
del lines[i]

pattern = "\n".join(line[n:] if line[:n] == indent else line
for line in lines)
return Template(pattern)


class GenericModelCodeGenerator:
"""
Core of model code generator. Extend it to customize fields of model or add some decorators.
Note that this class has nothing to do with models structure. It only can add nested models as strings.
"""
BODY = template("""
{%- for decorator in decorators -%}
@{{ decorator }}
{% endfor -%}
class {{ name }}:

{%- for code in nested %}
{{ code }}
{% endfor -%}

{%- for field in fields %}
{{ field }}
{%- endfor %}
""")

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

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


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, fields = self.fields
data = {
"decorators": self.decorators,
"name": self.model.name,
"fields": fields
}
if nested_classes:
data["nested"] = [indent(s) for s in nested_classes]
return imports, self.BODY.render(**data)

@property
def decorators(self) -> List[str]:
"""
:return: List of decorators code (without @)
"""
return []

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, typing = metadata_to_typing(meta)
data = {
"name": name,
"type": typing
}
return imports, data

@property
def fields(self) -> Tuple[ImportPathList, List[str]]:
"""
Generate fields strings

:return: imports, list of fields as string
"""
required, optional = sort_fields(self.model)
imports: ImportPathList = []
strings: List[str] = []
for is_optional, fields in enumerate((required, optional)):
for field in fields:
field_imports, data = self.field_data(field, self.model.type[field], bool(is_optional))
imports.extend(field_imports)
strings.append(self.FIELD.render(**data))
return imports, strings


def _generate_code(
structure: List[dict],
class_generator: Type[GenericModelCodeGenerator],
class_generator_kwargs: dict,
lvl=0
) -> Tuple[ImportPathList, List[str]]:
"""
Walk thought models structure and covert them into code

:param structure: Result of compose_models or similar function
:param class_generator: GenericModelCodeGenerator subclass
:param class_generator_kwargs: kwags for GenericModelCodeGenerator init
:param lvl: Recursion depth
:return: imports, list of first lvl classes
"""
imports = []
classes = []
for data in structure:
nested_imports, nested_classes = _generate_code(
data["nested"],
class_generator,
class_generator_kwargs,
lvl=lvl + 1
)
imports.extend(nested_imports)
gen = class_generator(data["model"], **class_generator_kwargs)
cls_imports, cls_string = gen.generate(nested_classes)
imports.extend(cls_imports)
classes.append(cls_string)
return imports, classes


def generate_code(structure: List[dict], class_generator: Type[GenericModelCodeGenerator],
class_generator_kwargs: dict = None, objects_delimiter: str = OBJECTS_DELIMITER) -> str:
"""
Generate ready-to-use code

:param structure: Result of compose_models or similar function
:param class_generator: GenericModelCodeGenerator subclass
:param class_generator_kwargs: kwags for GenericModelCodeGenerator init
:param objects_delimiter: Delimiter between root level classes
:return: Generated code
"""
imports, classes = _generate_code(structure, class_generator, class_generator_kwargs or {})
if imports:
imports_str = compile_imports(imports) + objects_delimiter
else:
imports_str = ""
return imports_str + objects_delimiter.join(classes) + "\n"
Loading