Skip to content

Commit f9ba763

Browse files
committed
Unicode conversion control;
Convert unicode in class name; Add output field argument;
1 parent e9038a8 commit f9ba763

File tree

7 files changed

+107
-60
lines changed

7 files changed

+107
-60
lines changed

TODO.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
- (!) README.md
1+
- README
2+
- [ ] Restrictions
3+
- [ ] Low lvl API wiki or sphinx docs
24
- Docstrings
35
- Features
46
- Models layer
@@ -35,10 +37,11 @@
3537
- [ ] Complex python types annotations
3638
- [ ] Decorator to specify field metatype
3739
- [ ] Specify metatype in attr/dataclass argument (if dataclasses has such)
38-
- [X] String based types (Warning: 6 times slow down)
40+
- String based types (Warning: 6 times slow down)
3941
- [X] ISO date
4042
- [X] ISO time
4143
- [X] ISO datetime
44+
- [ ] Web addresses (www, http, https, etc.)
4245
- [X] Don't create metadata (J2M_ORIGINAL_FIELD) if original_field == generated_field
4346
- [X] Decode unicode in keys
4447
- [X] Cli tool

json_to_models/cli.py

Lines changed: 67 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,24 @@
44
import json
55
import os.path
66
import re
7+
import sys
78
from collections import defaultdict
89
from datetime import datetime
910
from pathlib import Path
1011
from typing import Any, Callable, Dict, Generator, Iterable, List, Tuple, Type, Union
1112

12-
import json_to_models
13-
from json_to_models.dynamic_typing import ModelMeta, register_datetime_classes
14-
from json_to_models.generator import MetadataGenerator
15-
from json_to_models.models import ModelsStructureType
16-
from json_to_models.models.attr import AttrsModelCodeGenerator
17-
from json_to_models.models.base import GenericModelCodeGenerator, generate_code
18-
from json_to_models.models.dataclasses import DataclassModelCodeGenerator
19-
from json_to_models.models.structure import compose_models, compose_models_flat
20-
from json_to_models.registry import (
13+
from . import __version__ as VERSION
14+
from .dynamic_typing import ModelMeta, register_datetime_classes
15+
from .generator import MetadataGenerator
16+
from .models import ModelsStructureType
17+
from .models.attr import AttrsModelCodeGenerator
18+
from .models.base import GenericModelCodeGenerator, generate_code
19+
from .models.dataclasses import DataclassModelCodeGenerator
20+
from .models.structure import compose_models, compose_models_flat
21+
from .registry import (
2122
ModelCmp, ModelFieldsEquals, ModelFieldsNumberMatch, ModelFieldsPercentMatch, ModelRegistry
2223
)
23-
from json_to_models.utils import convert_args
24+
from .utils import convert_args
2425

2526
STRUCTURE_FN_TYPE = Callable[[Dict[str, ModelMeta]], ModelsStructureType]
2627
bool_js_style = lambda s: {"true": True, "false": False}.get(s, None)
@@ -75,7 +76,9 @@ def parse_args(self, args: List[str] = None):
7576
(model_name, (lookup, Path(path)))
7677
for model_name, lookup, path in namespace.list or ()
7778
]
79+
self.output_file = namespace.output
7880
self.enable_datetime = namespace.datetime
81+
disable_unicode_conversion = namespace.disable_unicode_conversion
7982
self.strings_converters = namespace.strings_converters
8083
merge_policy = [m.split("_") if "_" in m else m for m in namespace.merge]
8184
structure = namespace.structure
@@ -88,7 +91,7 @@ def parse_args(self, args: List[str] = None):
8891
self.validate(models, models_lists, merge_policy, framework, code_generator)
8992
self.setup_models_data(models, models_lists)
9093
self.set_args(merge_policy, structure, framework, code_generator, code_generator_kwargs_raw,
91-
dict_keys_regex, dict_keys_fields)
94+
dict_keys_regex, dict_keys_fields, disable_unicode_conversion)
9295

