Skip to content

Commit 3908778

Browse files
committed
Fix mutable defaults in generated dataclasses. Closes #53
1 parent 2ef30b4 commit 3908778

File tree

6 files changed

+97
-30
lines changed

6 files changed

+97
-30
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3434
- Client will still be generated if there are recoverable errors, excluding endpoints that had those errors
3535
- Output from isort and black when generating will now be suppressed
3636

37+
### Fixes
38+
- Defaults within models dataclasses for `Dict` or `List` properties will now be properly declared as a
39+
`field` with the `default_factory` parameter to prevent errors related to mutable defaults.
40+
3741
## 0.3.0 - 2020-04-25
3842
### Additions
3943
- Link to the GitHub repository from PyPI (#26). Thanks @theY4Kman!

end_to_end_tests/fastapi_app/__init__.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from datetime import date, datetime
44
from enum import Enum
55
from pathlib import Path
6-
from typing import List, Union
6+
from typing import Any, Dict, List, Union
77

88
from fastapi import APIRouter, FastAPI, File, Query, UploadFile
99
from pydantic import BaseModel
@@ -42,7 +42,8 @@ class AModel(BaseModel):
4242
""" A Model for testing all the ways custom objects can be used """
4343

4444
an_enum_value: AnEnum
45-
nested_list_of_enums: List[List[DifferentEnum]]
45+
nested_list_of_enums: List[List[DifferentEnum]] = []
46+
some_dict: Dict[str, str] = {}
4647
aCamelDateTime: Union[datetime, date]
4748
a_date: date
4849

end_to_end_tests/fastapi_app/openapi.json

+10-2
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,6 @@
144144
"title": "AModel",
145145
"required": [
146146
"an_enum_value",
147-
"nested_list_of_enums",
148147
"aCamelDateTime",
149148
"a_date"
150149
],
@@ -168,7 +167,16 @@
168167
"OTHER"
169168
]
170169
}
171-
}
170+
},
171+
"default": []
172+
},
173+
"some_dict": {
174+
"title": "Some Dict",
175+
"type": "object",
176+
"additionalProperties": {
177+
"type": "string"
178+
},
179+
"default": {}
172180
},
173181
"aCamelDateTime": {
174182
"title": "Acameldatetime",
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from __future__ import annotations
22

3-
from dataclasses import dataclass
3+
from dataclasses import dataclass, field
44
from datetime import date, datetime
5-
from typing import Any, Dict, List, Union, cast
5+
from typing import Any, Dict, List, Optional, Union, cast
66

77
from .an_enum_value import AnEnumValue
88
from .an_enum_value1 import AnEnumValue1
@@ -13,23 +13,16 @@ class AModel:
1313
""" A Model for testing all the ways custom objects can be used """
1414

1515
an_enum_value: AnEnumValue
16-
nested_list_of_enums: List[List[AnEnumValue1]]
1716
a_camel_date_time: Union[datetime, date]
1817
a_date: date
18+
nested_list_of_enums: Optional[List[List[AnEnumValue1]]] = field(
19+
default_factory=lambda: cast(Optional[List[List[AnEnumValue1]]], [])
20+
)
21+
some_dict: Optional[Dict[Any, Any]] = field(default_factory=lambda: cast(Optional[Dict[Any, Any]], {}))
1922

2023
def to_dict(self) -> Dict[str, Any]:
2124
an_enum_value = self.an_enum_value.value
2225

23-
nested_list_of_enums = []
24-
for nested_list_of_enums_item_data in self.nested_list_of_enums:
25-
nested_list_of_enums_item = []
26-
for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data:
27-
nested_list_of_enums_item_item = nested_list_of_enums_item_item_data.value
28-
29-
nested_list_of_enums_item.append(nested_list_of_enums_item_item)
30-
31-
nested_list_of_enums.append(nested_list_of_enums_item)
32-
3326
if isinstance(self.a_camel_date_time, datetime):
3427
a_camel_date_time = self.a_camel_date_time.isoformat()
3528

@@ -38,27 +31,33 @@ def to_dict(self) -> Dict[str, Any]:
3831

3932
a_date = self.a_date.isoformat()
4033

34+
if self.nested_list_of_enums is None:
35+
nested_list_of_enums = None
36+
else:
37+
nested_list_of_enums = []
38+
for nested_list_of_enums_item_data in self.nested_list_of_enums:
39+
nested_list_of_enums_item = []
40+
for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data:
41+
nested_list_of_enums_item_item = nested_list_of_enums_item_item_data.value
42+
43+
nested_list_of_enums_item.append(nested_list_of_enums_item_item)
44+
45+
nested_list_of_enums.append(nested_list_of_enums_item)
46+
47+
some_dict = self.some_dict
48+
4149
return {
4250
"an_enum_value": an_enum_value,
43-
"nested_list_of_enums": nested_list_of_enums,
4451
"aCamelDateTime": a_camel_date_time,
4552
"a_date": a_date,
53+
"nested_list_of_enums": nested_list_of_enums,
54+
"some_dict": some_dict,
4655
}
4756

4857
@staticmethod
4958
def from_dict(d: Dict[str, Any]) -> AModel:
5059
an_enum_value = AnEnumValue(d["an_enum_value"])
5160

52-
nested_list_of_enums = []
53-
for nested_list_of_enums_item_data in d["nested_list_of_enums"]:
54-
nested_list_of_enums_item = []
55-
for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data:
56-
nested_list_of_enums_item_item = AnEnumValue1(nested_list_of_enums_item_item_data)
57-
58-
nested_list_of_enums_item.append(nested_list_of_enums_item_item)
59-
60-
nested_list_of_enums.append(nested_list_of_enums_item)
61-
6261
def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime, date]:
6362
a_camel_date_time: Union[datetime, date]
6463
try:
@@ -75,9 +74,22 @@ def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime, date]:
7574

