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 fc857027a..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 @@ -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 @@ -33,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): @@ -47,6 +47,7 @@ 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 +60,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 +111,11 @@ 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 = 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) 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/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 fc857027a..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 @@ -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 @@ -33,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): @@ -47,6 +47,7 @@ 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 +60,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 +111,11 @@ 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 = 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) 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/openapi.json b/end_to_end_tests/openapi.json index 2a0c1a644..9e3d78908 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" diff --git a/openapi_python_client/templates/property_templates/date_property.pyi b/openapi_python_client/templates/property_templates/date_property.pyi index 22624c7e5..a3a980c8f 100644 --- a/openapi_python_client/templates/property_templates/date_property.pyi +++ b/openapi_python_client/templates/property_templates/date_property.pyi @@ -1,5 +1,5 @@ {% macro construct(property, source, initial_value="None") %} -{% if property.required %} +{% if property.required and not property.nullable %} {{ property.python_name }} = isoparse({{ source }}).date() {% else %} {{ property.python_name }} = {{ initial_value }} @@ -11,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/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 }} 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()