From f9ba763a6d4e6aacc243f271a7a89df8bd5f8f26 Mon Sep 17 00:00:00 2001 From: bogdandm Date: Thu, 13 Jun 2019 13:27:37 +0300 Subject: [PATCH 1/4] Unicode conversion control; Convert unicode in class name; Add output field argument; --- TODO.md | 7 +- json_to_models/cli.py | 102 ++++++++++++++++++--------- json_to_models/generator.py | 1 + json_to_models/models/attr.py | 5 +- json_to_models/models/base.py | 37 ++++++---- json_to_models/models/dataclasses.py | 5 +- json_to_models/utils.py | 10 ++- 7 files changed, 107 insertions(+), 60 deletions(-) diff --git a/TODO.md b/TODO.md index 202f2f7..100f4a4 100644 --- a/TODO.md +++ b/TODO.md @@ -1,4 +1,6 @@ -- (!) README.md +- README + - [ ] Restrictions + - [ ] Low lvl API wiki or sphinx docs - Docstrings - Features - Models layer @@ -35,10 +37,11 @@ - [ ] Complex python types annotations - [ ] Decorator to specify field metatype - [ ] Specify metatype in attr/dataclass argument (if dataclasses has such) - - [X] String based types (Warning: 6 times slow down) + - String based types (Warning: 6 times slow down) - [X] ISO date - [X] ISO time - [X] ISO datetime + - [ ] Web addresses (www, http, https, etc.) - [X] Don't create metadata (J2M_ORIGINAL_FIELD) if original_field == generated_field - [X] Decode unicode in keys - [X] Cli tool diff --git a/json_to_models/cli.py b/json_to_models/cli.py index 8144e90..2488eb7 100644 --- a/json_to_models/cli.py +++ b/json_to_models/cli.py @@ -4,23 +4,24 @@ import json import os.path import re +import sys from collections import defaultdict from datetime import datetime from pathlib import Path from typing import Any, Callable, Dict, Generator, Iterable, List, Tuple, Type, Union -import json_to_models -from json_to_models.dynamic_typing import ModelMeta, register_datetime_classes -from json_to_models.generator import MetadataGenerator -from json_to_models.models import ModelsStructureType -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.models.structure import compose_models, compose_models_flat -from json_to_models.registry import ( +from . import __version__ as VERSION +from .dynamic_typing import ModelMeta, register_datetime_classes +from .generator import MetadataGenerator +from .models import ModelsStructureType +from .models.attr import AttrsModelCodeGenerator +from .models.base import GenericModelCodeGenerator, generate_code +from .models.dataclasses import DataclassModelCodeGenerator +from .models.structure import compose_models, compose_models_flat +from .registry import ( ModelCmp, ModelFieldsEquals, ModelFieldsNumberMatch, ModelFieldsPercentMatch, ModelRegistry ) -from json_to_models.utils import convert_args +from .utils import convert_args STRUCTURE_FN_TYPE = Callable[[Dict[str, ModelMeta]], ModelsStructureType] bool_js_style = lambda s: {"true": True, "false": False}.get(s, None) @@ -75,7 +76,9 @@ def parse_args(self, args: List[str] = None): (model_name, (lookup, Path(path))) for model_name, lookup, path in namespace.list or () ] + self.output_file = namespace.output self.enable_datetime = namespace.datetime + disable_unicode_conversion = namespace.disable_unicode_conversion self.strings_converters = namespace.strings_converters merge_policy = [m.split("_") if "_" in m else m for m in namespace.merge] structure = namespace.structure @@ -88,7 +91,7 @@ def parse_args(self, args: List[str] = None): self.validate(models, models_lists, merge_policy, framework, code_generator) self.setup_models_data(models, models_lists) self.set_args(merge_policy, structure, framework, code_generator, code_generator_kwargs_raw, - dict_keys_regex, dict_keys_fields) + dict_keys_regex, dict_keys_fields, disable_unicode_conversion) def run(self): if self.enable_datetime: @@ -104,7 +107,23 @@ def run(self): registry.merge_models(generator) registry.generate_names() structure = self.structure_fn(registry.models_map) - return generate_code(structure, self.model_generator, class_generator_kwargs=self.model_generator_kwargs) + output = self.version_string + \ + generate_code(structure, self.model_generator, class_generator_kwargs=self.model_generator_kwargs) + if self.output_file: + with open(self.output_file, "w", encoding="utf-8") as f: + f.write(output) + return f"Output is written to {self.output_file}" + else: + return output + + @property + def version_string(self): + return ( + 'r"""\n' + f'generated by json2python-models v{VERSION} at {datetime.now().ctime()}\n' + f'command: {" ".join(sys.argv)}\n' + '"""\n' + ) def validate(self, models, models_list, merge_policy, framework, code_generator): """ @@ -149,9 +168,17 @@ def setup_models_data(self, models: Iterable[Tuple[str, Iterable[Path]]], for model_name, list_of_gen in models_dict.items() } - def set_args(self, merge_policy: List[Union[List[str], str]], - structure: str, framework: str, code_generator: str, code_generator_kwargs_raw: List[str], - dict_keys_regex: List[str], dict_keys_fields: List[str]): + def set_args( + self, + merge_policy: List[Union[List[str], str]], + structure: str, + framework: str, + code_generator: str, + code_generator_kwargs_raw: List[str], + dict_keys_regex: List[str], + dict_keys_fields: List[str], + disable_unicode_conversion: bool + ): """ Convert CLI args to python representation and set them to appropriate object attributes """ @@ -175,6 +202,7 @@ def set_args(self, merge_policy: List[Union[List[str], str]], self.model_generator = getattr(m, cls) self.model_generator_kwargs = {} if not self.strings_converters else {'post_init_converters': True} + self.model_generator_kwargs['convert_unicode'] = not disable_unicode_conversion if code_generator_kwargs_raw: for item in code_generator_kwargs_raw: if item[0] == '"': @@ -216,6 +244,11 @@ def _create_argparser(cls) -> argparse.ArgumentParser: "I.e. for file that contains dict {\"a\": {\"b\": [model_data, ...]}} you should\n" "pass 'a.b' as .\n\n" ) + parser.add_argument( + "-o", "--output", + metavar="FILE", default="", + help="Path to output file\n\n" + ) parser.add_argument( "-f", "--framework", default="base", @@ -243,6 +276,11 @@ def _create_argparser(cls) -> argparse.ArgumentParser: action="store_true", help="Enable generation of string types converters (i.e. IsoDatetimeString or BooleanString).\n\n" ) + parser.add_argument( + "--disable-unicode-conversion", "--no-unidecode", + action="store_true", + help="Disabling unicode conversion in fields and class names.\n\n" + ) default_percent = f"{ModelFieldsPercentMatch.DEFAULT * 100:.0f}" default_number = f"{ModelFieldsNumberMatch.DEFAULT:.0f}" @@ -250,15 +288,17 @@ def _create_argparser(cls) -> argparse.ArgumentParser: "--merge", default=["percent", "number"], nargs="+", - help=f"Merge policy settings. Default is 'percent_{default_percent} number_{default_number}' (percent of field match\n" - "or number of fields match).\n" - "Possible values are:\n" - "'percent[_]' - two models had a certain percentage of matched field names.\n" - f" Default percent is {default_percent}%%. " - "Custom value could be i.e. 'percent_95'.\n" - "'number[_]' - two models had a certain number of matched field names.\n" - f" Default number of fields is {default_number}.\n" - "'exact' - two models should have exact same field names to merge.\n\n" + help=( + f"Merge policy settings. Default is 'percent_{default_percent} number_{default_number}' (percent of field match\n" + "or number of fields match).\n" + "Possible values are:\n" + "'percent[_]' - two models had a certain percentage of matched field names.\n" + f" Default percent is {default_percent}%%. " + "Custom value could be i.e. 'percent_95'.\n" + "'number[_]' - two models had a certain number of matched field names.\n" + f" Default number of fields is {default_number}.\n" + "'exact' - two models should have exact same field names to merge.\n\n" + ) ) parser.add_argument( "--dict-keys-regex", "--dkr", @@ -293,8 +333,7 @@ def _create_argparser(cls) -> argparse.ArgumentParser: return parser -def main(version_string=None): - import sys +def main(): import os if os.getenv("TRAVIS", None) or os.getenv("FORCE_COVERAGE", None): @@ -305,14 +344,7 @@ def main(version_string=None): cli = Cli() cli.parse_args() - if not version_string: - version_string = ( - 'r"""\n' - f'generated by json2python-models v{json_to_models.__version__} at {datetime.now().ctime()}\n' - f'command: {" ".join(sys.argv)}\n' - '"""\n' - ) - print(version_string + cli.run()) + print(cli.run()) def path_split(path: str) -> List[str]: @@ -374,7 +406,7 @@ def safe_json_load(path: Path) -> Union[dict, list]: """ Open file, load json and close it. """ - with path.open() as f: + with path.open(encoding="utf-8") as f: return json.load(f) diff --git a/json_to_models/generator.py b/json_to_models/generator.py index 239a0a7..ea58881 100644 --- a/json_to_models/generator.py +++ b/json_to_models/generator.py @@ -8,6 +8,7 @@ _static_types = {float, bool, int} + class MetadataGenerator: CONVERTER_TYPE = Optional[Callable[[str], Any]] diff --git a/json_to_models/models/attr.py b/json_to_models/models/attr.py index 75bc84b..7ff31d4 100644 --- a/json_to_models/models/attr.py +++ b/json_to_models/models/attr.py @@ -15,7 +15,8 @@ class AttrsModelCodeGenerator(GenericModelCodeGenerator): ATTRS = template(f"attr.s{{% if kwargs %}}({KWAGRS_TEMPLATE}){{% endif %}}") ATTRIB = template(f"attr.ib({KWAGRS_TEMPLATE})") - def __init__(self, model: ModelMeta, meta=False, post_init_converters=False, attrs_kwargs: dict = None): + def __init__(self, model: ModelMeta, meta=False, post_init_converters=False, attrs_kwargs: dict = None, + convert_unicode=True): """ :param model: ModelMeta instance :param meta: Enable generation of metadata as attrib argument @@ -23,7 +24,7 @@ def __init__(self, model: ModelMeta, meta=False, post_init_converters=False, att :param attrs_kwargs: kwargs for @attr.s() decorators :param kwargs: """ - super().__init__(model, post_init_converters) + super().__init__(model, post_init_converters, convert_unicode) self.no_meta = not meta self.attrs_kwargs = attrs_kwargs or {} diff --git a/json_to_models/models/base.py b/json_to_models/models/base.py index 7b983a0..5489106 100644 --- a/json_to_models/models/base.py +++ b/json_to_models/models/base.py @@ -12,7 +12,7 @@ from .utils import indent from ..dynamic_typing import (AbsoluteModelRef, ImportPathList, MetaData, ModelMeta, compile_imports, metadata_to_typing) -from ..utils import cached_classmethod +from ..utils import cached_method METADATA_FIELD_NAME = "J2M_ORIGINAL_FIELD" KWAGRS_TEMPLATE = "{% for key, value in kwargs.items() %}" \ @@ -71,20 +71,19 @@ class {{ name }}: % KWAGRS_TEMPLATE) FIELD: Template = template("{{name}}: {{type}}{% if body %} = {{ body }}{% endif %}") - def __init__(self, model: ModelMeta, post_init_converters=False): + def __init__(self, model: ModelMeta, post_init_converters=False, convert_unicode=True): self.model = model self.post_init_converters = post_init_converters + self.convert_unicode = convert_unicode - @cached_classmethod - def convert_field_name(cls, name): - if name in keywords_set: - name += "_" - name = unidecode(name) - name = re.sub(r"\W", "", name) - if not ('a' <= name[0].lower() <= 'z'): - if '0' <= name[0] <= '9': - name = ones[int(name[0])] + "_" + name[1:] - return inflection.underscore(name) + @cached_method + def convert_class_name(self, name): + # TODO: Convert names in typing links (lei_da_tu: '雷达图') + return prepare_label(name, convert_unicode=self.convert_unicode) + + @cached_method + def convert_field_name(self, name): + return inflection.underscore(prepare_label(name, convert_unicode=self.convert_unicode)) def generate(self, nested_classes: List[str] = None, extra: str = "") -> Tuple[ImportPathList, str]: """ @@ -95,7 +94,7 @@ def generate(self, nested_classes: List[str] = None, extra: str = "") -> Tuple[I decorator_imports, decorators = self.decorators data = { "decorators": decorators, - "name": self.model.name, + "name": self.convert_class_name(self.model.name), "fields": fields, "extra": extra } @@ -241,3 +240,15 @@ def sort_kwargs(kwargs: dict, ordering: Iterable[Iterable[str]]) -> dict: current[item] = value sorted_dict = {**sorted_dict_1, **kwargs, **sorted_dict_2} return sorted_dict + + +def prepare_label(s: str, convert_unicode: bool) -> str: + if s in keywords_set: + s += "_" + if convert_unicode: + s = unidecode(s) + s = re.sub(r"\W", "", s) + if not ('a' <= s[0].lower() <= 'z'): + if '0' <= s[0] <= '9': + s = ones[int(s[0])] + "_" + s[1:] + return s diff --git a/json_to_models/models/dataclasses.py b/json_to_models/models/dataclasses.py index 12ffcf2..e9123cb 100644 --- a/json_to_models/models/dataclasses.py +++ b/json_to_models/models/dataclasses.py @@ -15,7 +15,8 @@ class DataclassModelCodeGenerator(GenericModelCodeGenerator): DC_DECORATOR = template(f"dataclass{{% if kwargs %}}({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): + def __init__(self, model: ModelMeta, meta=False, post_init_converters=False, dataclass_kwargs: dict = None, + convert_unicode=True): """ :param model: ModelMeta instance :param meta: Enable generation of metadata as attrib argument @@ -23,7 +24,7 @@ def __init__(self, model: ModelMeta, meta=False, post_init_converters=False, dat :param dataclass_kwargs: kwargs for @dataclass() decorators :param kwargs: """ - super().__init__(model, post_init_converters) + super().__init__(model, post_init_converters, convert_unicode) self.no_meta = not meta self.dataclass_kwargs = dataclass_kwargs or {} diff --git a/json_to_models/utils.py b/json_to_models/utils.py index 5b096a0..609569d 100644 --- a/json_to_models/utils.py +++ b/json_to_models/utils.py @@ -101,14 +101,13 @@ def cached_method(func: Callable): """ Decorator to cache method return values """ - null = object() @wraps(func) def cached_fn(self, *args): if getattr(self, '__cache__', None) is None: setattr(self, '__cache__', {}) - value = self.__cache__.get(args, null) - if value is null: + value = self.__cache__.get(args, ...) + if value is Ellipsis: value = func(self, *args) self.__cache__[args] = value return value @@ -121,12 +120,11 @@ def cached_classmethod(func: Callable): Decorator to cache classmethod return values """ cache = {} - null = object() @wraps(func) def cached_fn(cls, *args): - value = cache.get(args, null) - if value is null: + value = cache.get(args, ...) + if value is Ellipsis: value = func(cls, *args) cache[args] = value return value From f26d21845dd8d0b02e4b414b1ec7aa267e062693 Mon Sep 17 00:00:00 2001 From: bogdandm Date: Thu, 13 Jun 2019 13:45:06 +0300 Subject: [PATCH 2/4] Convert unicode in model references --- json_to_models/models/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/json_to_models/models/base.py b/json_to_models/models/base.py index 5489106..32fbb45 100644 --- a/json_to_models/models/base.py +++ b/json_to_models/models/base.py @@ -75,10 +75,10 @@ def __init__(self, model: ModelMeta, post_init_converters=False, convert_unicode self.model = model self.post_init_converters = post_init_converters self.convert_unicode = convert_unicode + self.model.name = self.convert_class_name(self.model.name) @cached_method def convert_class_name(self, name): - # TODO: Convert names in typing links (lei_da_tu: '雷达图') return prepare_label(name, convert_unicode=self.convert_unicode) @cached_method @@ -94,7 +94,7 @@ def generate(self, nested_classes: List[str] = None, extra: str = "") -> Tuple[I decorator_imports, decorators = self.decorators data = { "decorators": decorators, - "name": self.convert_class_name(self.model.name), + "name": self.model.name, "fields": fields, "extra": extra } From 55d2b6b82b780c4ea9cc7f56cf42a9c7ec219b14 Mon Sep 17 00:00:00 2001 From: bogdandm Date: Thu, 13 Jun 2019 15:26:17 +0300 Subject: [PATCH 3/4] Fix fields sorting with unicode model ref; --- README.md | 11 +++++++++-- json_to_models/dynamic_typing/base.py | 10 +++++++++- json_to_models/models/base.py | 2 +- json_to_models/models/structure.py | 12 +++++++++--- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5e521c5..9400394 100644 --- a/README.md +++ b/README.md @@ -162,13 +162,17 @@ Arguments: * **Example**: `-l Car - cars.json -l Person fetch_results.items.persons result.json` * **Note**: Models names under this arguments should be unique. +* `-o`, `--output` - Output file + * **Format**: `-o ` + * **Example**: `-o car_model.py` + * `-f`, `--framework` - Model framework for which python code is generated. `base` (default) mean no framework so code will be generated without any decorators and additional meta-data. * **Format**: `-f {base,attrs,dataclasses,custom}` * **Example**: `-f attrs` * **Default**: `-f base` -* `-s , --structure` - Models composition style. +* `-s`, `--structure` - Models composition style. * **Format**: `-s {nested, flat}` * **Example**: `-s flat` * **Default**: `-s nested` @@ -177,6 +181,9 @@ Arguments: * **Default**: disabled * **Warning**: This can lead to 6-7 times slowdown on large datasets. Be sure that you really need this option. +* `--disable-unicode-conversion`, `--no-unidecode` - Disable unicode conversion in field labels and class names + * **Default**: enabled + * `--strings-converters` - Enable generation of string types converters (i.e. `IsoDatetimeString` or `BooleanString`). * **Default**: disabled @@ -196,7 +203,7 @@ Arguments: * **Format**: `--dkr RegEx [RegEx ...]` * **Example**: `--dkr node_\d+ \d+_\d+_\d+` * **Note**: `^` and `$` (string borders) tokens will be added automatically but you - have escape to other special characters manually. + have to escape other special characters manually. * **Optional** * `--dict-keys-fields`, `--dkf` - List of model fields names that will be marked as dict fields diff --git a/json_to_models/dynamic_typing/base.py b/json_to_models/dynamic_typing/base.py index 5e80b6d..672cee8 100644 --- a/json_to_models/dynamic_typing/base.py +++ b/json_to_models/dynamic_typing/base.py @@ -1,5 +1,5 @@ from inspect import isclass -from typing import Iterable, List, Tuple, Union +from typing import Any, Generator, Iterable, List, Tuple, Union ImportPathList = List[Tuple[str, Union[Iterable[str], str, None]]] @@ -50,6 +50,14 @@ def _to_hash_string(self) -> str: """ raise NotImplementedError() + def iter_child(self) -> Generator['MetaData', Any, None]: + yield self + for child in self: + if isinstance(child, BaseType): + yield from child.iter_child() + else: + yield child + class UnknownType(BaseType): __slots__ = [] diff --git a/json_to_models/models/base.py b/json_to_models/models/base.py index 32fbb45..2468e83 100644 --- a/json_to_models/models/base.py +++ b/json_to_models/models/base.py @@ -143,7 +143,7 @@ def fields(self) -> Tuple[ImportPathList, List[str]]: :return: imports, list of fields as string """ - required, optional = sort_fields(self.model) + required, optional = sort_fields(self.model, unicode_fix=not self.convert_unicode) imports: ImportPathList = [] strings: List[str] = [] for is_optional, fields in enumerate((required, optional)): diff --git a/json_to_models/models/structure.py b/json_to_models/models/structure.py index cdddd69..475cd30 100644 --- a/json_to_models/models/structure.py +++ b/json_to_models/models/structure.py @@ -2,7 +2,7 @@ from . import Index, ModelsStructureType from .utils import ListEx, PositionsDict -from ..dynamic_typing import DOptional, ModelMeta, ModelPtr +from ..dynamic_typing import BaseType, DOptional, ModelMeta, ModelPtr def compose_models(models_map: Dict[str, ModelMeta]) -> ModelsStructureType: @@ -137,7 +137,7 @@ def extract_root(model: ModelMeta) -> Set[Index]: return roots -def sort_fields(model_meta: ModelMeta) -> Tuple[List[str], List[str]]: +def sort_fields(model_meta: ModelMeta, unicode_fix=False) -> Tuple[List[str], List[str]]: """ Split fields into required and optional groups @@ -145,10 +145,16 @@ def sort_fields(model_meta: ModelMeta) -> Tuple[List[str], List[str]]: """ fields = model_meta.type required = [] + required_2 = [] optional = [] for key, meta in fields.items(): if isinstance(meta, DOptional): optional.append(key) + elif unicode_fix and isinstance(meta, BaseType) and any( + isinstance(node, ModelMeta) + for node in meta.iter_child() + ): + required_2.append(key) else: required.append(key) - return required, optional + return required + required_2, optional From 05677b1fc444b39504eaf01186a8bed654bcaea5 Mon Sep 17 00:00:00 2001 From: bogdandm Date: Thu, 13 Jun 2019 15:26:33 +0300 Subject: [PATCH 4/4] Tests --- test/test_cli/data/unicode.json | 22 +++++ test/test_cli/test_script.py | 23 ++++- .../test_models_code_generator.py | 86 +++++++++++++++++++ test/test_code_generation/test_typing.py | 4 - 4 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 test/test_cli/data/unicode.json diff --git a/test/test_cli/data/unicode.json b/test/test_cli/data/unicode.json new file mode 100644 index 0000000..f0c7e1f --- /dev/null +++ b/test/test_cli/data/unicode.json @@ -0,0 +1,22 @@ +{ + "_id": "5b45bd7bfdc0754510853734", + "名称": "MaVol_screen", + "路径": "https://gitlab.com/jinniu-stra/strawman/tree/master/src/screen/MaVol_screen", + "标识符": "02180706mav001", + "备注": "", + "雷达图": { + "盈利": "4.83", + "抗风险": "4.28", + "稳定": "4.83", + "择股": "5.0" + }, + "原理说明": "对均线和成交量进行评估选股。", + "中文名称": "一阳穿多均", + "套装配置": "", + "入选理由": "该股近期在60日均线之上,当前同时上穿5、10、20和30日均线,成交量是前一日的2倍,短期强势", + "策略话术": "", + "策略优势": "", + "状态": "稳定在线", + "装备详情": "http://bbs.jinniuai.com/t/topic/69", + "截止时间": "2019-06-11 00:00:00" +} \ No newline at end of file diff --git a/test/test_cli/test_script.py b/test/test_cli/test_script.py index 87e7e0b..1988850 100644 --- a/test/test_cli/test_script.py +++ b/test/test_cli/test_script.py @@ -75,11 +75,23 @@ def test_help(): pytest.param(f"""{executable} -l User - "{test_data_path / 'users.json'}" --strings-converters""", id="users_strings_converters"), + pytest.param(f"""{executable} -m SomeUnicode "{test_data_path / 'unicode.json'}" """, + id="convert_unicode"), + pytest.param(f"""{executable} -m SomeUnicode "{test_data_path / 'unicode.json'}" --no-unidecode""", + id="dont_convert_unicode"), + pytest.param(f"""{executable} -m SomeUnicode "{test_data_path / 'unicode.json'}" --disable-unicode-conversion""", + id="dont_convert_unicode_2"), ] -def _validate_result(proc: subprocess.Popen) -> Tuple[str, str]: +def _validate_result(proc: subprocess.Popen, output=None, output_file: Path = None) -> Tuple[str, str]: stdout, stderr = map(bytes.decode, proc.communicate()) + if output_file: + assert output is None + with output_file.open(encoding='utf-8') as f: + output = f.read() + if output: + stdout = output assert not stderr, stderr assert stdout, stdout assert proc.returncode == 0 @@ -153,3 +165,12 @@ def test_wrong_arguments(command): print("Command:", command) proc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) _validate_result(proc) + + +@pytest.mark.parametrize("command", test_commands) +def test_script_output_file(command): + file = tmp_path / 'out.py' + command += f" -o {file}" + proc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = _validate_result(proc, output_file=file) + print(stdout) diff --git a/test/test_code_generation/test_models_code_generator.py b/test/test_code_generation/test_models_code_generator.py index 405e7fb..3f3cff5 100644 --- a/test/test_code_generation/test_models_code_generator.py +++ b/test/test_code_generation/test_models_code_generator.py @@ -206,3 +206,89 @@ def test_absolute_model_ref(): assert wrapper.to_typing_code()[1] == "List[List['TestModel']]" with AbsoluteModelRef.inject({test_model: test_model}): assert wrapper.to_typing_code()[1] == "List[List['TestModel.TestModel']]" + + +test_unicode_data = [ + pytest.param( + model_factory("Test", { + "foo": int, + "bar": int, + "baz": float + }), + {'convert_unicode': True}, + trim(""" + class Test: + foo: int + bar: int + baz: float + """), + id="test_pytest_setup" + ), + pytest.param( + model_factory("Test", { + "поле1": int, + "bar": int, + "baz": float + }), + {'convert_unicode': True}, + trim(""" + class Test: + pole1: int + bar: int + baz: float + """), + id="test_field_on" + ), + pytest.param( + model_factory("Test", { + "поле1": int, + "bar": int, + "baz": float + }), + {'convert_unicode': False}, + trim(""" + class Test: + поле1: int + bar: int + baz: float + """), + id="test_field_off" + ), + pytest.param( + model_factory("Тест", { + "поле1": int, + "bar": int, + "baz": float + }), + {'convert_unicode': True}, + trim(""" + class Test: + pole1: int + bar: int + baz: float + """), + id="test_field_on" + ), + pytest.param( + model_factory("Тест", { + "поле1": int, + "bar": int, + "baz": float + }), + {'convert_unicode': False}, + trim(""" + class Тест: + поле1: int + bar: int + baz: float + """), + id="test_classname_off" + ), +] + + +@pytest.mark.parametrize("value,kwargs,expected", test_unicode_data) +def test_generated(value: ModelMeta, kwargs: dict, expected: str): + generated = generate_code(([{"model": value, "nested": []}], {}), + GenericModelCodeGenerator, class_generator_kwargs=kwargs) + assert generated.rstrip() == expected, generated diff --git a/test/test_code_generation/test_typing.py b/test/test_code_generation/test_typing.py index 2923b3f..65c04df 100644 --- a/test/test_code_generation/test_typing.py +++ b/test/test_code_generation/test_typing.py @@ -56,10 +56,6 @@ def model(data: dict, name: str): return ModelPtr(meta) -class TestModel: - pass - - # MetaData | Tuple[import_stmnt, type] test_data = [ pytest.param(