Skip to content

Commit 72b0c20

Browse files
committed
Add yaml and ini parsers
1 parent d00e80d commit 72b0c20

File tree

7 files changed

+2091
-29
lines changed

7 files changed

+2091
-29
lines changed

json_to_models/cli.py

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import argparse
2+
import configparser
23
import importlib
34
import itertools
45
import json
@@ -10,6 +11,14 @@
1011
from pathlib import Path
1112
from typing import Any, Callable, Dict, Generator, Iterable, List, Tuple, Type, Union
1213

14+
try:
15+
import yaml
16+
except ImportError:
17+
try:
18+
import ruamel.yaml as yaml
19+
except ImportError:
20+
yaml = None
21+
1322
from . import __version__ as VERSION
1423
from .dynamic_typing import ModelMeta, register_datetime_classes
1524
from .generator import MetadataGenerator
@@ -80,6 +89,7 @@ def parse_args(self, args: List[str] = None):
8089
(model_name, (lookup, Path(path)))
8190
for model_name, lookup, path in namespace.list or ()
8291
]
92+
parser = getattr(FileLoaders, namespace.input_format)
8393
self.output_file = namespace.output
8494
self.enable_datetime = namespace.datetime
8595
disable_unicode_conversion = namespace.disable_unicode_conversion
@@ -94,7 +104,7 @@ def parse_args(self, args: List[str] = None):
94104
dict_keys_fields: List[str] = namespace.dict_keys_fields
95105

96106
self.validate(models, models_lists, merge_policy, framework, code_generator)
97-
self.setup_models_data(models, models_lists)
107+
self.setup_models_data(models, models_lists, parser)
98108
self.set_args(merge_policy, structure, framework, code_generator, code_generator_kwargs_raw,
99109
dict_keys_regex, dict_keys_fields, disable_unicode_conversion)
100110

@@ -157,16 +167,20 @@ def validate(self, models, models_list, merge_policy, framework, code_generator)
157167
elif framework != 'custom' and code_generator is not None:
158168
raise ValueError("--code-generator argument has no effect without '--framework custom' argument")
159169

160-
def setup_models_data(self, models: Iterable[Tuple[str, Iterable[Path]]],
161-
models_lists: Iterable[Tuple[str, Tuple[str, Path]]]):
170+
def setup_models_data(
171+
self,
172+
models: Iterable[Tuple[str, Iterable[Path]]],
173+
models_lists: Iterable[Tuple[str, Tuple[str, Path]]],
174+
parser: 'FileLoaders.T'
175+
):
162176
"""
163177
Initialize lazy loaders for models data
164178
"""
165179
models_dict: Dict[str, List[Iterable[dict]]] = defaultdict(list)
166180
for model_name, paths in models:
167-
models_dict[model_name].append(map(safe_json_load, paths))
181+
models_dict[model_name].append(parser(path) for path in paths)
168182
for model_name, (lookup, path) in models_lists:
169-
models_dict[model_name].append(iter_json_file(path, lookup))
183+
models_dict[model_name].append(iter_json_file(parser(path), lookup))
170184

