Skip to content

Commit 9be61fd

Browse files
authored
Merge pull request #45 from ialarmedalien/add_preamble_section
Add preamble section
2 parents ebf9d0a + 86caba8 commit 9be61fd

File tree

4 files changed

+167
-70
lines changed

4 files changed

+167
-70
lines changed

README.md

Lines changed: 73 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77

88
![Example](/etc/convert.png)
99

10-
json2python-models is a [Python](https://www.python.org/) tool that can generate Python models classes
11-
([pydantic](https://github.com/samuelcolvin/pydantic), dataclasses, [attrs](https://github.com/python-attrs/attrs))
12-
from JSON dataset.
10+
json2python-models is a [Python](https://www.python.org/) tool that can generate Python models classes
11+
([pydantic](https://github.com/samuelcolvin/pydantic), dataclasses, [attrs](https://github.com/python-attrs/attrs))
12+
from JSON datasets.
1313

1414
## Features
1515

@@ -155,9 +155,9 @@ class Constructor(BaseModel):
155155

156156
`swagger.json` from any online API (I tested file generated by drf-yasg and another one for Spotify API)
157157

158-
It requires a lit bit of tweaking:
158+
It requires a bit of tweaking:
159159
* Some fields store routes/models specs as dicts
160-
* There is a lot of optinal fields so we reduce merging threshold
160+
* There are a lot of optinal fields so we reduce merging threshold
161161
* Disable string literals
162162

163163
```
@@ -405,9 +405,45 @@ class Run(BaseModel):
405405

406406
</p></details>
407407

408+
### Example with preamble
409+
410+
<details><summary>----- Show -----</summary>
411+
<p>
412+
A simple example to demonstrate adding extra code before the class list.
413+
414+
```sh
415+
json2models -f pydantic --preamble "# set up defaults
416+
USERNAME = 'user'
417+
SERVER_IP = '127.0.0.1'
418+
" -m Swagger testing_tools/swagger.json
419+
```
420+
421+
```py
422+
r"""
423+
generated by json2python-models v0.2.5 at Tue Aug 23 08:55:09 2022
424+
command: json2models -f pydantic --preamble # set up defaults
425+
USERNAME = 'user'
426+
SERVER_IP = '127.0.0.1'
427+
-m Swagger testing_tools/swagger.json -o output.py
428+
"""
429+
from pydantic import BaseModel, Field
430+
from typing import Any, List, Literal, Optional, Union
431+
432+
433+
# set up defaults
434+
USERNAME = 'user'
435+
SERVER_IP = '127.0.0.1'
436+
437+
438+
439+
class Swagger(BaseModel):
440+
# etc.
441+
```
442+
</p></details>
443+
408444
## Installation
409445

410-
| **Be ware**: this project supports only `python3.7` and higher. |
446+
| **Beware**: this project supports only `python3.7` and higher. |
411447
| --- |
412448

413449
To install it, use `pip`:
@@ -426,7 +462,7 @@ python setup.py install
426462

427463
### CLI
428464

429-
For regular usage CLI tool is the best option. After you install this package you could use it as `json2models <arguments>`
465+
For regular usage CLI tool is the best option. After you install this package you can use it as `json2models <arguments>`
430466
or `python -m json_to_models <arguments>`. I.e.:
431467
```
432468
json2models -m Car car_*.json -f attrs > car.py
@@ -464,61 +500,71 @@ Arguments:
464500
* **Format**: `-f {base, pydantic, attrs, dataclasses, custom}`
465501
* **Example**: `-f pydantic`
466502
* **Default**: `-f base`
467-
503+
468504
* `-s`, `--structure` - Models composition style.
469-
* **Format**: `-s {flat, nested}`
505+
* **Format**: `-s {flat, nested}`
470506
* **Example**: `-s nested`
471507
* **Default**: `-s flat`
472-
508+
509+
* `--preamble` - Additional material to be
510+
* **Format**: `--preamble "<formatted python code string to be added after module imports>"`
511+
* **Example**:
512+
```sh
513+
--preamble "# set up defaults
514+
USERNAME = 'user'
515+
SERVER = '127.0.0.1'"
516+
```
517+
* **Optional**
518+
473519
* `--datetime` - Enable datetime/date/time strings parsing.
474520
* **Default**: disabled
475521
* **Warning**: This can lead to 6-7 times slowdown on large datasets. Be sure that you really need this option.
476-
522+
477523
* `--disable-unicode-conversion`, `--no-unidecode` - Disable unicode conversion in field labels and class names
478524
* **Default**: enabled
479-
525+
480526
* `--strings-converters` - Enable generation of string types converters (i.e. `IsoDatetimeString` or `BooleanString`).
481527
* **Default**: disabled
482-
528+
483529
* `--max-strings-literals` - Generate `Literal['foo', 'bar']` when field have less than NUMBER string constants as values.
484-
* **Format**: `--max-strings-literals <NUMBER>`
530+
* **Format**: `--max-strings-literals <NUMBER>`
485531
* **Default**: 10 (generator classes could override it)
486532
* **Example**: `--max-strings-literals 5` - only 5 literals will be saved and used to code generation
487533
* **Note**: There could not be more than **15** literals per field (for performance reasons)
488534
* **Note**: `attrs` code generator do not use Literals and just generate `str` fields instead
489535

490-
* `--merge` - Merge policy settings. Possible values are:
536+
* `--merge` - Merge policy settings. Possible values are:
491537
* **Format**: `--merge MERGE_POLICY [MERGE_POLICY ...]`
492538
* **Possible values** (MERGE_POLICY):
493-
* `percent[_<percent>]` - two models had a certain percentage of matched field names.
494-
Custom value could be i.e. `percent_95`.
495-
* `number[_<number>]` - two models had a certain number of matched field names.
539+
* `percent[_<percent>]` - two models had a certain percentage of matched field names.
540+
Custom value could be i.e. `percent_95`.
541+
* `number[_<number>]` - two models had a certain number of matched field names.
496542
* `exact` - two models should have exact same field names to merge.
497543
* **Example**: `--merge percent_95 number_20` - merge if 95% of fields are matched or 20 of fields are matched
498544
* **Default**: `--merge percent_70 number_10`
499-
545+
500546
* `--dict-keys-regex`, `--dkr` - List of regular expressions (Python syntax).
501-
If all keys of some dict are match one of the pattern then
547+
If all keys of some dict are match one of the pattern then
502548
this dict will be marked as dict field but not nested model.
503549
* **Format**: `--dkr RegEx [RegEx ...]`
504550
* **Example**: `--dkr node_\d+ \d+_\d+_\d+`
505-
* **Note**: `^` and `$` (string borders) tokens will be added automatically but you
551+
* **Note**: `^` and `$` (string borders) tokens will be added automatically but you
506552
have to escape other special characters manually.
507553
* **Optional**
508-
554+
509555
* `--dict-keys-fields`, `--dkf` - List of model fields names that will be marked as dict fields
510556
* **Format**: `--dkf FIELD_NAME [FIELD_NAME ...]`
511557
* **Example**: `--dkf "dict_data" "mapping"`
512558
* **Optional**
513-
559+
514560
* `--code-generator` - Absolute import path to `GenericModelCodeGenerator` subclass.
515561
* **Format**: `--code-generator CODE_GENERATOR`
516562
* **Example**: `-f mypackage.mymodule.DjangoModelsGenerator`
517563
* **Note**: Is ignored without `-f custom` but is required with it.
518-
519-
* `--code-generator-kwargs` - List of GenericModelCodeGenerator subclass arguments (for `__init__` method,
520-
see docs of specific subclass).
521-
Each argument should be in following format: `argument_name=value` or `"argument_name=value with space"`.
564+
565+
* `--code-generator-kwargs` - List of GenericModelCodeGenerator subclass arguments (for `__init__` method,
566+
see docs of specific subclass).
567+
Each argument should be in following format: `argument_name=value` or `"argument_name=value with space"`.
522568
Boolean values should be passed in JS style: `true` or `false`
523569
* **Format**: `--code-generator-kwargs [NAME=VALUE [NAME=VALUE ...]]`
524570
* **Example**: `--code-generator-kwargs kwarg1=true kwarg2=10 "kwarg3=It is string with spaces"`

json_to_models/cli.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,12 @@ def parse_args(self, args: List[str] = None):
102102
code_generator_kwargs_raw: List[str] = namespace.code_generator_kwargs
103103
dict_keys_regex: List[str] = namespace.dict_keys_regex
104104
dict_keys_fields: List[str] = namespace.dict_keys_fields
105+
preamble: str = namespace.preamble
105106

106-
self.validate(models, models_lists, merge_policy, framework, code_generator)
107+
self.validate(models_lists, merge_policy, framework, code_generator)
107108
self.setup_models_data(models, models_lists, parser)
108109
self.set_args(merge_policy, structure, framework, code_generator, code_generator_kwargs_raw,
109-
dict_keys_regex, dict_keys_fields, disable_unicode_conversion)
110+
dict_keys_regex, dict_keys_fields, disable_unicode_conversion, preamble)
110111

111112
def run(self):
112113
if self.enable_datetime:
@@ -122,8 +123,11 @@ def run(self):
122123
registry.merge_models(generator)
123124
registry.generate_names()
124125
structure = self.structure_fn(registry.models_map)
125-
output = self.version_string + \
126-
generate_code(structure, self.model_generator, class_generator_kwargs=self.model_generator_kwargs)
126+
output = self.version_string + generate_code(
127+
structure,
128+
self.model_generator,
129+
class_generator_kwargs=self.model_generator_kwargs,
130+
preamble=self.preamble)
127131
if self.output_file:
128132
with open(self.output_file, "w", encoding="utf-8") as f:
129133
f.write(output)
@@ -140,11 +144,10 @@ def version_string(self):
140144
'"""\n'
141145
)
142146

143-
def validate(self, models, models_list, merge_policy, framework, code_generator):
147+
def validate(self, models_list, merge_policy, framework, code_generator):
144148
"""
145149
Validate parsed args
146150
147-
:param models: List of pairs (model name, list of filesystem path)
148151
:param models_list: List of pairs (model name, list of lookup expr and filesystem path)
149152
:param merge_policy: List of merge policies. Each merge policy is either string or string and policy arguments
150153
:param framework: Framework name (predefined code generator)
@@ -196,7 +199,8 @@ def set_args(
196199
code_generator_kwargs_raw: List[str],
197200
dict_keys_regex: List[str],
198201
dict_keys_fields: List[str],
199-
disable_unicode_conversion: bool
202+
disable_unicode_conversion: bool,
203+
preamble: str,
200204
):
201205
"""
202206
Convert CLI args to python representation and set them to appropriate object attributes
@@ -236,7 +240,9 @@ def set_args(
236240

237241
self.dict_keys_regex = [re.compile(rf"^{r}$") for r in dict_keys_regex] if dict_keys_regex else ()
238242
self.dict_keys_fields = dict_keys_fields or ()
239-
243+
if preamble:
244+
preamble = preamble.strip()
245+
self.preamble = preamble or None
240246
self.initialized = True
241247

242248
@classmethod
@@ -366,6 +372,11 @@ def _create_argparser(cls) -> argparse.ArgumentParser:
366372
"Boolean values should be passed in JS style: true | false"
367373
"\n\n"
368374
)
375+
parser.add_argument(
376+
"--preamble",
377+
type=str,
378+
help="Code to insert into the generated file after the imports and before the list of classes\n\n"
379+
)
369380

370381
return parser
371382

json_to_models/models/base.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,19 +54,19 @@ class GenericModelCodeGenerator:
5454
@{{ decorator }}
5555
{% endfor -%}
5656
class {{ name }}{% if bases %}({{ bases }}){% endif %}:
57-
57+
5858
{%- for code in nested %}
5959
{{ code }}
6060
{% endfor -%}
61-
61+
6262
{%- if fields -%}
6363
{%- for field in fields %}
6464
{{ field }}
6565
{%- endfor %}
6666
{%- else %}
6767
pass
6868
{%- endif -%}
69-
{%- if extra %}
69+
{%- if extra %}
7070
{{ extra }}
7171
{%- endif -%}
7272
""")
@@ -210,7 +210,7 @@ def _generate_code(
210210
lvl=0
211211
) -> Tuple[ImportPathList, List[str]]:
212212
"""
213-
Walk thought models structure and covert them into code
213+
Walk through the model structures and convert them into code
214214
215215
:param structure: Result of compose_models or similar function
216216
:param class_generator: GenericModelCodeGenerator subclass
@@ -241,23 +241,28 @@ def _generate_code(
241241

242242

243243
def generate_code(structure: ModelsStructureType, class_generator: Type[GenericModelCodeGenerator],
244-
class_generator_kwargs: dict = None, objects_delimiter: str = OBJECTS_DELIMITER) -> str:
244+
class_generator_kwargs: dict = None,
245+
objects_delimiter: str = OBJECTS_DELIMITER,
246+
preamble: str = None) -> str:
245247
"""
246248
Generate ready-to-use code
247249
248250
:param structure: Result of compose_models or similar function
249251
:param class_generator: GenericModelCodeGenerator subclass
250252
:param class_generator_kwargs: kwags for GenericModelCodeGenerator init
251253
:param objects_delimiter: Delimiter between root level classes
254+
:param preamble: code to insert after the imports and before the classes
252255
:return: Generated code
253256
"""
254257
root, mapping = structure
255258
with AbsoluteModelRef.inject(mapping):
256259
imports, classes = _generate_code(root, class_generator, class_generator_kwargs or {})
260+
imports_str = ""
257261
if imports:
258262
imports_str = compile_imports(imports) + objects_delimiter
259-
else:
260-
imports_str = ""
263+
if preamble:
264+
imports_str += preamble + objects_delimiter
265+
261266
return imports_str + objects_delimiter.join(classes) + "\n"
262267

263268

0 commit comments

Comments
 (0)