7675
a_date = date.fromisoformat(d["a_date"])
7776

77+
nested_list_of_enums = []
78+
for nested_list_of_enums_item_data in d.get("nested_list_of_enums") or []:
79+
nested_list_of_enums_item = []
80+
for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data:
81+
nested_list_of_enums_item_item = AnEnumValue1(nested_list_of_enums_item_item_data)
82+
83+
nested_list_of_enums_item.append(nested_list_of_enums_item_item)
84+
85+
nested_list_of_enums.append(nested_list_of_enums_item)
86+
87+
some_dict = d.get("some_dict")
88+
7889
return AModel(
7990
an_enum_value=an_enum_value,
80-
nested_list_of_enums=nested_list_of_enums,
8191
a_camel_date_time=a_camel_date_time,
8292
a_date=a_date,
93+
nested_list_of_enums=nested_list_of_enums,
94+
some_dict=some_dict,
8395
)

openapi_python_client/openapi_parser/properties.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,11 @@ class ListProperty(Property, Generic[InnerProp]):
176176
inner_property: InnerProp
177177
template: ClassVar[str] = "list_property.pyi"
178178

179+
def __post_init__(self) -> None:
180+
super().__post_init__()
181+
if self.default is not None:
182+
self.default = f"field(default_factory=lambda: cast({self.get_type_string()}, {self.default}))"
183+
179184
def get_type_string(self) -> str:
180185
""" Get a string representation of type that should be used when declaring this property """
181186
if self.required:
@@ -192,6 +197,9 @@ def get_imports(self, *, prefix: str) -> Set[str]:
192197
imports = super().get_imports(prefix=prefix)
193198
imports.update(self.inner_property.get_imports(prefix=prefix))
194199
imports.add("from typing import List")
200+
if self.default is not None:
201+
imports.add("from dataclasses import field")
202+
imports.add("from typing import cast")
195203
return imports
196204

197205

@@ -331,6 +339,11 @@ class DictProperty(Property):
331339

332340
_type_string: ClassVar[str] = "Dict[Any, Any]"
333341

342+
def __post_init__(self) -> None:
343+
super().__post_init__()
344+
if self.default is not None:
345+
self.default = f"field(default_factory=lambda: cast({self.get_type_string()}, {self.default}))"
346+
334347
def get_imports(self, *, prefix: str) -> Set[str]:
335348
"""
336349
Get a set of import strings that should be included when this property is used somewhere
@@ -340,6 +353,9 @@ def get_imports(self, *, prefix: str) -> Set[str]:
340353
"""
341354
imports = super().get_imports(prefix=prefix)
342355
imports.add("from typing import Dict")
356+
if self.default is not None:
357+
imports.add("from dataclasses import field")
358+
imports.add("from typing import cast")
343359
return imports
344360

345361

@@ -391,7 +407,7 @@ def property_from_dict(name: str, required: bool, data: Dict[str, Any]) -> Prope
391407
return ListProperty(
392408
name=name,
393409
required=required,
394-
default=None,
410+
default=data.get("default"),
395411
inner_property=property_from_dict(name=f"{name}_item", required=True, data=data["items"]),
396412
)
397413
elif data["type"] == "object":

tests/test_openapi_parser/test_properties.py

+26
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ def test_get_type_string(self, mocker):
130130
p.required = False
131131
assert p.get_type_string() == f"Optional[List[{inner_type_string}]]"
132132

133+
p = ListProperty(name="test", required=True, default=[], inner_property=inner_property)
134+
assert p.default == f"field(default_factory=lambda: cast(List[{inner_type_string}], []))"
135+
133136
def test_get_type_imports(self, mocker):
134137
from openapi_python_client.openapi_parser.properties import ListProperty
135138

@@ -150,6 +153,15 @@ def test_get_type_imports(self, mocker):
150153
"from typing import Optional",
151154
}
152155

156+
p.default = mocker.MagicMock()
157+
assert p.get_imports(prefix=prefix) == {
158+
inner_import,
159+
"from typing import Optional",
160+
"from typing import List",
161+
"from typing import cast",
162+
"from dataclasses import field",
163+
}
164+
153165

154166
class TestUnionProperty:
155167
def test_get_type_string(self, mocker):
@@ -328,6 +340,12 @@ def test_get_imports(self, mocker):
328340

329341

330342
class TestDictProperty:
343+
def test___post_init__(self):
344+
from openapi_python_client.openapi_parser.properties import DictProperty
345+
346+
p = DictProperty(name="blah", required=True, default={})
347+
assert p.default == "field(default_factory=lambda: cast(Dict[Any, Any], {}))"
348+
331349
def test_get_imports(self, mocker):
332350
from openapi_python_client.openapi_parser.properties import DictProperty
333351

@@ -345,6 +363,14 @@ def test_get_imports(self, mocker):
345363
"from typing import Dict",
346364
}
347365

366+
p.default = mocker.MagicMock()
367+
assert p.get_imports(prefix=prefix) == {
368+
"from typing import Optional",
369+
"from typing import Dict",
370+
"from typing import cast",
371+
"from dataclasses import field",
372+
}
373+
348374

349375
class TestPropertyFromDict:
350376
def test_property_from_dict_enum(self, mocker):

0 commit comments

Comments
 (0)