Skip to content

Commit b50e836

Browse files
committed
Add Literal supports for string constants (#30)
* Add types_styles parameter * Pydantic: rewrite string_serializable replace with actual types using types_style * Integrate StringLiteral into core * Pydantic: Implement StringLiteral logic * Use StringLiteral for base generator and dataclass generator * Add --max-strings-literals CLI arg * Add test for type styles and string literals
1 parent eb32a94 commit b50e836

File tree

22 files changed

+493
-156
lines changed

22 files changed

+493
-156
lines changed

README.md

Lines changed: 56 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -83,47 +83,46 @@ driver_standings.json
8383
```
8484

8585
```
86-
json2models -f attrs -l DriverStandings driver_standings.json
86+
json2models -f pydantic -s flat -l DriverStandings - driver_standings.json
8787
```
8888

8989
```python
90-
import attr
91-
from json_to_models.dynamic_typing import IntString, IsoDateString
90+
r"""
91+
generated by json2python-models v0.1.2 at Mon May 4 17:46:30 2020
92+
command: /opt/projects/json2python-models/venv/bin/json2models -f pydantic -s flat -l DriverStandings - driver_standings.json
93+
"""
94+
from pydantic import BaseModel, Field
9295
from typing import List
93-
94-
95-
@attr.s
96-
class DriverStandings:
97-
@attr.s
98-
class DriverStanding:
99-
@attr.s
100-
class Driver:
101-
driver_id: str = attr.ib()
102-
permanent_number: IntString = attr.ib(converter=IntString)
103-
code: str = attr.ib()
104-
url: str = attr.ib()
105-
given_name: str = attr.ib()
106-
family_name: str = attr.ib()
107-
date_of_birth: IsoDateString = attr.ib(converter=IsoDateString)
108-
nationality: str = attr.ib()
109-
110-
@attr.s
111-
class Constructor:
112-
constructor_id: str = attr.ib()
113-
url: str = attr.ib()
114-
name: str = attr.ib()
115-
nationality: str = attr.ib()
116-
117-
position: IntString = attr.ib(converter=IntString)
118-
position_text: IntString = attr.ib(converter=IntString)
119-
points: IntString = attr.ib(converter=IntString)
120-
wins: IntString = attr.ib(converter=IntString)
121-
driver: 'Driver' = attr.ib()
122-
constructors: List['Constructor'] = attr.ib()
123-
124-
season: IntString = attr.ib(converter=IntString)
125-
round: IntString = attr.ib(converter=IntString)
126-
driver_standings: List['DriverStanding'] = attr.ib()
96+
from typing_extensions import Literal
97+
98+
class DriverStandings(BaseModel):
99+
season: int
100+
round_: int = Field(..., alias="round")
101+
DriverStandings: List['DriverStanding']
102+
103+
class DriverStanding(BaseModel):
104+
position: int
105+
position_text: int = Field(..., alias="positionText")
106+
points: int
107+
wins: int
108+
driver: 'Driver' = Field(..., alias="Driver")
109+
constructors: List['Constructor'] = Field(..., alias="Constructors")
110+
111+
class Driver(BaseModel):
112+
driver_id: str = Field(..., alias="driverId")
113+
permanent_number: int = Field(..., alias="permanentNumber")
114+
code: str
115+
url: str
116+
given_name: str = Field(..., alias="givenName")
117+
family_name: str = Field(..., alias="familyName")
118+
date_of_birth: str = Field(..., alias="dateOfBirth")
119+
nationality: str
120+
121+
class Constructor(BaseModel):
122+
constructor_id: str = Field(..., alias="constructorId")
123+
url: str
124+
name: str
125+
nationality: Literal["Austrian", "German", "American", "British", "Italian", "French"]
127126
```
128127

129128
</p>
@@ -139,14 +138,19 @@ class DriverStandings:
139138
It requires a lit bit of tweaking:
140139
* Some fields store routes/models specs as dicts
141140
* There is a lot of optinal fields so we reduce merging threshold
141+
* Disable string literals
142142

143143
```
144-
json_to_models -s flat -f dataclasses -m Swagger testing_tools/swagger.json
145-
--dict-keys-fields securityDefinitions paths responses definitions properties
146-
--merge percent_50 number
144+
json2models -s flat -f dataclasses -m Swagger testing_tools/swagger.json \
145+
--dict-keys-fields securityDefinitions paths responses definitions properties \
146+
--merge percent_50 number --max-strings-literals 0
147147
```
148148

149149
```python
150+
r"""
151+
generated by json2python-models v0.1.2 at Mon May 4 18:08:09 2020
152+
command: /opt/projects/json2python-models/json_to_models/__main__.py -s flat -f dataclasses -m Swagger testing_tools/swagger.json --max-strings-literals 0 --dict-keys-fields securityDefinitions paths responses definitions properties --merge percent_50 number
153+
"""
150154
from dataclasses import dataclass, field
151155
from json_to_models.dynamic_typing import FloatString
152156
from typing import Any, Dict, List, Optional, Union
@@ -192,15 +196,15 @@ class Path:
192196

193197
@dataclass
194198
class Property:
195-
type: str
196-
format: Optional[str] = None
199+
type_: str
200+
format_: Optional[str] = None
197201
xnullable: Optional[bool] = None
198202
items: Optional['Item_Schema'] = None
199203

200204

201205
@dataclass
202206
class Property_2E:
203-
type: str
207+
type_: str
204208
title: Optional[str] = None
205209
read_only: Optional[bool] = None
206210
max_length: Optional[int] = None
@@ -209,26 +213,26 @@ class Property_2E:
209213
enum: Optional[List[str]] = field(default_factory=list)
210214
maximum: Optional[int] = None
211215
minimum: Optional[int] = None
212-
format: Optional[str] = None
216+
format_: Optional[str] = None
213217

214218

215219
@dataclass
216220
class Item:
217-
ref: Optional[str] = None
218221
title: Optional[str] = None
219-
type: Optional[str] = None
222+
type_: Optional[str] = None
223+
ref: Optional[str] = None
220224
max_length: Optional[int] = None
221225
min_length: Optional[int] = None
222226

223227

224228
@dataclass
225229
class Parameter_SecurityDefinition:
226-
name: str
227-
in_: str
230+
name: Optional[str] = None
231+
in_: Optional[str] = None
228232
required: Optional[bool] = None
229233
schema: Optional['Item_Schema'] = None
230-
type: Optional[str] = None
231234
description: Optional[str] = None
235+
type_: Optional[str] = None
232236

233237

234238
@dataclass
@@ -253,10 +257,10 @@ class Response:
253257

254258
@dataclass
255259
class Definition_Schema:
256-
ref: Optional[str] = None
260+
type_: str
257261
required: Optional[List[str]] = field(default_factory=list)
258-
type: Optional[str] = None
259-
properties: Optional[Dict[str, Union['Property_2E', 'Property']]] = field(default_factory=dict)
262+
properties: Optional[Dict[str, Union['Property', 'Property_2E']]] = field(default_factory=dict)
263+
ref: Optional[str] = None
260264
```
261265

262266
</p>

json_to_models/cli.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ def __init__(self):
5353
self.models_data: Dict[str, Iterable[dict]] = {} # -m/-l
5454
self.enable_datetime: bool = False # --datetime
5555
self.strings_converters: bool = False # --strings-converters
56+
self.max_literals: int = -1 # --max-strings-literals
5657
self.merge_policy: List[ModelCmp] = [] # --merge
5758
self.structure_fn: STRUCTURE_FN_TYPE = None # -s
5859
self.model_generator: Type[GenericModelCodeGenerator] = None # -f & --code-generator
@@ -83,6 +84,7 @@ def parse_args(self, args: List[str] = None):
8384
self.enable_datetime = namespace.datetime
8485
disable_unicode_conversion = namespace.disable_unicode_conversion
8586
self.strings_converters = namespace.strings_converters
87+
self.max_literals = namespace.max_strings_literals
8688
merge_policy = [m.split("_") if "_" in m else m for m in namespace.merge]
8789
structure = namespace.structure
8890
framework = namespace.framework
@@ -204,8 +206,11 @@ def set_args(
204206
m = importlib.import_module(module)
205207
self.model_generator = getattr(m, cls)
206208

207-
self.model_generator_kwargs = {} if not self.strings_converters else {'post_init_converters': True}
208-
self.model_generator_kwargs['convert_unicode'] = not disable_unicode_conversion
209+
self.model_generator_kwargs = dict(
210+
post_init_converters=self.strings_converters,
211+
convert_unicode=not disable_unicode_conversion,
212+
max_literals=self.max_literals
213+
)
209214
if code_generator_kwargs_raw:
210215
for item in code_generator_kwargs_raw:
211216
if item[0] == '"':
@@ -279,6 +284,15 @@ def _create_argparser(cls) -> argparse.ArgumentParser:
279284
action="store_true",
280285
help="Enable generation of string types converters (i.e. IsoDatetimeString or BooleanString).\n\n"
281286
)
287+
parser.add_argument(
288+
"--max-strings-literals",
289+
type=int,
290+
default=GenericModelCodeGenerator.DEFAULT_MAX_LITERALS,
291+
metavar='NUMBER',
292+
help="Generate Literal['foo', 'bar'] when field have less than NUMBER string constants as values.\n"
293+
f"Pass 0 to disable. By default NUMBER={GenericModelCodeGenerator.DEFAULT_MAX_LITERALS}"
294+
f" (some generator classes could override it)\n\n"
295+
)
282296
parser.add_argument(
283297
"--disable-unicode-conversion", "--no-unidecode",
284298
action="store_true",

json_to_models/dynamic_typing/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from .base import (
22
BaseType, ImportPathList, MetaData, Null, Unknown, get_hash_string
33
)
4-
from .complex import ComplexType, DDict, DList, DOptional, DTuple, DUnion, SingleType
4+
from .complex import ComplexType, DDict, DList, DOptional, DTuple, DUnion, SingleType, StringLiteral
55
from .models_meta import AbsoluteModelRef, ModelMeta, ModelPtr
66
from .string_datetime import IsoDateString, IsoDatetimeString, IsoTimeString, register_datetime_classes
77
from .string_serializable import (

json_to_models/dynamic_typing/base.py

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

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

@@ -21,14 +21,30 @@ def replace(self, t: Union['MetaData', List['MetaData']], **kwargs) -> 'BaseType
2121
"""
2222
raise NotImplementedError()
2323

24-
def to_typing_code(self) -> Tuple[ImportPathList, str]:
24+
def to_typing_code(self, types_style: Dict[Union['BaseType', Type['BaseType']], dict]) \
25+
-> Tuple[ImportPathList, str]:
2526
"""
2627
Return typing code that represents this metadata and import path of classes that are used in this code
2728
29+
:param types_style: Hints for .to_typing_code() for different type wrappers
2830
:return: ((module_name, (class_name, ...)), code)
2931
"""
3032
raise NotImplementedError()
3133

34+
@classmethod
35+
def get_options_for_type(
36+
cls,
37+
t: Union['BaseType', Type['BaseType']],
38+
types_style: Dict[Union['BaseType', Type['BaseType']], dict]
39+
) -> dict:
40+
t_cls = t if isclass(t) else type(t)
41+
mro = t_cls.__mro__
42+
for base in mro:
43+
options = types_style.get(base, ...)
44+
if options is not Ellipsis:
45+
return options
46+
return {}
47+
3248
def to_hash_string(self) -> str:
3349
"""
3450
Return unique string that can be used to generate hash of type instance.
@@ -71,7 +87,8 @@ def __iter__(self) -> Iterable['MetaData']:
7187
def replace(self, t: 'MetaData', **kwargs) -> 'UnknownType':
7288
return self
7389

74-
def to_typing_code(self) -> Tuple[ImportPathList, str]:
90+
def to_typing_code(self, types_style: Dict[Union['BaseType', Type['BaseType']], dict]) \
91+
-> Tuple[ImportPathList, str]:
7592
return ([('typing', 'Any')], 'Any')
7693

7794
def to_hash_string(self) -> str:
@@ -90,7 +107,8 @@ def __iter__(self) -> Iterable['MetaData']:
90107
def replace(self, t: 'MetaData', **kwargs) -> 'NoneType':
91108
return self
92109

93-
def to_typing_code(self) -> Tuple[ImportPathList, str]:
110+
def to_typing_code(self, types_style: Dict[Union['BaseType', Type['BaseType']], dict]) \
111+
-> Tuple[ImportPathList, str]:
94112
return ([], 'None')
95113

96114
def to_hash_string(self) -> str:

0 commit comments

Comments
 (0)