Skip to content

Commit 2536cfd

Browse files
committed
Handle list data with regular --model argument;
Deprecate --list argument
1 parent 74750f1 commit 2536cfd

File tree

2 files changed

+68
-72
lines changed

2 files changed

+68
-72
lines changed

json_to_models/cli.py

Lines changed: 66 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,6 @@ def parse_args(self, args: List[str] = None):
8181
namespace = parser.parse_args(args)
8282

8383
# Extract args
84-
models: List[Tuple[str, Iterable[Path]]] = [
85-
(model_name, itertools.chain(*map(_process_path, paths)))
86-
for model_name, *paths in namespace.model or ()
87-
]
88-
models_lists: List[Tuple[str, Tuple[str, Path]]] = [
89-
(model_name, (lookup, Path(path)))
90-
for model_name, lookup, path in namespace.list or ()
91-
]
9284
parser = getattr(FileLoaders, namespace.input_format)
9385
self.output_file = namespace.output
9486
self.enable_datetime = namespace.datetime
@@ -104,8 +96,8 @@ def parse_args(self, args: List[str] = None):
10496
dict_keys_fields: List[str] = namespace.dict_keys_fields
10597
preamble: str = namespace.preamble
10698

107-
self.validate(models_lists, merge_policy, framework, code_generator)
108-
self.setup_models_data(models, models_lists, parser)
99+
self.setup_models_data(namespace.model or (), namespace.list or (), parser)
100+
self.validate(merge_policy, framework, code_generator)
109101
self.set_args(merge_policy, structure, framework, code_generator, code_generator_kwargs_raw,
110102
dict_keys_regex, dict_keys_fields, disable_unicode_conversion, preamble)
111103

@@ -144,20 +136,15 @@ def version_string(self):
144136
'"""\n'
145137
)
146138

147-
def validate(self, models_list, merge_policy, framework, code_generator):
139+
def validate(self, merge_policy, framework, code_generator):
148140
"""
149141
Validate parsed args
150142
151-
:param models_list: List of pairs (model name, list of lookup expr and filesystem path)
152143
:param merge_policy: List of merge policies. Each merge policy is either string or string and policy arguments
153144
:param framework: Framework name (predefined code generator)
154145
:param code_generator: Code generator import string
155146
:return:
156147
"""
157-
names = {name for name, _ in models_list}
158-
if len(names) != len(models_list):
159-
raise ValueError("Model names under -l flag should be unique")
160-
161148
for m in merge_policy:
162149
if isinstance(m, list):
163150
if m[0] not in self.MODEL_CMP_MAPPING:
@@ -172,23 +159,33 @@ def validate(self, models_list, merge_policy, framework, code_generator):
172159

173160
def setup_models_data(
174161
self,
175-
models: Iterable[Tuple[str, Iterable[Path]]],
176-
models_lists: Iterable[Tuple[str, Tuple[str, Path]]],
162+
models: Iterable[Union[
163+
Tuple[str, str],
164+
Tuple[str, str, str],
165+
]],
166+
models_lists: Iterable[Tuple[str, str, str]],
177167
parser: 'FileLoaders.T'
178168
):
179169
"""
180170
Initialize lazy loaders for models data
181171
"""
182-
models_dict: Dict[str, List[Iterable[dict]]] = defaultdict(list)
183-
for model_name, paths in models:
184-
models_dict[model_name].append(parser(path) for path in paths)
185-
for model_name, (lookup, path) in models_lists:
186-
models_dict[model_name].append(iter_json_file(parser(path), lookup))
172+
models_dict: Dict[str, List[dict]] = defaultdict(list)
173+
174+
models = list(models) + list(models_lists)
175+
for model_tuple in models:
176+
if len(model_tuple) == 2:
177+
model_name, path_raw = model_tuple
178+
lookup = '-'
179+
elif len(model_tuple) == 3:
180+
model_name, lookup, path_raw = model_tuple
181+
else:
182+
raise RuntimeError('`--model` argument should contain exactly 2 or 3 strings')
187183

188-
self.models_data = {
189-
model_name: itertools.chain(*list_of_gen)
190-
for model_name, list_of_gen in models_dict.items()
191-
}
184+
for real_path in process_path(path_raw):
185+
iterator = iter_json_file(parser(real_path), lookup)
186+
models_dict[model_name].extend(iterator)
187+
188+
self.models_data = models_dict
192189