9396
def run(self):
9497
if self.enable_datetime:
@@ -104,7 +107,23 @@ def run(self):
104107
registry.merge_models(generator)
105108
registry.generate_names()
106109
structure = self.structure_fn(registry.models_map)
107-
return generate_code(structure, self.model_generator, class_generator_kwargs=self.model_generator_kwargs)
110+
output = self.version_string + \
111+
generate_code(structure, self.model_generator, class_generator_kwargs=self.model_generator_kwargs)
112+
if self.output_file:
113+
with open(self.output_file, "w", encoding="utf-8") as f:
114+
f.write(output)
115+
return f"Output is written to {self.output_file}"
116+
else:
117+
return output
118+
119+
@property
120+
def version_string(self):
121+
return (
122+
'r"""\n'
123+
f'generated by json2python-models v{VERSION} at {datetime.now().ctime()}\n'
124+
f'command: {" ".join(sys.argv)}\n'
125+
'"""\n'
126+
)
108127

109128
def validate(self, models, models_list, merge_policy, framework, code_generator):
110129
"""
@@ -149,9 +168,17 @@ def setup_models_data(self, models: Iterable[Tuple[str, Iterable[Path]]],
149168
for model_name, list_of_gen in models_dict.items()
150169
}
151170

152-
def set_args(self, merge_policy: List[Union[List[str], str]],
153-
structure: str, framework: str, code_generator: str, code_generator_kwargs_raw: List[str],
154-
dict_keys_regex: List[str], dict_keys_fields: List[str]):
171+
def set_args(
172+
self,
173+
merge_policy: List[Union[List[str], str]],
174+
structure: str,
175+
framework: str,
176+
code_generator: str,
177+
code_generator_kwargs_raw: List[str],
178+
dict_keys_regex: List[str],
179+
dict_keys_fields: List[str],
180+
disable_unicode_conversion: bool
181+
):
155182
"""
156183
Convert CLI args to python representation and set them to appropriate object attributes
157184
"""
@@ -175,6 +202,7 @@ def set_args(self, merge_policy: List[Union[List[str], str]],
175202
self.model_generator = getattr(m, cls)
176203

177204
self.model_generator_kwargs = {} if not self.strings_converters else {'post_init_converters': True}
205+
self.model_generator_kwargs['convert_unicode'] = not disable_unicode_conversion
178206
if code_generator_kwargs_raw:
179207
for item in code_generator_kwargs_raw:
180208
if item[0] == '"':
@@ -216,6 +244,11 @@ def _create_argparser(cls) -> argparse.ArgumentParser:
216244
"I.e. for file that contains dict {\"a\": {\"b\": [model_data, ...]}} you should\n"
217245
"pass 'a.b' as <JSON key>.\n\n"
218246
)
247+
parser.add_argument(
248+
"-o", "--output",
249+
metavar="FILE", default="",
250+
help="Path to output file\n\n"
251+
)
219252
parser.add_argument(
220253
"-f", "--framework",
221254
default="base",
@@ -243,22 +276,29 @@ def _create_argparser(cls) -> argparse.ArgumentParser:
243276
action="store_true",
244277
help="Enable generation of string types converters (i.e. IsoDatetimeString or BooleanString).\n\n"
245278
)
279+
parser.add_argument(
280+
"--disable-unicode-conversion", "--no-unidecode",
281+
action="store_true",
282+
help="Disabling unicode conversion in fields and class names.\n\n"
283+
)
246284

247285
default_percent = f"{ModelFieldsPercentMatch.DEFAULT * 100:.0f}"
248286
default_number = f"{ModelFieldsNumberMatch.DEFAULT:.0f}"
249287
parser.add_argument(
250288
"--merge",
251289
default=["percent", "number"],
252290
nargs="+",
253-
help=f"Merge policy settings. Default is 'percent_{default_percent} number_{default_number}' (percent of field match\n"
254-
"or number of fields match).\n"
255-
"Possible values are:\n"
256-
"'percent[_<percent>]' - two models had a certain percentage of matched field names.\n"
257-
f" Default percent is {default_percent}%%. "
258-
"Custom value could be i.e. 'percent_95'.\n"
259-
"'number[_<number>]' - two models had a certain number of matched field names.\n"
260-
f" Default number of fields is {default_number}.\n"
261-
"'exact' - two models should have exact same field names to merge.\n\n"
291+
help=(
292+
f"Merge policy settings. Default is 'percent_{default_percent} number_{default_number}' (percent of field match\n"
293+
"or number of fields match).\n"
294+
"Possible values are:\n"
295+
"'percent[_<percent>]' - two models had a certain percentage of matched field names.\n"
296+
f" Default percent is {default_percent}%%. "
297+
"Custom value could be i.e. 'percent_95'.\n"
298+
"'number[_<number>]' - two models had a certain number of matched field names.\n"
299+
f" Default number of fields is {default_number}.\n"
300+
"'exact' - two models should have exact same field names to merge.\n\n"
301+
)
262302
)
263303
parser.add_argument(
264304
"--dict-keys-regex", "--dkr",
@@ -293,8 +333,7 @@ def _create_argparser(cls) -> argparse.ArgumentParser:
293333
return parser
294334

295335

296-
def main(version_string=None):
297-
import sys
336+
def main():
298337
import os
299338

300339
if os.getenv("TRAVIS", None) or os.getenv("FORCE_COVERAGE", None):
@@ -305,14 +344,7 @@ def main(version_string=None):
305344

306345
cli = Cli()
307346
cli.parse_args()
308-
if not version_string:
309-
version_string = (
310-
'r"""\n'
311-
f'generated by json2python-models v{json_to_models.__version__} at {datetime.now().ctime()}\n'
312-
f'command: {" ".join(sys.argv)}\n'
313-
'"""\n'
314-
)
315-
print(version_string + cli.run())
347+
print(cli.run())
316348

