From 9203aff4b02f30c96f6398490a219efeb6fa56c4 Mon Sep 17 00:00:00 2001 From: Simon Guigui Date: Tue, 15 Dec 2020 11:41:04 +0100 Subject: [PATCH 1/3] Add a nullable date property --- end_to_end_tests/openapi.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/end_to_end_tests/openapi.json b/end_to_end_tests/openapi.json index 1cbfd9597..05009e355 100644 --- a/end_to_end_tests/openapi.json +++ b/end_to_end_tests/openapi.json @@ -622,7 +622,7 @@ "schemas": { "AModel": { "title": "AModel", - "required": ["an_enum_value", "aCamelDateTime", "a_date", "required_nullable", "required_not_nullable"], + "required": ["an_enum_value", "aCamelDateTime", "a_date", "a_nullable_date", "required_nullable", "required_not_nullable"], "type": "object", "properties": { "an_enum_value": { @@ -657,6 +657,12 @@ "type": "string", "format": "date" }, + "a_nullable_date": { + "title": "A Nullable Date", + "type": "string", + "format": "date", + "nullable": true + }, "1_leading_digit": { "title": "Leading Digit", "type": "string" From 48a2ac636e6e7fc983f78f9bad8c02b13c31db51 Mon Sep 17 00:00:00 2001 From: Simon Guigui Date: Tue, 15 Dec 2020 12:01:24 +0100 Subject: [PATCH 2/3] Bug fix for nullable date/datetime properties --- .../golden-record-custom/custom_e2e/models/a_model.py | 8 ++++++++ .../golden-record/my_test_api_client/models/a_model.py | 8 ++++++++ .../templates/property_templates/date_property.pyi | 5 +++++ .../templates/property_templates/datetime_property.pyi | 5 +++++ 4 files changed, 26 insertions(+) diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/models/a_model.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/a_model.py index fc857027a..74a31a394 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/models/a_model.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/models/a_model.py @@ -17,6 +17,7 @@ class AModel: a_camel_date_time: Union[datetime.datetime, datetime.date] a_date: datetime.date required_not_nullable: str + a_nullable_date: Optional[datetime.date] required_nullable: Optional[str] nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET attr_1_leading_digit: Union[Unset, str] = UNSET @@ -47,6 +48,8 @@ def to_dict(self) -> Dict[str, Any]: nested_list_of_enums.append(nested_list_of_enums_item) + a_nullable_date = self.a_nullable_date.isoformat() if self.a_nullable_date else None + attr_1_leading_digit = self.attr_1_leading_digit required_nullable = self.required_nullable not_required_nullable = self.not_required_nullable @@ -59,6 +62,7 @@ def to_dict(self) -> Dict[str, Any]: "aCamelDateTime": a_camel_date_time, "a_date": a_date, "required_not_nullable": required_not_nullable, + "a_nullable_date": a_nullable_date, "required_nullable": required_nullable, } ) @@ -109,6 +113,9 @@ def _parse_a_camel_date_time(data: Any) -> Union[datetime.datetime, datetime.dat nested_list_of_enums.append(nested_list_of_enums_item) + a_nullable_date = d.pop("a_nullable_date") + a_nullable_date = isoparse(a_nullable_date).date() if a_nullable_date else None + attr_1_leading_digit = d.pop("1_leading_digit", UNSET) required_nullable = d.pop("required_nullable") @@ -123,6 +130,7 @@ def _parse_a_camel_date_time(data: Any) -> Union[datetime.datetime, datetime.dat a_date=a_date, required_not_nullable=required_not_nullable, nested_list_of_enums=nested_list_of_enums, + a_nullable_date=a_nullable_date, attr_1_leading_digit=attr_1_leading_digit, required_nullable=required_nullable, not_required_nullable=not_required_nullable, diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py index fc857027a..74a31a394 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py @@ -17,6 +17,7 @@ class AModel: a_camel_date_time: Union[datetime.datetime, datetime.date] a_date: datetime.date required_not_nullable: str + a_nullable_date: Optional[datetime.date] required_nullable: Optional[str] nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET attr_1_leading_digit: Union[Unset, str] = UNSET @@ -47,6 +48,8 @@ def to_dict(self) -> Dict[str, Any]: nested_list_of_enums.append(nested_list_of_enums_item) + a_nullable_date = self.a_nullable_date.isoformat() if self.a_nullable_date else None + attr_1_leading_digit = self.attr_1_leading_digit required_nullable = self.required_nullable not_required_nullable = self.not_required_nullable @@ -59,6 +62,7 @@ def to_dict(self) -> Dict[str, Any]: "aCamelDateTime": a_camel_date_time, "a_date": a_date, "required_not_nullable": required_not_nullable, + "a_nullable_date": a_nullable_date, "required_nullable": required_nullable, } ) @@ -109,6 +113,9 @@ def _parse_a_camel_date_time(data: Any) -> Union[datetime.datetime, datetime.dat nested_list_of_enums.append(nested_list_of_enums_item) + a_nullable_date = d.pop("a_nullable_date") + a_nullable_date = isoparse(a_nullable_date).date() if a_nullable_date else None + attr_1_leading_digit = d.pop("1_leading_digit", UNSET) required_nullable = d.pop("required_nullable") @@ -123,6 +130,7 @@ def _parse_a_camel_date_time(data: Any) -> Union[datetime.datetime, datetime.dat a_date=a_date, required_not_nullable=required_not_nullable, nested_list_of_enums=nested_list_of_enums, + a_nullable_date=a_nullable_date, attr_1_leading_digit=attr_1_leading_digit, required_nullable=required_nullable, not_required_nullable=not_required_nullable, diff --git a/openapi_python_client/templates/property_templates/date_property.pyi b/openapi_python_client/templates/property_templates/date_property.pyi index 22624c7e5..9bec811fa 100644 --- a/openapi_python_client/templates/property_templates/date_property.pyi +++ b/openapi_python_client/templates/property_templates/date_property.pyi @@ -1,6 +1,11 @@ {% macro construct(property, source, initial_value="None") %} {% if property.required %} +{% if property.nullable %} +{{ property.python_name }} = {{ source }} +{{ property.python_name }} = isoparse({{ property.python_name }}).date() if {{ property.python_name }} else None +{% else %} {{ property.python_name }} = isoparse({{ source }}).date() +{% endif %} {% else %} {{ property.python_name }} = {{ initial_value }} _{{ property.python_name }} = {{ source }} diff --git a/openapi_python_client/templates/property_templates/datetime_property.pyi b/openapi_python_client/templates/property_templates/datetime_property.pyi index 353978fa0..b8e1b8ff0 100644 --- a/openapi_python_client/templates/property_templates/datetime_property.pyi +++ b/openapi_python_client/templates/property_templates/datetime_property.pyi @@ -1,6 +1,11 @@ {% macro construct(property, source, initial_value="None") %} {% if property.required %} +{% if property.nullable %} +{{ property.python_name }} = {{ source }} +{{ property.python_name }} = isoparse({{ property.python_name }}) if {{ property.python_name }} else None +{% else %} {{ property.python_name }} = isoparse({{ source }}) +{% endif %} {% else %} {{ property.python_name }} = {{ initial_value }} _{{ property.python_name }} = {{ source }} From 847696e3f07d47f4d8ccc922aa3a771901d1621b Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Mon, 21 Dec 2020 14:33:47 -0700 Subject: [PATCH 3/3] Based off of #267 with suggestions applied. --- CHANGELOG.md | 1 + .../custom_e2e/api/tests/get_user_list.py | 1 - .../custom_e2e/models/a_model.py | 10 +-- .../api/tests/get_user_list.py | 1 - .../my_test_api_client/models/a_model.py | 10 +-- .../property_templates/date_property.pyi | 13 +--- .../date_property_template.py | 8 +++ .../test_date_property/optional_nullable.py | 22 ++++++ .../test_date_property/required_not_null.py | 15 ++++ .../test_date_property/required_nullable.py | 18 +++++ .../test_date_property/test_date_property.py | 69 +++++++++++++++++++ 11 files changed, 145 insertions(+), 23 deletions(-) create mode 100644 tests/test_templates/test_property_templates/test_date_property/date_property_template.py create mode 100644 tests/test_templates/test_property_templates/test_date_property/optional_nullable.py create mode 100644 tests/test_templates/test_property_templates/test_date_property/required_not_null.py create mode 100644 tests/test_templates/test_property_templates/test_date_property/required_nullable.py create mode 100644 tests/test_templates/test_property_templates/test_date_property/test_date_property.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a04b0519..c068085c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixes - Spacing and extra returns for Union types of `additionalProperties` (#266 & #268). Thanks @joshzana & @packyg! - Title of inline schemas will no longer be missing characters (#271 & #274). Thanks @kalzoo! +- Handling of nulls (Nones) when parsing or constructing dates (#267). Thanks @fyhertz! ## 0.7.2 - 2020-12-08 ### Fixes diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_user_list.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_user_list.py index bc3909126..72758fd75 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_user_list.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_user_list.py @@ -55,7 +55,6 @@ def httpx_request( if isinstance(some_date, datetime.date): json_some_date = some_date.isoformat() - else: json_some_date = some_date.isoformat() diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/models/a_model.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/a_model.py index 74a31a394..2bf3e140d 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/models/a_model.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/models/a_model.py @@ -1,5 +1,5 @@ import datetime -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Union, cast import attr from dateutil.parser import isoparse @@ -34,7 +34,6 @@ def to_dict(self) -> Dict[str, Any]: a_camel_date_time = self.a_camel_date_time.isoformat() a_date = self.a_date.isoformat() - required_not_nullable = self.required_not_nullable nested_list_of_enums: Union[Unset, List[Any]] = UNSET if not isinstance(self.nested_list_of_enums, Unset): @@ -49,7 +48,6 @@ def to_dict(self) -> Dict[str, Any]: nested_list_of_enums.append(nested_list_of_enums_item) a_nullable_date = self.a_nullable_date.isoformat() if self.a_nullable_date else None - attr_1_leading_digit = self.attr_1_leading_digit required_nullable = self.required_nullable not_required_nullable = self.not_required_nullable @@ -113,8 +111,10 @@ def _parse_a_camel_date_time(data: Any) -> Union[datetime.datetime, datetime.dat nested_list_of_enums.append(nested_list_of_enums_item) - a_nullable_date = d.pop("a_nullable_date") - a_nullable_date = isoparse(a_nullable_date).date() if a_nullable_date else None + a_nullable_date = None + _a_nullable_date = d.pop("a_nullable_date") + if _a_nullable_date is not None: + a_nullable_date = isoparse(cast(str, _a_nullable_date)).date() attr_1_leading_digit = d.pop("1_leading_digit", UNSET) diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py index f8d0062a7..e43c92d9b 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py @@ -28,7 +28,6 @@ def _get_kwargs( if isinstance(some_date, datetime.date): json_some_date = some_date.isoformat() - else: json_some_date = some_date.isoformat() diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py index 74a31a394..2bf3e140d 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py @@ -1,5 +1,5 @@ import datetime -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Union, cast import attr from dateutil.parser import isoparse @@ -34,7 +34,6 @@ def to_dict(self) -> Dict[str, Any]: a_camel_date_time = self.a_camel_date_time.isoformat() a_date = self.a_date.isoformat() - required_not_nullable = self.required_not_nullable nested_list_of_enums: Union[Unset, List[Any]] = UNSET if not isinstance(self.nested_list_of_enums, Unset): @@ -49,7 +48,6 @@ def to_dict(self) -> Dict[str, Any]: nested_list_of_enums.append(nested_list_of_enums_item) a_nullable_date = self.a_nullable_date.isoformat() if self.a_nullable_date else None - attr_1_leading_digit = self.attr_1_leading_digit required_nullable = self.required_nullable not_required_nullable = self.not_required_nullable @@ -113,8 +111,10 @@ def _parse_a_camel_date_time(data: Any) -> Union[datetime.datetime, datetime.dat nested_list_of_enums.append(nested_list_of_enums_item) - a_nullable_date = d.pop("a_nullable_date") - a_nullable_date = isoparse(a_nullable_date).date() if a_nullable_date else None + a_nullable_date = None + _a_nullable_date = d.pop("a_nullable_date") + if _a_nullable_date is not None: + a_nullable_date = isoparse(cast(str, _a_nullable_date)).date() attr_1_leading_digit = d.pop("1_leading_digit", UNSET) diff --git a/openapi_python_client/templates/property_templates/date_property.pyi b/openapi_python_client/templates/property_templates/date_property.pyi index 9bec811fa..a3a980c8f 100644 --- a/openapi_python_client/templates/property_templates/date_property.pyi +++ b/openapi_python_client/templates/property_templates/date_property.pyi @@ -1,11 +1,6 @@ {% macro construct(property, source, initial_value="None") %} -{% if property.required %} -{% if property.nullable %} -{{ property.python_name }} = {{ source }} -{{ property.python_name }} = isoparse({{ property.python_name }}).date() if {{ property.python_name }} else None -{% else %} +{% if property.required and not property.nullable %} {{ property.python_name }} = isoparse({{ source }}).date() -{% endif %} {% else %} {{ property.python_name }} = {{ initial_value }} _{{ property.python_name }} = {{ source }} @@ -16,11 +11,7 @@ if _{{ property.python_name }} is not None: {% macro transform(property, source, destination, declare_type=True) %} {% if property.required %} -{% if property.nullable %} -{{ destination }} = {{ source }}.isoformat() if {{ source }} else None -{% else %} -{{ destination }} = {{ source }}.isoformat() -{% endif %} +{{ destination }} = {{ source }}.isoformat() {% if property.nullable %}if {{ source }} else None {%endif%} {% else %} {{ destination }}{% if declare_type %}: Union[Unset, str]{% endif %} = UNSET if not isinstance({{ source }}, Unset): diff --git a/tests/test_templates/test_property_templates/test_date_property/date_property_template.py b/tests/test_templates/test_property_templates/test_date_property/date_property_template.py new file mode 100644 index 000000000..9393b28ed --- /dev/null +++ b/tests/test_templates/test_property_templates/test_date_property/date_property_template.py @@ -0,0 +1,8 @@ +from datetime import date +from typing import cast, Union + +from dateutil.parser import isoparse +{% from "property_templates/date_property.pyi" import transform, construct %} +some_source = date(2020, 10, 12) +{{ transform(property, "some_source", "some_destination") }} +{{ construct(property, "some_destination") }} diff --git a/tests/test_templates/test_property_templates/test_date_property/optional_nullable.py b/tests/test_templates/test_property_templates/test_date_property/optional_nullable.py new file mode 100644 index 000000000..cf8780024 --- /dev/null +++ b/tests/test_templates/test_property_templates/test_date_property/optional_nullable.py @@ -0,0 +1,22 @@ +from datetime import date +from typing import cast, Union + +from dateutil.parser import isoparse + +some_source = date(2020, 10, 12) + + +some_destination: Union[Unset, str] = UNSET +if not isinstance(some_source, Unset): + + some_destination = some_source.isoformat() if some_source else None + + + + + +a_prop = None +_a_prop = some_destination +if _a_prop is not None: + a_prop = isoparse(cast(str, _a_prop)).date() + diff --git a/tests/test_templates/test_property_templates/test_date_property/required_not_null.py b/tests/test_templates/test_property_templates/test_date_property/required_not_null.py new file mode 100644 index 000000000..79620bce9 --- /dev/null +++ b/tests/test_templates/test_property_templates/test_date_property/required_not_null.py @@ -0,0 +1,15 @@ +from datetime import date +from typing import cast, Union + +from dateutil.parser import isoparse + +some_source = date(2020, 10, 12) + + +some_destination = some_source.isoformat() + + + + +a_prop = isoparse(some_destination).date() + diff --git a/tests/test_templates/test_property_templates/test_date_property/required_nullable.py b/tests/test_templates/test_property_templates/test_date_property/required_nullable.py new file mode 100644 index 000000000..b6ef423b8 --- /dev/null +++ b/tests/test_templates/test_property_templates/test_date_property/required_nullable.py @@ -0,0 +1,18 @@ +from datetime import date +from typing import cast, Union + +from dateutil.parser import isoparse + +some_source = date(2020, 10, 12) + + +some_destination = some_source.isoformat() if some_source else None + + + + +a_prop = None +_a_prop = some_destination +if _a_prop is not None: + a_prop = isoparse(cast(str, _a_prop)).date() + diff --git a/tests/test_templates/test_property_templates/test_date_property/test_date_property.py b/tests/test_templates/test_property_templates/test_date_property/test_date_property.py new file mode 100644 index 000000000..3a8ad435f --- /dev/null +++ b/tests/test_templates/test_property_templates/test_date_property/test_date_property.py @@ -0,0 +1,69 @@ +from pathlib import Path + +import jinja2 + + +def test_required_not_nullable(): + from openapi_python_client.parser.properties import DateProperty + + prop = DateProperty( + name="a_prop", + required=True, + nullable=False, + default=None, + ) + here = Path(__file__).parent + templates_dir = here.parent.parent.parent.parent / "openapi_python_client" / "templates" + + env = jinja2.Environment( + loader=jinja2.ChoiceLoader([jinja2.FileSystemLoader(here), jinja2.FileSystemLoader(templates_dir)]) + ) + + template = env.get_template("date_property_template.py") + content = template.render(property=prop) + expected = here / "required_not_null.py" + assert content == expected.read_text() + + +def test_required_nullable(): + from openapi_python_client.parser.properties import DateProperty + + prop = DateProperty( + name="a_prop", + required=True, + nullable=True, + default=None, + ) + here = Path(__file__).parent + templates_dir = here.parent.parent.parent.parent / "openapi_python_client" / "templates" + + env = jinja2.Environment( + loader=jinja2.ChoiceLoader([jinja2.FileSystemLoader(here), jinja2.FileSystemLoader(templates_dir)]) + ) + + template = env.get_template("date_property_template.py") + content = template.render(property=prop) + expected = here / "required_nullable.py" + assert content == expected.read_text() + + +def test_optional_nullable(): + from openapi_python_client.parser.properties import DateProperty + + prop = DateProperty( + name="a_prop", + required=False, + nullable=True, + default=None, + ) + here = Path(__file__).parent + templates_dir = here.parent.parent.parent.parent / "openapi_python_client" / "templates" + + env = jinja2.Environment( + loader=jinja2.ChoiceLoader([jinja2.FileSystemLoader(here), jinja2.FileSystemLoader(templates_dir)]) + ) + + template = env.get_template("date_property_template.py") + content = template.render(property=prop) + expected = here / "optional_nullable.py" + assert content == expected.read_text()