193190
def set_args(
194191
self,
@@ -257,20 +254,13 @@ def _create_argparser(cls) -> argparse.ArgumentParser:
257254

258255
parser.add_argument(
259256
"-m", "--model",
260-
nargs="+", action="append", metavar=("<Model name>", "<JSON files>"),
257+
nargs="+", action="append", metavar=("<Model name> [<JSON lookup>] <File path or pattern>", ""),
261258
help="Model name and its JSON data as path or unix-like path pattern.\n"
262259
"'*', '**' or '?' patterns symbols are supported.\n\n"
263-
)
264-
parser.add_argument(
265-
"-l", "--list",
266-
nargs=3, action="append", metavar=("<Model name>", "<JSON key>", "<JSON file>"),
267-
help="Like -m but given json file should contain list of model data.\n"
260+
"JSON data could be array of models or single model\n\n"
268261
"If this file contains dict with nested list than you can pass\n"
269-
"<JSON key> to lookup. Deep lookups are supported by dot-separated path.\n"
270-
"If no lookup needed pass '-' as <JSON key>\n\n"
271-
272-
"I.e. for file that contains dict {\"a\": {\"b\": [model_data, ...]}} you should\n"
273-
"pass 'a.b' as <JSON key>.\n\n"
262+
"<JSON lookup>. Deep lookups are supported by dot-separated path.\n"
263+
"If no lookup needed pass '-' as <JSON lookup> (default)\n\n"
274264
)
275265
parser.add_argument(
276266
"-i", "--input-format",
@@ -377,6 +367,11 @@ def _create_argparser(cls) -> argparse.ArgumentParser:
377367
type=str,
378368
help="Code to insert into the generated file after the imports and before the list of classes\n\n"
379369
)
370+
parser.add_argument(
371+
"-l", "--list",
372+
nargs=3, action="append", metavar=("<Model name>", "<JSON lookup>", "<JSON file>"),
373+
help="DEPRECATED, use --model argument instead"
374+
)
380375

381376
return parser
382377

@@ -395,27 +390,6 @@ def main():
395390
print(cli.run())
396391

397392

398-
def path_split(path: str) -> List[str]:
399-
"""
400-
Split path into list of components
401-
402-
:param path: string path
403-
:return: List of files/patterns
404-
"""
405-
folders = []
406-
while True:
407-
path, folder = os.path.split(path)
408-
409-
if folder:
410-
folders.append(folder)
411-
else:
412-
if path:
413-
folders.append(path)
414-
break
415-
folders.reverse()
416-
return folders
417-
418-
419393
class FileLoaders:
420394
T = Callable[[Path], Union[dict, list]]
421395

@@ -442,7 +416,7 @@ def ini(path: Path) -> dict:
442416

443417
def dict_lookup(d: Union[dict, list], lookup: str) -> Union[dict, list]:
444418
"""
445-
Extract nested dictionary value from key path.
419+
Extract nested value from key path.
446420
If lookup is "-" returns dict as is.
447421
448422
:param d: Nested dict
@@ -460,25 +434,26 @@ def dict_lookup(d: Union[dict, list], lookup: str) -> Union[dict, list]:
460434

461435
def iter_json_file(data: Union[dict, list], lookup: str) -> Generator[Union[dict, list], Any, None]:
462436
"""
463-
Loads given 'path' file, perform lookup and return generator over json list.
437+
Perform lookup and return generator over json list.
464438
Does not open file until iteration is started.
465439
466-
:param path: File Path instance
440+
:param data: JSON data
467441
:param lookup: Dot separated lookup path
468-
:return:
442+
:return: Generator of the model data
469443
"""
470-
l = dict_lookup(data, lookup)
471-
assert isinstance(l, list), f"Dict lookup return {type(l)} but list is expected, check your lookup path"
472-
yield from l
444+
item = dict_lookup(data, lookup)
445+
if isinstance(item, list):
446+
yield from item
447+
elif isinstance(item, dict):
448+
yield item
449+
else:
450+
raise TypeError(f'dict or list is expected at {lookup if lookup != "-" else "JSON root"}, not {type(item)}')
473451

474452

475-
def _process_path(path: str) -> Iterable[Path]:
453+
def process_path(path: str) -> Iterable[Path]:
476454
"""
477455
Convert path pattern into path iterable.
478456
If non-pattern path is given return tuple of one element: (path,)
479-
480-
:param path:
481-
:return:
482457
"""
483458
split_path = path_split(path)
484459
clean_path = list(itertools.takewhile(
@@ -502,3 +477,24 @@ def _process_path(path: str) -> Iterable[Path]:
502477
return path.glob(pattern_path)
503478
else:
504479
return path,
480+
481+
482+
def path_split(path: str) -> List[str]:
483+
"""
484+
Split path into list of components
485+
486+
:param path: string path
487+
:return: List of files/patterns
488+
"""
489+
folders = []
490+
while True:
491+
path, folder = os.path.split(path)
492+
493+
if folder:
494+
folders.append(folder)
495+
else:
496+
if path:
497+
folders.append(path)
498+
break
499+
folders.reverse()
500+
return folders

test/test_cli/test_utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import pytest
66

7-
from json_to_models.cli import _process_path, dict_lookup, iter_json_file, path_split
7+
from json_to_models.cli import dict_lookup, iter_json_file, path_split, process_path
88
from json_to_models.utils import convert_args
99

1010
echo = lambda *args, **kwargs: (args, kwargs)
@@ -158,5 +158,5 @@ def test_iter_json_file(value, expected):
158158

159159
@pytest.mark.parametrize("value,expected", test_process_path_data)
160160
def test_process_path(value, expected):
161-
result = set(str(p).replace("\\", "/") for p in _process_path(value))
161+
result = set(str(p).replace("\\", "/") for p in process_path(value))
162162
assert result == expected, f"(in value: {value})"

0 commit comments

Comments
 (0)