Skip to content

Commit 74f81c1

Browse files
authored
Merge pull request #11 from bogdandm/features-27.11.18
Add --dict-keys-regex and --dict-keys-fields arguments;
2 parents bbd7e79 + f72a81e commit 74f81c1

File tree

7 files changed

+125
-46
lines changed

7 files changed

+125
-46
lines changed

.travis.yml

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ language: python
44
cache: pip
55

66
install:
7+
- python setup.py sdist
78
- python setup.py install
89
- pip install pytest pytest-cov requests coveralls codacy-coverage
910

@@ -22,11 +23,23 @@ matrix:
2223
- python: 3.8-dev
2324

2425
deploy:
25-
provider: pypi
26-
user: "bogdandm"
27-
password:
28-
secure: "bfIiVsBVMRAjsZYT/UtvNFiY5RU9+2b8QyveKXB3JhZCC/+S8nU/ZJkM9/3Q4V9sUVTVd+NR4Iq1OWkgFwYhJXK+eYlqWJDwfzcxTY5sXAcIxtmXGpNKJaqOF8hyCRdeusGFXR9O1RqzZVQ1ioLCj43eOkjvgFbOw2pIRtmG8Sip6oSJUmY9MSYHFzz4EoY6s+Jv9g30P7bSGY9bekOySFi7LmnX2IM+2T7AMuRvRyd4gHlLVy7pxZxj3S/jz9IiwkVq55HUl5ITWCZVrk6RnYiQhWn5D1AllrOilJGCNQ6HmKQWhDu/fNXEg24BFUTs87OymPKAFNRFniaH9PGu5v2yoN0BWN6+23c2Zc/G9QpQzqm+eVIpW5pkp25EiQw8Cz1pjDmLAVzSZuPXxJnpmkihLUAmSoktI4Zo6+QeLaBoqxds//aLqupMPhO3kzUZe1O5CrogPXTCwpHUGKzMxtjaaXdLo3z7EVT8kRDRtggpdE9KD1shDRUMBrakmuOFA+Tms+iKZSBrW5xhC2g/lFnZluZidj2ir8hyJ9lPMUmGxn/OkIQIBcMkEKCsDFC3wPD39MY/eDbkBvmK++Uhah9T0ljLOR+j1n2ZoJ+N3zD/UpucW5681dGA/sDpgjyDc9ESj14IHjTPLI7sw3IXaial4X2h0ZdfXf6NesLDraI="
29-
on:
30-
tags: true
31-
all_branches: false
32-
skip_existing: true
26+
- provider: pypi
27+
user: bogdandm
28+
password:
29+
secure: bfIiVsBVMRAjsZYT/UtvNFiY5RU9+2b8QyveKXB3JhZCC/+S8nU/ZJkM9/3Q4V9sUVTVd+NR4Iq1OWkgFwYhJXK+eYlqWJDwfzcxTY5sXAcIxtmXGpNKJaqOF8hyCRdeusGFXR9O1RqzZVQ1ioLCj43eOkjvgFbOw2pIRtmG8Sip6oSJUmY9MSYHFzz4EoY6s+Jv9g30P7bSGY9bekOySFi7LmnX2IM+2T7AMuRvRyd4gHlLVy7pxZxj3S/jz9IiwkVq55HUl5ITWCZVrk6RnYiQhWn5D1AllrOilJGCNQ6HmKQWhDu/fNXEg24BFUTs87OymPKAFNRFniaH9PGu5v2yoN0BWN6+23c2Zc/G9QpQzqm+eVIpW5pkp25EiQw8Cz1pjDmLAVzSZuPXxJnpmkihLUAmSoktI4Zo6+QeLaBoqxds//aLqupMPhO3kzUZe1O5CrogPXTCwpHUGKzMxtjaaXdLo3z7EVT8kRDRtggpdE9KD1shDRUMBrakmuOFA+Tms+iKZSBrW5xhC2g/lFnZluZidj2ir8hyJ9lPMUmGxn/OkIQIBcMkEKCsDFC3wPD39MY/eDbkBvmK++Uhah9T0ljLOR+j1n2ZoJ+N3zD/UpucW5681dGA/sDpgjyDc9ESj14IHjTPLI7sw3IXaial4X2h0ZdfXf6NesLDraI=
30+
on:
31+
tags: true
32+
all_branches: false
33+
34+
- provider: releases
35+
api_key:
36+
secure: AalzD76UPcpYdW0EqIbIi0prnxj1rsyRI6S4EBx91XGFY+BRJV+wJ99MiCE36A4q+cerDCxLmosiAQdzYkJ6/1ageezytJYD0FiC8GxvXtWoutySi0Ka1UTxvIQY22680lh5Xj4vki8tmQAMiLc9gTtwkk+e1J6bPgbQVNQtd5HD3697fdbeBrK70kzHUt93nW1x3N6Ers+WuLtZzV8ft1QzKGQYqgEGigV3rfCM6BThhOXq8B5DnzwwRHg/4TbHvtjyqKchPSzj9+zxGrInJ4dMAut8sGbvFRypDS8Y/I0ODveEBbbmi1SQkoRTH7OuP76T1qAPurmYR2nhq1owxQ4OLjDLgtSiFyzLTEXTjFAOENa8vlZcwC75iCyun/jj2lBVFvW/1rAIiogHxwLsKELbRZQzfY8pTJK5D5KPLNMtNDYecrxI0Bg0z7RPRjk5cpT3gAIyRHuVqCeXZUh+bP87Ev632jtK0thilF67vJNfSamHxLJZH6hEQNKOMjztcOdRDlY+vNbIVwl/RScrhSi+dQCH6FAINXNL9a6TENkwxYjgHYhB9xxNQXq6p+miSID7V9ptBTDVsNIGcv8xLzaChVWExzkFR/BflObjccKmiN9hTlzHSEUaRBpw3EX/L9YNKtPHvPSo/1KxZPt+AB/z0OgvWqSdNaVVtopooQg=
37+
file_glob: true
38+
file: dist/*
39+
skip_cleanup: true
40+
overwrite: true
41+
draft: true
42+
on:
43+
repo: bogdandm/json2python-models
44+
tags: true
45+
all_branches: false

json_to_models/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
from pkg_resources import parse_version
22

3-
__version__ = "0.1a1"
3+
__version__ = "0.1b1"
44
VERSION = parse_version(__version__)

json_to_models/cli.py

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import itertools
44
import json
55
import os.path
6+
import re
67
from collections import defaultdict
78
from datetime import datetime
89
from pathlib import Path
@@ -79,26 +80,21 @@ def parse_args(self, args: List[str] = None):
7980
framework = namespace.framework
8081
code_generator = namespace.code_generator
8182
code_generator_kwargs_raw: List[str] = namespace.code_generator_kwargs
82-
code_generator_kwargs = {}
83-
if code_generator_kwargs_raw:
84-
for item in code_generator_kwargs_raw:
85-
if item[0] == '"':
86-
item = item[1:]
87-
if item[-1] == '"':
88-
item = item[:-1]
89-
name, value = item.split("=", 1)
90-
code_generator_kwargs[name] = value
83+
dict_keys_regex: List[str] = namespace.dict_keys_regex
84+
dict_keys_fields: List[str] = namespace.dict_keys_fields
9185

9286
self.validate(models, models_lists, merge_policy, framework, code_generator)
9387
self.setup_models_data(models, models_lists)
94-
self.set_args(merge_policy, structure, framework, code_generator, code_generator_kwargs)
88+
self.set_args(merge_policy, structure, framework, code_generator, code_generator_kwargs_raw,
89+
dict_keys_regex, dict_keys_fields)
9590

9691
def run(self):
9792
if self.enable_datetime:
9893
register_datetime_classes()
99-
100-
# TODO: Inject dict_keys_regex and dict_keys_fields
101-
generator = MetadataGenerator()
94+
generator = MetadataGenerator(
95+
dict_keys_regex=self.dict_keys_regex,
96+
dict_keys_fields=self.dict_keys_fields
97+
)
10298
registry = ModelRegistry(*self.merge_policy)
10399
for name, data in self.models_data.items():
104100
meta = generator.generate(*data)
@@ -152,7 +148,8 @@ def setup_models_data(self, models: Iterable[Tuple[str, Iterable[Path]]],
152148
}
153149

154150
def set_args(self, merge_policy: List[Union[List[str], str]],
155-
structure: str, framework: str, code_generator: str, code_generator_kwargs: dict):
151+
structure: str, framework: str, code_generator: str, code_generator_kwargs_raw: List[str],
152+
dict_keys_regex: List[str], dict_keys_fields: List[str]):
156153
"""
157154
Convert CLI args to python representation and set them to appropriate object attributes
158155
"""
@@ -175,8 +172,18 @@ def set_args(self, merge_policy: List[Union[List[str], str]],
175172
m = importlib.import_module(module)
176173
self.model_generator = getattr(m, cls)
177174

178-
if code_generator_kwargs:
179-
self.model_generator_kwargs = code_generator_kwargs
175+
self.model_generator_kwargs = {}
176+
if code_generator_kwargs_raw:
177+
for item in code_generator_kwargs_raw:
178+
if item[0] == '"':
179+
item = item[1:]
180+
if item[-1] == '"':
181+
item = item[:-1]
182+
name, value = item.split("=", 1)
183+
self.model_generator_kwargs[name] = value
184+
185+
self.dict_keys_regex = [re.compile(rf"^{r}$") for r in dict_keys_regex] if dict_keys_regex else ()
186+
self.dict_keys_fields = dict_keys_fields or ()
180187

181188
self.initialize = True
182189

@@ -185,7 +192,6 @@ def _create_argparser() -> argparse.ArgumentParser:
185192
"""
186193
ArgParser factory
187194
"""
188-
# TODO: dict_keys_regex and dict_keys_fields arguments
189195
parser = argparse.ArgumentParser(
190196
formatter_class=argparse.RawTextHelpFormatter,
191197
description="Convert given json files into Python models."
@@ -215,6 +221,21 @@ def _create_argparser() -> argparse.ArgumentParser:
215221
"Warn.: This can lead to 6-7 times slowdown on large datasets.\n"
216222
" Be sure that you really need this option.\n\n"
217223
)
224+
parser.add_argument(
225+
"--dict-keys-regex", "--dkr",
226+
nargs="+", metavar="RegEx",
227+
help="List of regular expressions (Python syntax).\n"
228+
"If all keys of some dict are match one of them\n"
229+
"then this dict will be marked as dict field but not nested model.\n"
230+
"Note: ^ and $ tokens will be added automatically but you have to\n"
231+
"escape other special characters manually.\n"
232+
)
233+
parser.add_argument(
234+
"--dict-keys-fields", "--dkf",
235+
nargs="+", metavar="FIELD NAME",
236+
help="List of model fields names that will be marked as dict fields\n\n"
237+
)
238+
218239
default_percent = f"{ModelFieldsPercentMatch.DEFAULT * 100:.0f}"
219240
default_number = f"{ModelFieldsNumberMatch.DEFAULT:.0f}"
220241
parser.add_argument(

json_to_models/generator.py

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,12 @@ def optimize_type(self, meta: MetaData, process_model_ptr=False) -> MetaData:
182182
elif isinstance(meta, DUnion):
183183
return self._optimize_union(meta)
184184

185+
elif isinstance(meta, DOptional):
186+
t = self.optimize_type(meta.type)
187+
if isinstance(t, DOptional):
188+
t = t.type
189+
return meta.replace(t)
190+
185191
elif isinstance(meta, SingleType) and (process_model_ptr or not isinstance(meta, ModelPtr)):
186192
# Optimize nested type
187193
return meta.replace(self.optimize_type(meta.type))
@@ -193,21 +199,27 @@ def optimize_type(self, meta: MetaData, process_model_ptr=False) -> MetaData:
193199

194200
def _optimize_union(self, t: DUnion):
195201
# Replace DUnion of 1 element with this element
196-
if len(t) == 1:
197-
return t.types[0]
202+
# if len(t) == 1:
203+
# return t.types[0]
198204

199205
# Split nested types into categories
200206
str_types: List[Union[type, StringSerializable]] = []
201207
types_to_merge: List[dict] = []
202208
list_types: List[DList] = []
209+
dict_types: List[DList] = []
203210
other_types: List[MetaData] = []
204211
for item in t.types:
212+
if isinstance(item, DOptional):
213+
item = item.type
214+
other_types.append(NoneType)
205215
if isinstance(item, dict):
206216
types_to_merge.append(item)
207217
elif item in self.str_types_registry or item is str:
208218
str_types.append(item)
209219
elif isinstance(item, DList):
210220
list_types.append(item)
221+
elif isinstance(item, DDict):
222+
dict_types.append(item)
211223
else:
212224
other_types.append(item)
213225

@@ -217,10 +229,11 @@ def _optimize_union(self, t: DUnion):
217229
if types_to_merge:
218230
other_types.append(self.merge_field_sets(types_to_merge))
219231

220-
if list_types:
221-
other_types.append(DList(DUnion(*(
222-
t.type for t in list_types
223-
))))
232+
for cls, iterable_types in ((DList, list_types), (DDict, dict_types)):
233+
if iterable_types:
234+
other_types.append(cls(DUnion(*(
235+
t.type for t in iterable_types
236+
))))
224237

225238
if str in str_types:
226239
other_types.append(str)
@@ -234,14 +247,20 @@ def _optimize_union(self, t: DUnion):
234247
if Unknown in types:
235248
types.remove(Unknown)
236249

237-
if len(types) > 1:
238-
if NoneType in types:
250+
optional = False
251+
if NoneType in types:
252+
optional = True
253+
while NoneType in types:
239254
types.remove(NoneType)
240-
if len(types) > 1:
241-
return DOptional(DUnion(*types))
242-
else:
243-
return DOptional(types[0])
244-
return DUnion(*types)
245255

256+
if len(types) > 1:
257+
meta_type = DUnion(*types)
258+
if len(meta_type.types) == 1:
259+
meta_type = meta_type.types[0]
260+
else:
261+
meta_type = types[0]
262+
263+
if optional:
264+
return DOptional(meta_type)
246265
else:
247-
return types[0]
266+
return meta_type

json_to_models/registry.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,10 +162,16 @@ def merge_models(self, generator, strict=False) -> List[Tuple[ModelMeta, Set[Mod
162162
groups = new_groups
163163

164164
replaces = []
165+
replaces_ids = set()
165166
for group in groups:
166167
model_meta = self._merge(generator, *group)
167168
generator.optimize_type(model_meta)
169+
replaces_ids.add(model_meta.index)
168170
replaces.append((model_meta, group))
171+
172+
for model_meta in self.models:
173+
if model_meta.index not in replaces_ids:
174+
generator.optimize_type(model_meta)
169175
return replaces
170176

171177
def _merge(self, generator, *models: ModelMeta):

test/test_cli/test_script.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,11 @@ def test_help():
6262
-l User - "{test_data_path / 'users.json'}" """,
6363
id="list1_list2"),
6464

65-
pytest.param(f"""{executable} -m Gist "{tmp_path / '*.gist'}" """, id="gists"),
66-
pytest.param(f"""{executable} -m Gist "{tmp_path / '*.gist'}" --datetime""", id="gists_datetime"),
67-
pytest.param(f"""{executable} -m Gist "{tmp_path / '*.gist'}" --merge percent number_10""",
65+
pytest.param(f"""{executable} -m Gist "{tmp_path / '*.gist'}" --dkf files""", id="gists"),
66+
pytest.param(f"""{executable} -m Gist "{tmp_path / '*.gist'}" --dkf files --datetime""", id="gists_datetime"),
67+
pytest.param(f"""{executable} -m Gist "{tmp_path / '*.gist'}" --dkf files --merge percent number_10""",
6868
id="gists_merge_policy"),
69-
pytest.param(f"""{executable} -m Gist "{tmp_path / '*.gist'}" --merge exact""",
69+
pytest.param(f"""{executable} -m Gist "{tmp_path / '*.gist'}" --dkf files --merge exact""",
7070
id="gists_no_merge"),
7171
]
7272

test/test_generator/test_optimize_type.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pytest
22

3-
from json_to_models.dynamic_typing import (BooleanString, DList, DOptional, DTuple, DUnion, FloatString, IntString,
4-
NoneType, Unknown)
3+
from json_to_models.dynamic_typing import (BooleanString, DDict, DList, DOptional, DTuple, DUnion, FloatString,
4+
IntString, NoneType, Unknown)
55
from json_to_models.generator import MetadataGenerator
66

77
# MetaData | Optimized MetaData
@@ -106,6 +106,26 @@
106106
DList(DUnion(str, int)),
107107
id="union_of_str_int_FloatString"
108108
),
109+
pytest.param(
110+
DOptional(DUnion(DOptional(str), str)),
111+
DOptional(str),
112+
id="optional_union_nested"
113+
),
114+
pytest.param(
115+
DUnion(NoneType, str, NoneType),
116+
DOptional(str),
117+
id="optional_str"
118+
),
119+
pytest.param(
120+
DUnion(DDict(str), DDict(str), DDict(str)),
121+
DDict(str),
122+
id="dict_union"
123+
),
124+
pytest.param(
125+
DUnion(DDict(str), DDict(int), DDict(str)),
126+
DDict(DUnion(str, int)),
127+
id="dict_union_2"
128+
),
109129
]
110130

111131

0 commit comments

Comments
 (0)