Skip to content

Commit 5683553

Browse files
authored
Merge pull request #24 from bogdandm/disable-unicode-conversion-flag
Disable unicode conversion flag
2 parents 74c1441 + 3ecfe95 commit 5683553

File tree

14 files changed

+264
-71
lines changed

14 files changed

+264
-71
lines changed

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,13 +162,17 @@ Arguments:
162162
* **Example**: `-l Car - cars.json -l Person fetch_results.items.persons result.json`
163163
* **Note**: Models names under this arguments should be unique.
164164

165+
* `-o`, `--output` - Output file
166+
* **Format**: `-o <FILE>`
167+
* **Example**: `-o car_model.py`
168+
165169
* `-f`, `--framework` - Model framework for which python code is generated.
166170
`base` (default) mean no framework so code will be generated without any decorators and additional meta-data.
167171
* **Format**: `-f {base,attrs,dataclasses,custom}`
168172
* **Example**: `-f attrs`
169173
* **Default**: `-f base`
170174

171-
* `-s , --structure` - Models composition style.
175+
* `-s`, `--structure` - Models composition style.
172176
* **Format**: `-s {nested, flat}`
173177
* **Example**: `-s flat`
174178
* **Default**: `-s nested`
@@ -177,6 +181,9 @@ Arguments:
177181
* **Default**: disabled
178182
* **Warning**: This can lead to 6-7 times slowdown on large datasets. Be sure that you really need this option.
179183

184+
* `--disable-unicode-conversion`, `--no-unidecode` - Disable unicode conversion in field labels and class names
185+
* **Default**: enabled
186+
180187
* `--strings-converters` - Enable generation of string types converters (i.e. `IsoDatetimeString` or `BooleanString`).
181188
* **Default**: disabled
182189

@@ -196,7 +203,7 @@ Arguments:
196203
* **Format**: `--dkr RegEx [RegEx ...]`
197204
* **Example**: `--dkr node_\d+ \d+_\d+_\d+`
198205
* **Note**: `^` and `$` (string borders) tokens will be added automatically but you
199-
have escape to other special characters manually.
206+
have to escape other special characters manually.
200207
* **Optional**
201208

202209
* `--dict-keys-fields`, `--dkf` - List of model fields names that will be marked as dict fields

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/dynamic_typing/base.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from inspect import isclass
2-
from typing import Iterable, List, Tuple, Union
2+
from typing import Any, Generator, Iterable, List, Tuple, Union
33

44
ImportPathList = List[Tuple[str, Union[Iterable[str], str, None]]]
55

@@ -50,6 +50,14 @@ def _to_hash_string(self) -> str:
5050
"""
5151
raise NotImplementedError()
5252

53+
def iter_child(self) -> Generator['MetaData', Any, None]:
54+
yield self
55+
for child in self:
56+
if isinstance(child, BaseType):
57+
yield from child.iter_child()
58+
else:
59+
yield child
60+
5361

5462
class UnknownType(BaseType):
5563
__slots__ = []

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
78+
self.model.name = self.convert_class_name(self.model.name)
7779

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)
80+
@cached_method
81+
def convert_class_name(self, name):
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
"""
@@ -144,7 +143,7 @@ def fields(self) -> Tuple[ImportPathList, List[str]]:
144143
145144
:return: imports, list of fields as string
146145
"""
147-
required, optional = sort_fields(self.model)
146+
required, optional = sort_fields(self.model, unicode_fix=not self.convert_unicode)
148147
imports: ImportPathList = []
149148
strings: List[str] = []
150149
for is_optional, fields in enumerate((required, optional)):
@@ -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

0 commit comments

Comments
 (0)