Skip to content

Commit d8a9a59

Browse files
dbantyfyhertz
andauthored
Fix nullable dates (#277)
* Bug fix for nullable date/datetime properties Co-authored-by: Simon Guigui <[email protected]>
1 parent ac49ce8 commit d8a9a59

File tree

13 files changed

+167
-13
lines changed

13 files changed

+167
-13
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
### Fixes
1010
- Spacing and extra returns for Union types of `additionalProperties` (#266 & #268). Thanks @joshzana & @packyg!
1111
- Title of inline schemas will no longer be missing characters (#271 & #274). Thanks @kalzoo!
12+
- Handling of nulls (Nones) when parsing or constructing dates (#267). Thanks @fyhertz!
1213

1314
## 0.7.2 - 2020-12-08
1415
### Fixes

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

-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ def httpx_request(
5555

5656
if isinstance(some_date, datetime.date):
5757
json_some_date = some_date.isoformat()
58-
5958
else:
6059
json_some_date = some_date.isoformat()
6160

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

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

44
import attr
55
from dateutil.parser import isoparse
@@ -17,6 +17,7 @@ class AModel:
1717
a_camel_date_time: Union[datetime.datetime, datetime.date]
1818
a_date: datetime.date
1919
required_not_nullable: str
20+
a_nullable_date: Optional[datetime.date]
2021
required_nullable: Optional[str]
2122
nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET
2223
attr_1_leading_digit: Union[Unset, str] = UNSET
@@ -33,7 +34,6 @@ def to_dict(self) -> Dict[str, Any]:
3334
a_camel_date_time = self.a_camel_date_time.isoformat()
3435

3536
a_date = self.a_date.isoformat()
36-
3737
required_not_nullable = self.required_not_nullable
3838
nested_list_of_enums: Union[Unset, List[Any]] = UNSET
3939
if not isinstance(self.nested_list_of_enums, Unset):
@@ -47,6 +47,7 @@ def to_dict(self) -> Dict[str, Any]:
4747

4848
nested_list_of_enums.append(nested_list_of_enums_item)
4949

50+
a_nullable_date = self.a_nullable_date.isoformat() if self.a_nullable_date else None
5051
attr_1_leading_digit = self.attr_1_leading_digit
5152
required_nullable = self.required_nullable
5253
not_required_nullable = self.not_required_nullable
@@ -59,6 +60,7 @@ def to_dict(self) -> Dict[str, Any]:
5960
"aCamelDateTime": a_camel_date_time,
6061
"a_date": a_date,
6162
"required_not_nullable": required_not_nullable,
63+
"a_nullable_date": a_nullable_date,
6264
"required_nullable": required_nullable,
6365
}
6466
)
@@ -109,6 +111,11 @@ def _parse_a_camel_date_time(data: Any) -> Union[datetime.datetime, datetime.dat
109111

110112
nested_list_of_enums.append(nested_list_of_enums_item)
111113

114+
a_nullable_date = None
115+
_a_nullable_date = d.pop("a_nullable_date")
116+
if _a_nullable_date is not None:
117+
a_nullable_date = isoparse(cast(str, _a_nullable_date)).date()
118+
112119
attr_1_leading_digit = d.pop("1_leading_digit", UNSET)
113120

114121
required_nullable = d.pop("required_nullable")
@@ -123,6 +130,7 @@ def _parse_a_camel_date_time(data: Any) -> Union[datetime.datetime, datetime.dat
123130
a_date=a_date,
124131
required_not_nullable=required_not_nullable,
125132
nested_list_of_enums=nested_list_of_enums,
133+
a_nullable_date=a_nullable_date,
126134
attr_1_leading_digit=attr_1_leading_digit,
127135
required_nullable=required_nullable,
128136
not_required_nullable=not_required_nullable,

end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py

-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ def _get_kwargs(
2828

2929
if isinstance(some_date, datetime.date):
3030
json_some_date = some_date.isoformat()
31-
3231
else:
3332
json_some_date = some_date.isoformat()
3433

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

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

44
import attr
55
from dateutil.parser import isoparse
@@ -17,6 +17,7 @@ class AModel:
1717
a_camel_date_time: Union[datetime.datetime, datetime.date]
1818
a_date: datetime.date
1919
required_not_nullable: str
20+
a_nullable_date: Optional[datetime.date]
2021
required_nullable: Optional[str]
2122
nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET
2223
attr_1_leading_digit: Union[Unset, str] = UNSET
@@ -33,7 +34,6 @@ def to_dict(self) -> Dict[str, Any]:
3334
a_camel_date_time = self.a_camel_date_time.isoformat()
3435

3536
a_date = self.a_date.isoformat()
36-
3737
required_not_nullable = self.required_not_nullable
3838
nested_list_of_enums: Union[Unset, List[Any]] = UNSET
3939
if not isinstance(self.nested_list_of_enums, Unset):
@@ -47,6 +47,7 @@ def to_dict(self) -> Dict[str, Any]:
4747

4848
nested_list_of_enums.append(nested_list_of_enums_item)
4949

50+
a_nullable_date = self.a_nullable_date.isoformat() if self.a_nullable_date else None
5051
attr_1_leading_digit = self.attr_1_leading_digit
5152
required_nullable = self.required_nullable
5253
not_required_nullable = self.not_required_nullable
@@ -59,6 +60,7 @@ def to_dict(self) -> Dict[str, Any]:
5960
"aCamelDateTime": a_camel_date_time,
6061
"a_date": a_date,
6162
"required_not_nullable": required_not_nullable,
63+
"a_nullable_date": a_nullable_date,
6264
"required_nullable": required_nullable,
6365
}
6466
)
@@ -109,6 +111,11 @@ def _parse_a_camel_date_time(data: Any) -> Union[datetime.datetime, datetime.dat
109111

110112
nested_list_of_enums.append(nested_list_of_enums_item)
111113

114+
a_nullable_date = None
115+
_a_nullable_date = d.pop("a_nullable_date")
116+
if _a_nullable_date is not None:
117+
a_nullable_date = isoparse(cast(str, _a_nullable_date)).date()
118+
112119
attr_1_leading_digit = d.pop("1_leading_digit", UNSET)
113120

114121
required_nullable = d.pop("required_nullable")
@@ -123,6 +130,7 @@ def _parse_a_camel_date_time(data: Any) -> Union[datetime.datetime, datetime.dat
123130
a_date=a_date,
124131
required_not_nullable=required_not_nullable,
125132
nested_list_of_enums=nested_list_of_enums,
133+
a_nullable_date=a_nullable_date,
126134
attr_1_leading_digit=attr_1_leading_digit,
127135
required_nullable=required_nullable,
128136
not_required_nullable=not_required_nullable,

end_to_end_tests/openapi.json

+7-1
Original file line numberDiff line numberDiff line change
@@ -622,7 +622,7 @@
622622
"schemas": {
623623
"AModel": {
624624
"title": "AModel",
625-
"required": ["an_enum_value", "aCamelDateTime", "a_date", "required_nullable", "required_not_nullable"],
625+
"required": ["an_enum_value", "aCamelDateTime", "a_date", "a_nullable_date", "required_nullable", "required_not_nullable"],
626626
"type": "object",
627627
"properties": {
628628
"an_enum_value": {
@@ -657,6 +657,12 @@
657657
"type": "string",
658658
"format": "date"
659659
},
660+
"a_nullable_date": {
661+
"title": "A Nullable Date",
662+
"type": "string",
663+
"format": "date",
664+
"nullable": true
665+
},
660666
"1_leading_digit": {
661667
"title": "Leading Digit",
662668
"type": "string"

openapi_python_client/templates/property_templates/date_property.pyi

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{% macro construct(property, source, initial_value="None") %}
2-
{% if property.required %}
2+
{% if property.required and not property.nullable %}
33
{{ property.python_name }} = isoparse({{ source }}).date()
44
{% else %}
55
{{ property.python_name }} = {{ initial_value }}
@@ -11,11 +11,7 @@ if _{{ property.python_name }} is not None:
1111

1212
{% macro transform(property, source, destination, declare_type=True) %}
1313
{% if property.required %}
14-
{% if property.nullable %}
15-
{{ destination }} = {{ source }}.isoformat() if {{ source }} else None
16-
{% else %}
17-
{{ destination }} = {{ source }}.isoformat()
18-
{% endif %}
14+
{{ destination }} = {{ source }}.isoformat() {% if property.nullable %}if {{ source }} else None {%endif%}
1915
{% else %}
2016
{{ destination }}{% if declare_type %}: Union[Unset, str]{% endif %} = UNSET
2117
if not isinstance({{ source }}, Unset):

openapi_python_client/templates/property_templates/datetime_property.pyi

+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
{% macro construct(property, source, initial_value="None") %}
22
{% 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 %}
37
{{ property.python_name }} = isoparse({{ source }})
8+
{% endif %}
49
{% else %}
510
{{ property.python_name }} = {{ initial_value }}
611
_{{ property.python_name }} = {{ source }}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from datetime import date
2+
from typing import cast, Union
3+
4+
from dateutil.parser import isoparse
5+
{% from "property_templates/date_property.pyi" import transform, construct %}
6+
some_source = date(2020, 10, 12)
7+
{{ transform(property, "some_source", "some_destination") }}
8+
{{ construct(property, "some_destination") }}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from datetime import date
2+
from typing import cast, Union
3+
4+
from dateutil.parser import isoparse
5+
6+
some_source = date(2020, 10, 12)
7+
8+
9+
some_destination: Union[Unset, str] = UNSET
10+
if not isinstance(some_source, Unset):
11+
12+
some_destination = some_source.isoformat() if some_source else None
13+
14+
15+
16+
17+
18+
a_prop = None
19+
_a_prop = some_destination
20+
if _a_prop is not None:
21+
a_prop = isoparse(cast(str, _a_prop)).date()
22+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from datetime import date
2+
from typing import cast, Union
3+
4+
from dateutil.parser import isoparse
5+
6+
some_source = date(2020, 10, 12)
7+
8+
9+
some_destination = some_source.isoformat()
10+
11+
12+
13+
14+
a_prop = isoparse(some_destination).date()
15+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from datetime import date
2+
from typing import cast, Union
3+
4+
from dateutil.parser import isoparse
5+
6+
some_source = date(2020, 10, 12)
7+
8+
9+
some_destination = some_source.isoformat() if some_source else None
10+
11+
12+
13+
14+
a_prop = None
15+
_a_prop = some_destination
16+
if _a_prop is not None:
17+
a_prop = isoparse(cast(str, _a_prop)).date()
18+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from pathlib import Path
2+
3+
import jinja2
4+
5+
6+
def test_required_not_nullable():
7+
from openapi_python_client.parser.properties import DateProperty
8+
9+
prop = DateProperty(
10+
name="a_prop",
11+
required=True,
12+
nullable=False,
13+
default=None,
14+
)
15+
here = Path(__file__).parent
16+
templates_dir = here.parent.parent.parent.parent / "openapi_python_client" / "templates"
17+
18+
env = jinja2.Environment(
19+
loader=jinja2.ChoiceLoader([jinja2.FileSystemLoader(here), jinja2.FileSystemLoader(templates_dir)])
20+
)
21+
22+
template = env.get_template("date_property_template.py")
23+
content = template.render(property=prop)
24+
expected = here / "required_not_null.py"
25+
assert content == expected.read_text()
26+
27+
28+
def test_required_nullable():
29+
from openapi_python_client.parser.properties import DateProperty
30+
31+
prop = DateProperty(
32+
name="a_prop",
33+
required=True,
34+
nullable=True,
35+
default=None,
36+
)
37+
here = Path(__file__).parent
38+
templates_dir = here.parent.parent.parent.parent / "openapi_python_client" / "templates"
39+
40+
env = jinja2.Environment(
41+
loader=jinja2.ChoiceLoader([jinja2.FileSystemLoader(here), jinja2.FileSystemLoader(templates_dir)])
42+
)
43+
44+
template = env.get_template("date_property_template.py")
45+
content = template.render(property=prop)
46+
expected = here / "required_nullable.py"
47+
assert content == expected.read_text()
48+
49+
50+
def test_optional_nullable():
51+
from openapi_python_client.parser.properties import DateProperty
52+
53+
prop = DateProperty(
54+
name="a_prop",
55+
required=False,
56+
nullable=True,
57+
default=None,
58+
)
59+
here = Path(__file__).parent
60+
templates_dir = here.parent.parent.parent.parent / "openapi_python_client" / "templates"
61+
62+
env = jinja2.Environment(
63+
loader=jinja2.ChoiceLoader([jinja2.FileSystemLoader(here), jinja2.FileSystemLoader(templates_dir)])
64+
)
65+
66+
template = env.get_template("date_property_template.py")
67+
content = template.render(property=prop)
68+
expected = here / "optional_nullable.py"
69+
assert content == expected.read_text()

0 commit comments

Comments
 (0)