317349

318350
def path_split(path: str) -> List[str]:
@@ -374,7 +406,7 @@ def safe_json_load(path: Path) -> Union[dict, list]:
374406
"""
375407
Open file, load json and close it.
376408
"""
377-
with path.open() as f:
409+
with path.open(encoding="utf-8") as f:
378410
return json.load(f)
379411

380412

json_to_models/generator.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
_static_types = {float, bool, int}
1010

11+
1112
class MetadataGenerator:
1213
CONVERTER_TYPE = Optional[Callable[[str], Any]]
1314

json_to_models/models/attr.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,16 @@ class AttrsModelCodeGenerator(GenericModelCodeGenerator):
1515
ATTRS = template(f"attr.s{{% if kwargs %}}({KWAGRS_TEMPLATE}){{% endif %}}")
1616
ATTRIB = template(f"attr.ib({KWAGRS_TEMPLATE})")
1717

18-
def __init__(self, model: ModelMeta, meta=False, post_init_converters=False, attrs_kwargs: dict = None):
18+
def __init__(self, model: ModelMeta, meta=False, post_init_converters=False, attrs_kwargs: dict = None,
19+
convert_unicode=True):
1920
"""
2021
:param model: ModelMeta instance
2122
:param meta: Enable generation of metadata as attrib argument
2223
:param post_init_converters: Enable generation of type converters in __post_init__ methods
2324
:param attrs_kwargs: kwargs for @attr.s() decorators
2425
:param kwargs:
2526
"""
26-
super().__init__(model, post_init_converters)
27+
super().__init__(model, post_init_converters, convert_unicode)
2728
self.no_meta = not meta
2829
self.attrs_kwargs = attrs_kwargs or {}
2930

json_to_models/models/base.py

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from .utils import indent
1313
from ..dynamic_typing import (AbsoluteModelRef, ImportPathList, MetaData,
1414
ModelMeta, compile_imports, metadata_to_typing)
15-
from ..utils import cached_classmethod
15+
from ..utils import cached_method
1616

1717
METADATA_FIELD_NAME = "J2M_ORIGINAL_FIELD"
1818
KWAGRS_TEMPLATE = "{% for key, value in kwargs.items() %}" \
@@ -71,20 +71,19 @@ class {{ name }}:
7171
% KWAGRS_TEMPLATE)
7272
FIELD: Template = template("{{name}}: {{type}}{% if body %} = {{ body }}{% endif %}")
7373

74-
def __init__(self, model: ModelMeta, post_init_converters=False):
74+
def __init__(self, model: ModelMeta, post_init_converters=False, convert_unicode=True):
7575
self.model = model
7676
self.post_init_converters = post_init_converters
77+
self.convert_unicode = convert_unicode
7778

