Skip to content

Commit 351478f

Browse files
fix(template): Fix deserialization of non-required properties (#334)
Also unifies None and Unset checking in deserialization to prevent future similar bugs.
1 parent b2adc2c commit 351478f

19 files changed

+122
-103
lines changed

end_to_end_tests/golden-record-custom/custom_e2e/api/tests/upload_file_tests_upload_post.py

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ def _parse_response(*, response: httpx.Response) -> Optional[Union[
2424
if response.status_code == 422:
2525
response_422 = HTTPValidationError.from_dict(response.json())
2626

27+
28+
2729
return response_422
2830
return None
2931

end_to_end_tests/golden-record-custom/custom_e2e/models/a_model.py

+18-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import datetime
2-
from typing import Any, Dict, List, Optional, Type, TypeVar, Union, cast
2+
from typing import Any, Dict, List, Optional, Type, TypeVar, Union
33

44
import attr
55
from dateutil.parser import isoparse
@@ -28,6 +28,7 @@ class AModel:
2828
required_nullable: Optional[str]
2929
nullable_model: Optional[AModelNullableModel]
3030
nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET
31+
a_not_required_date: Union[Unset, datetime.date] = UNSET
3132
attr_1_leading_digit: Union[Unset, str] = UNSET
3233
not_required_nullable: Union[Unset, Optional[str]] = UNSET
3334
not_required_not_nullable: Union[Unset, str] = UNSET
@@ -60,6 +61,10 @@ def to_dict(self) -> Dict[str, Any]:
6061
nested_list_of_enums.append(nested_list_of_enums_item)
6162

6263
a_nullable_date = self.a_nullable_date.isoformat() if self.a_nullable_date else None
64+
a_not_required_date: Union[Unset, str] = UNSET
65+
if not isinstance(self.a_not_required_date, Unset):
66+
a_not_required_date = self.a_not_required_date.isoformat()
67+
6368
attr_1_leading_digit = self.attr_1_leading_digit
6469
required_nullable = self.required_nullable
6570
not_required_nullable = self.not_required_nullable
@@ -91,6 +96,8 @@ def to_dict(self) -> Dict[str, Any]:
9196
)
9297
if nested_list_of_enums is not UNSET:
9398
field_dict["nested_list_of_enums"] = nested_list_of_enums
99+
if a_not_required_date is not UNSET:
100+
field_dict["a_not_required_date"] = a_not_required_date
94101
if attr_1_leading_digit is not UNSET:
95102
field_dict["1_leading_digit"] = attr_1_leading_digit
96103
if not_required_nullable is not UNSET:
@@ -145,7 +152,12 @@ def _parse_a_camel_date_time(data: Any) -> Union[datetime.datetime, datetime.dat
145152
a_nullable_date = None
146153
_a_nullable_date = d.pop("a_nullable_date")
147154
if _a_nullable_date is not None:
148-
a_nullable_date = isoparse(cast(str, _a_nullable_date)).date()
155+
a_nullable_date = isoparse(_a_nullable_date).date()
156+
157+
a_not_required_date: Union[Unset, datetime.date] = UNSET
158+
_a_not_required_date = d.pop("a_not_required_date", UNSET)
159+
if not isinstance(_a_not_required_date, Unset):
160+
a_not_required_date = isoparse(_a_not_required_date).date()
149161

150162
attr_1_leading_digit = d.pop("1_leading_digit", UNSET)
151163

@@ -158,19 +170,17 @@ def _parse_a_camel_date_time(data: Any) -> Union[datetime.datetime, datetime.dat
158170
nullable_model = None
159171
_nullable_model = d.pop("nullable_model")
160172
if _nullable_model is not None:
161-
nullable_model = AModelNullableModel.from_dict(cast(Dict[str, Any], _nullable_model))
173+
nullable_model = AModelNullableModel.from_dict(_nullable_model)
162174

163175
not_required_model: Union[AModelNotRequiredModel, Unset] = UNSET
164176
_not_required_model = d.pop("not_required_model", UNSET)
165177
if not isinstance(_not_required_model, Unset):
166-
not_required_model = AModelNotRequiredModel.from_dict(cast(Dict[str, Any], _not_required_model))
178+
not_required_model = AModelNotRequiredModel.from_dict(_not_required_model)
167179

168180
not_required_nullable_model = None
169181
_not_required_nullable_model = d.pop("not_required_nullable_model", UNSET)
170182
if _not_required_nullable_model is not None and not isinstance(_not_required_nullable_model, Unset):
171-
not_required_nullable_model = AModelNotRequiredNullableModel.from_dict(
172-
cast(Dict[str, Any], _not_required_nullable_model)
173-
)
183+
not_required_nullable_model = AModelNotRequiredNullableModel.from_dict(_not_required_nullable_model)
174184

175185
a_model = cls(
176186
an_enum_value=an_enum_value,
@@ -180,6 +190,7 @@ def _parse_a_camel_date_time(data: Any) -> Union[datetime.datetime, datetime.dat
180190
model=model,
181191
nested_list_of_enums=nested_list_of_enums,
182192
a_nullable_date=a_nullable_date,
193+
a_not_required_date=a_not_required_date,
183194
attr_1_leading_digit=attr_1_leading_digit,
184195
required_nullable=required_nullable,
185196
not_required_nullable=not_required_nullable,

end_to_end_tests/golden-record-custom/custom_e2e/models/model_with_primitive_additional_properties.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Dict, List, Type, TypeVar, Union, cast
1+
from typing import Any, Dict, List, Type, TypeVar, Union
22

33
import attr
44

@@ -36,9 +36,7 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
3636
a_date_holder: Union[ModelWithPrimitiveAdditionalPropertiesADateHolder, Unset] = UNSET
3737
_a_date_holder = d.pop("a_date_holder", UNSET)
3838
if not isinstance(_a_date_holder, Unset):
39-
a_date_holder = ModelWithPrimitiveAdditionalPropertiesADateHolder.from_dict(
40-
cast(Dict[str, Any], _a_date_holder)
41-
)
39+
a_date_holder = ModelWithPrimitiveAdditionalPropertiesADateHolder.from_dict(_a_date_holder)
4240

4341
model_with_primitive_additional_properties = cls(
4442
a_date_holder=a_date_holder,

end_to_end_tests/golden-record-custom/custom_e2e/models/model_with_union_property.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,15 @@ def _parse_a_property(data: Any) -> Union[Unset, AnEnum, AnIntEnum]:
4646
try:
4747
a_property = UNSET
4848
_a_property = data
49-
if _a_property is not None and _a_property is not UNSET:
49+
if not isinstance(_a_property, Unset):
5050
a_property = AnEnum(_a_property)
5151

5252
return a_property
5353
except: # noqa: E722
5454
pass
5555
a_property = UNSET
5656
_a_property = data
57-
if _a_property is not None and _a_property is not UNSET:
57+
if not isinstance(_a_property, Unset):
5858
a_property = AnIntEnum(_a_property)
5959

6060
return a_property

end_to_end_tests/golden-record/my_test_api_client/models/a_model.py

+18-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import datetime
2-
from typing import Any, Dict, List, Optional, Type, TypeVar, Union, cast
2+
from typing import Any, Dict, List, Optional, Type, TypeVar, Union
33

44
import attr
55
from dateutil.parser import isoparse
@@ -28,6 +28,7 @@ class AModel:
2828
required_nullable: Optional[str]
2929
nullable_model: Optional[AModelNullableModel]
3030
nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET
31+
a_not_required_date: Union[Unset, datetime.date] = UNSET
3132
attr_1_leading_digit: Union[Unset, str] = UNSET
3233
not_required_nullable: Union[Unset, Optional[str]] = UNSET
3334
not_required_not_nullable: Union[Unset, str] = UNSET
@@ -60,6 +61,10 @@ def to_dict(self) -> Dict[str, Any]:
6061
nested_list_of_enums.append(nested_list_of_enums_item)
6162

6263
a_nullable_date = self.a_nullable_date.isoformat() if self.a_nullable_date else None
64+
a_not_required_date: Union[Unset, str] = UNSET
65+
if not isinstance(self.a_not_required_date, Unset):
66+
a_not_required_date = self.a_not_required_date.isoformat()
67+
6368
attr_1_leading_digit = self.attr_1_leading_digit
6469
required_nullable = self.required_nullable
6570
not_required_nullable = self.not_required_nullable
@@ -91,6 +96,8 @@ def to_dict(self) -> Dict[str, Any]:
9196
)
9297
if nested_list_of_enums is not UNSET:
9398
field_dict["nested_list_of_enums"] = nested_list_of_enums
99+
if a_not_required_date is not UNSET:
100+
field_dict["a_not_required_date"] = a_not_required_date
94101
if attr_1_leading_digit is not UNSET:
95102
field_dict["1_leading_digit"] = attr_1_leading_digit
96103
if not_required_nullable is not UNSET:
@@ -145,7 +152,12 @@ def _parse_a_camel_date_time(data: Any) -> Union[datetime.datetime, datetime.dat
145152
a_nullable_date = None
146153
_a_nullable_date = d.pop("a_nullable_date")
147154
if _a_nullable_date is not None:
148-
a_nullable_date = isoparse(cast(str, _a_nullable_date)).date()
155+
a_nullable_date = isoparse(_a_nullable_date).date()
156+
157+
a_not_required_date: Union[Unset, datetime.date] = UNSET
158+
_a_not_required_date = d.pop("a_not_required_date", UNSET)
159+
if not isinstance(_a_not_required_date, Unset):
160+
a_not_required_date = isoparse(_a_not_required_date).date()
149161

150162
attr_1_leading_digit = d.pop("1_leading_digit", UNSET)
151163

@@ -158,19 +170,17 @@ def _parse_a_camel_date_time(data: Any) -> Union[datetime.datetime, datetime.dat
158170
nullable_model = None
159171
_nullable_model = d.pop("nullable_model")
160172
if _nullable_model is not None:
161-
nullable_model = AModelNullableModel.from_dict(cast(Dict[str, Any], _nullable_model))
173+
nullable_model = AModelNullableModel.from_dict(_nullable_model)
162174

163175
not_required_model: Union[AModelNotRequiredModel, Unset] = UNSET
164176
_not_required_model = d.pop("not_required_model", UNSET)
165177
if not isinstance(_not_required_model, Unset):
166-
not_required_model = AModelNotRequiredModel.from_dict(cast(Dict[str, Any], _not_required_model))
178+
not_required_model = AModelNotRequiredModel.from_dict(_not_required_model)
167179

168180
not_required_nullable_model = None
169181
_not_required_nullable_model = d.pop("not_required_nullable_model", UNSET)
170182
if _not_required_nullable_model is not None and not isinstance(_not_required_nullable_model, Unset):
171-
not_required_nullable_model = AModelNotRequiredNullableModel.from_dict(
172-
cast(Dict[str, Any], _not_required_nullable_model)
173-
)
183+
not_required_nullable_model = AModelNotRequiredNullableModel.from_dict(_not_required_nullable_model)
174184

175185
a_model = cls(
176186
an_enum_value=an_enum_value,
@@ -180,6 +190,7 @@ def _parse_a_camel_date_time(data: Any) -> Union[datetime.datetime, datetime.dat
180190
model=model,
181191
nested_list_of_enums=nested_list_of_enums,
182192
a_nullable_date=a_nullable_date,
193+
a_not_required_date=a_not_required_date,
183194
attr_1_leading_digit=attr_1_leading_digit,
184195
required_nullable=required_nullable,
185196
not_required_nullable=not_required_nullable,

end_to_end_tests/golden-record/my_test_api_client/models/model_with_primitive_additional_properties.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Dict, List, Type, TypeVar, Union, cast
1+
from typing import Any, Dict, List, Type, TypeVar, Union
22

33
import attr
44

@@ -36,9 +36,7 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
3636
a_date_holder: Union[ModelWithPrimitiveAdditionalPropertiesADateHolder, Unset] = UNSET
3737
_a_date_holder = d.pop("a_date_holder", UNSET)
3838
if not isinstance(_a_date_holder, Unset):
39-
a_date_holder = ModelWithPrimitiveAdditionalPropertiesADateHolder.from_dict(
40-
cast(Dict[str, Any], _a_date_holder)
41-
)
39+
a_date_holder = ModelWithPrimitiveAdditionalPropertiesADateHolder.from_dict(_a_date_holder)
4240

4341
model_with_primitive_additional_properties = cls(
4442
a_date_holder=a_date_holder,

end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,15 @@ def _parse_a_property(data: Any) -> Union[Unset, AnEnum, AnIntEnum]:
4646
try:
4747
a_property = UNSET
4848
_a_property = data
49-
if _a_property is not None and _a_property is not UNSET:
49+
if not isinstance(_a_property, Unset):
5050
a_property = AnEnum(_a_property)
5151

5252
return a_property
5353
except: # noqa: E722
5454
pass
5555
a_property = UNSET
5656
_a_property = data
57-
if _a_property is not None and _a_property is not UNSET:
57+
if not isinstance(_a_property, Unset):
5858
a_property = AnIntEnum(_a_property)
5959

6060
return a_property

end_to_end_tests/openapi.json

+5
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,11 @@
663663
"format": "date",
664664
"nullable": true
665665
},
666+
"a_not_required_date": {
667+
"title": "A Nullable Date",
668+
"type": "string",
669+
"format": "date",
670+
},
666671
"1_leading_digit": {
667672
"title": "Leading Digit",
668673
"type": "string"

openapi_python_client/templates/property_templates/date_property.py.jinja

+8-9
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
{% macro construct(property, source, initial_value="None") %}
2-
{% if property.required and not property.nullable %}
3-
{{ property.python_name }} = isoparse({{ source }}).date()
4-
{% else %}
5-
{{ property.python_name }} = {{ initial_value }}
6-
_{{ property.python_name }} = {{ source }}
7-
if _{{ property.python_name }} is not None:
8-
{{ property.python_name }} = isoparse(cast(str, _{{ property.python_name }})).date()
9-
{% endif %}
1+
{% macro construct_function(property, source) %}
2+
isoparse({{ source }}).date()
3+
{% endmacro %}
4+
5+
{% from "property_templates/property_macros.py.jinja" import construct_template %}
6+
7+
{% macro construct(property, source, initial_value=None) %}
8+
{{ construct_template(construct_function, property, source, initial_value=initial_value) }}
109
{% endmacro %}
1110

1211
{% macro transform(property, source, destination, declare_type=True) %}

openapi_python_client/templates/property_templates/datetime_property.py.jinja

+8-14
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
1-
{% macro construct(property, source, initial_value="None") %}
2-
{% if property.required %}
3-
{% if property.nullable %}
4-
{{ property.python_name }} = {{ source }}
5-
{{ property.python_name }} = isoparse({{ property.python_name }}) if {{ property.python_name }} else None
6-
{% else %}
7-
{{ property.python_name }} = isoparse({{ source }})
8-
{% endif %}
9-
{% else %}
10-
{{ property.python_name }} = {{ initial_value }}
11-
_{{ property.python_name }} = {{ source }}
12-
if _{{ property.python_name }} is not None:
13-
{{ property.python_name }} = isoparse(cast(str, _{{ property.python_name }}))
14-
{% endif %}
1+
{% macro construct_function(property, source) %}
2+
isoparse({{ source }})
3+
{% endmacro %}
4+
5+
{% from "property_templates/property_macros.py.jinja" import construct_template %}
6+
7+
{% macro construct(property, source, initial_value=None) %}
8+
{{ construct_template(construct_function, property, source, initial_value=initial_value) }}
159
{% endmacro %}
1610

1711
{% macro transform(property, source, destination, declare_type=True) %}

openapi_python_client/templates/property_templates/enum_property.py.jinja

+8-9
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
{% macro construct(property, source, initial_value="None") %}
2-
{% if property.required %}
3-
{{ property.python_name }} = {{ property.reference.class_name }}({{ source }})
4-
{% else %}
5-
{{ property.python_name }} = {{ initial_value }}
6-
_{{ property.python_name }} = {{ source }}
7-
if _{{ property.python_name }} is not None and _{{ property.python_name }} is not UNSET:
8-
{{ property.python_name }} = {{ property.reference.class_name }}(_{{ property.python_name }})
9-
{% endif %}
1+
{% macro construct_function(property, source) %}
2+
{{ property.reference.class_name }}({{ source }})
3+
{% endmacro %}
4+
5+
{% from "property_templates/property_macros.py.jinja" import construct_template %}
6+
7+
{% macro construct(property, source, initial_value=None) %}
8+
{{ construct_template(construct_function, property, source, initial_value=initial_value) }}
109
{% endmacro %}
1110

1211
{% macro transform(property, source, destination, declare_type=True) %}

openapi_python_client/templates/property_templates/file_property.py.jinja

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1-
{% macro construct(property, source, initial_value=None) %}
2-
{{ property.python_name }} = File(
1+
{% macro construct_function(property, source) %}
2+
File(
33
payload = BytesIO({{ source }})
44
)
55
{% endmacro %}
66

7+
{% from "property_templates/property_macros.py.jinja" import construct_template %}
8+
9+
{% macro construct(property, source, initial_value=None) %}
10+
{{ construct_template(construct_function, property, source, initial_value=initial_value) }}
11+
{% endmacro %}
12+
713
{% macro transform(property, source, destination, declare_type=True) %}
814
{% if property.required %}
915
{% if property.nullable %}

openapi_python_client/templates/property_templates/list_property.py.jinja

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
{% set inner_source = inner_property.python_name + "_data" %}
55
{{ property.python_name }} = {{ initial_value }}
66
_{{ property.python_name }} = {{ source }}
7-
{% if property.required %}
7+
{% if property.required and not property.nullable %}
88
for {{ inner_source }} in (_{{ property.python_name }}):
99
{% else %}
1010
for {{ inner_source }} in (_{{ property.python_name }} or []):

openapi_python_client/templates/property_templates/model_property.py.jinja

+7-14
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,11 @@
1+
{% macro construct_function(property, source) %}
2+
{{ property.reference.class_name }}.from_dict({{ source }})
3+
{% endmacro %}
4+
5+
{% from "property_templates/property_macros.py.jinja" import construct_template %}
6+
17
{% macro construct(property, source, initial_value=None) %}
2-
{% if property.required and not property.nullable %}
3-
{{ property.python_name }} = {{ property.reference.class_name }}.from_dict({{ source }})
4-
{% else %}
5-
{% if initial_value != None %}
6-
{{ property.python_name }} = {{ initial_value }}
7-
{% elif property.nullable %}
8-
{{ property.python_name }} = None
9-
{% else %}
10-
{{ property.python_name }}: {{ property.get_type_string() }} = UNSET
11-
{% endif %}
12-
_{{ property.python_name }} = {{source}}
13-
if {% if property.nullable %}_{{ property.python_name }} is not None{% endif %}{% if property.nullable and not property.required %} and {% endif %}{% if not property.required %}not isinstance(_{{ property.python_name }}, Unset){% endif %}:
14-
{{ property.python_name }} = {{ property.reference.class_name }}.from_dict(cast(Dict[str, Any], _{{ property.python_name }}))
15-
{% endif %}
8+
{{ construct_template(construct_function, property, source, initial_value=initial_value) }}
169
{% endmacro %}
1710

1811
{% macro transform(property, source, destination, declare_type=True) %}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{% macro construct_template(construct_function, property, source, initial_value=None) %}
2+
{% if property.required and not property.nullable %}
3+
{{ property.python_name }} = {{ construct_function(property, source) }}
4+
{% else %}
5+
{% if initial_value != None %}
6+
{{ property.python_name }} = {{ initial_value }}
7+
{% elif property.nullable %}
8+
{{ property.python_name }} = None
9+
{% else %}
10+
{{ property.python_name }}: {{ property.get_type_string() }} = UNSET
11+
{% endif %}
12+
_{{ property.python_name }} = {{ source }}
13+
if {% if property.nullable %}_{{ property.python_name }} is not None{% endif %}{% if property.nullable and not property.required %} and {% endif %}{% if not property.required %}not isinstance(_{{ property.python_name }}, Unset){% endif %}:
14+
{{ property.python_name }} = {{ construct_function(property, "_" + property.python_name) }}
15+
{% endif %}
16+
{% endmacro %}

0 commit comments

Comments
 (0)