171185
self.models_data = {
172186
model_name: itertools.chain(*list_of_gen)
@@ -252,6 +266,12 @@ def _create_argparser(cls) -> argparse.ArgumentParser:
252266
"I.e. for file that contains dict {\"a\": {\"b\": [model_data, ...]}} you should\n"
253267
"pass 'a.b' as <JSON key>.\n\n"
254268
)
269+
parser.add_argument(
270+
"-i", "--input-format",
271+
metavar="FORMAT", default="json",
272+
choices=['json', 'yaml', 'ini'],
273+
help="Input files parser ('PyYaml' is required to parse yaml files)\n\n"
274+
)
255275
parser.add_argument(
256276
"-o", "--output",
257277
metavar="FILE", default="",
@@ -385,7 +405,31 @@ def path_split(path: str) -> List[str]:
385405
return folders
386406

387407

388-
def dict_lookup(d: dict, lookup: str) -> Union[dict, list]:
408+
class FileLoaders:
409+
T = Callable[[Path], Union[dict, list]]
410+
411+
@staticmethod
412+
def json(path: Path) -> Union[dict, list]:
413+
with path.open() as fp:
414+
return json.load(fp)
415+
416+
@staticmethod
417+
def yaml(path: Path) -> Union[dict, list]:
418+
if yaml is None:
419+
print('Yaml parser is not installed. To parse yaml files PyYaml (or ruamel.yaml) is required.')
420+
raise ImportError('yaml')
421+
with path.open() as fp:
422+
return yaml.safe_load(fp)
423+
424+
@staticmethod
425+
def ini(path: Path) -> dict:
426+
config = configparser.ConfigParser()
427+
with path.open() as fp:
428+
config.read_file(fp)
429+
return {s: dict(config.items(s)) for s in config.sections()}
430+
431+
432+
def dict_lookup(d: Union[dict, list], lookup: str) -> Union[dict, list]:
389433
"""
390434
Extract nested dictionary value from key path.
391435
If lookup is "-" returns dict as is.
@@ -403,7 +447,7 @@ def dict_lookup(d: dict, lookup: str) -> Union[dict, list]:
403447
return d
404448

405449

406-
def iter_json_file(path: Path, lookup: str) -> Generator[Union[dict, list], Any, None]:
450+
def iter_json_file(data: Union[dict, list], lookup: str) -> Generator[Union[dict, list], Any, None]:
407451
"""
408452
Loads given 'path' file, perform lookup and return generator over json list.
409453
Does not open file until iteration is started.
@@ -412,21 +456,11 @@ def iter_json_file(path: Path, lookup: str) -> Generator[Union[dict, list], Any,
412456
:param lookup: Dot separated lookup path
413457
:return:
414458
"""
415-
with path.open() as f:
416-
l = json.load(f)
417-
l = dict_lookup(l, lookup)
459+
l = dict_lookup(data, lookup)
418460
assert isinstance(l, list), f"Dict lookup return {type(l)} but list is expected, check your lookup path"
419461
yield from l
420462

421463

422-
def safe_json_load(path: Path) -> Union[dict, list]:
423-
"""
424-
Open file, load json and close it.
425-
"""
426-
with path.open(encoding="utf-8") as f:
427-
return json.load(f)
428-
429-
430464
def _process_path(path: str) -> Iterable[Path]:
431465
"""
432466
Convert path pattern into path iterable.

json_to_models/models/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
keywords_set = set(keyword.kwlist)
2525
builtins_set = set(__builtins__.keys())
26-
other_common_names_set = {'datetime', 'time', 'date', 'defaultdict'}
26+
other_common_names_set = {'datetime', 'time', 'date', 'defaultdict', 'schema'}
2727
blacklist_words = frozenset(keywords_set | builtins_set | other_common_names_set)
2828
ones = ['', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine']
2929

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,6 @@ def run_tests(self):
5050
},
5151
install_requires=required,
5252
cmdclass={"test": PyTest},
53-
tests_require=["pytest>=4.4.0", "pytest-xdist", "requests", "attrs", "pydantic>=1.3"],
53+
tests_require=["pytest>=4.4.0", "pytest-xdist", "requests", "attrs", "pydantic>=1.3", "PyYaml"],
5454
data_files=[('', ['requirements.txt', 'pytest.ini', '.coveragerc', 'LICENSE', 'README.md', 'CHANGELOG.md'])]
5555
)

test/test_cli/data/file.ini

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[owner]
2+
name = John Doe
3+
organization = Acme Widgets Inc.
4+
5+
[database]
6+
; use IP address in case network name resolution is not working
7+
server = 192.0.2.62
8+
port = 143
9+
file = payroll.dat

0 commit comments

Comments
 (0)