78-
@cached_classmethod
79-
def convert_field_name(cls, name):
80-
if name in keywords_set:
81-
name += "_"
82-
name = unidecode(name)
83-
name = re.sub(r"\W", "", name)
84-
if not ('a' <= name[0].lower() <= 'z'):
85-
if '0' <= name[0] <= '9':
86-
name = ones[int(name[0])] + "_" + name[1:]
87-
return inflection.underscore(name)
79+
@cached_method
80+
def convert_class_name(self, name):
81+
# TODO: Convert names in typing links (lei_da_tu: '雷达图')
82+
return prepare_label(name, convert_unicode=self.convert_unicode)
83+
84+
@cached_method
85+
def convert_field_name(self, name):
86+
return inflection.underscore(prepare_label(name, convert_unicode=self.convert_unicode))
8887

8988
def generate(self, nested_classes: List[str] = None, extra: str = "") -> Tuple[ImportPathList, str]:
9089
"""
@@ -95,7 +94,7 @@ def generate(self, nested_classes: List[str] = None, extra: str = "") -> Tuple[I
9594
decorator_imports, decorators = self.decorators
9695
data = {
9796
"decorators": decorators,
98-
"name": self.model.name,
97+
"name": self.convert_class_name(self.model.name),
9998
"fields": fields,
10099
"extra": extra
101100
}
@@ -241,3 +240,15 @@ def sort_kwargs(kwargs: dict, ordering: Iterable[Iterable[str]]) -> dict:
241240
current[item] = value
242241
sorted_dict = {**sorted_dict_1, **kwargs, **sorted_dict_2}
243242
return sorted_dict
243+
244+
245+
def prepare_label(s: str, convert_unicode: bool) -> str:
246+
if s in keywords_set:
247+
s += "_"
248+
if convert_unicode:
249+
s = unidecode(s)
250+
s = re.sub(r"\W", "", s)
251+
if not ('a' <= s[0].lower() <= 'z'):
252+
if '0' <= s[0] <= '9':
253+
s = ones[int(s[0])] + "_" + s[1:]
254+
return s

json_to_models/models/dataclasses.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,16 @@ class DataclassModelCodeGenerator(GenericModelCodeGenerator):
1515
DC_DECORATOR = template(f"dataclass{{% if kwargs %}}({KWAGRS_TEMPLATE}){{% endif %}}")
1616
DC_FIELD = template(f"field({KWAGRS_TEMPLATE})")
1717

18-
def __init__(self, model: ModelMeta, meta=False, post_init_converters=False, dataclass_kwargs: dict = None):
18+
def __init__(self, model: ModelMeta, meta=False, post_init_converters=False, dataclass_kwargs: dict = None,
19+
convert_unicode=True):
1920
"""
2021
:param model: ModelMeta instance
2122
:param meta: Enable generation of metadata as attrib argument
2223
:param post_init_converters: Enable generation of type converters in __post_init__ methods
2324
:param dataclass_kwargs: kwargs for @dataclass() decorators
2425
:param kwargs:
2526
"""
26-
super().__init__(model, post_init_converters)
27+
super().__init__(model, post_init_converters, convert_unicode)
2728
self.no_meta = not meta
2829
self.dataclass_kwargs = dataclass_kwargs or {}
2930

json_to_models/utils.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,13 @@ def cached_method(func: Callable):
101101
"""
102102
Decorator to cache method return values
103103
"""
104-
null = object()
105104

106105
@wraps(func)
107106
def cached_fn(self, *args):
108107
if getattr(self, '__cache__', None) is None:
109108
setattr(self, '__cache__', {})
110-
value = self.__cache__.get(args, null)
111-
if value is null:
109+
value = self.__cache__.get(args, ...)
110+
if value is Ellipsis:
112111
value = func(self, *args)
113112
self.__cache__[args] = value
114113
return value
@@ -121,12 +120,11 @@ def cached_classmethod(func: Callable):
121120
Decorator to cache classmethod return values
122121
"""
123122
cache = {}
124-
null = object()
125123

126124
@wraps(func)
127125
def cached_fn(cls, *args):
128-
value = cache.get(args, null)
129-
if value is null:
126+
value = cache.get(args, ...)
127+
if value is Ellipsis:
130128
value = func(cls, *args)
131129
cache[args] = value
132130
return value

0 commit comments

Comments
 (0)