From be09dad6e95ab78051bc82a4dd7a2e6bede4969c Mon Sep 17 00:00:00 2001 From: Ethan Mann Date: Mon, 2 Nov 2020 13:50:54 -0500 Subject: [PATCH 01/13] Added support for UNSET values and better differentiation between required and nullable. Model to_dict methods now have parameters to alter what fields are included/excluded --- openapi_python_client/parser/properties.py | 6 ++-- .../templates/endpoint_macros.pyi | 4 +-- openapi_python_client/templates/model.pyi | 28 +++++++++++++++++-- .../property_templates/date_property.pyi | 14 +++++++++- .../property_templates/datetime_property.pyi | 14 +++++++++- .../property_templates/dict_property.pyi | 6 ++-- .../property_templates/enum_property.pyi | 14 +++++++++- .../property_templates/file_property.pyi | 14 +++++++++- .../property_templates/list_property.pyi | 17 ++++++++++- .../property_templates/ref_property.pyi | 14 +++++++++- .../property_templates/union_property.pyi | 10 ++++++- openapi_python_client/templates/types.py | 5 +++- 12 files changed, 128 insertions(+), 18 deletions(-) diff --git a/openapi_python_client/parser/properties.py b/openapi_python_client/parser/properties.py index afc1711ec..69ab3e324 100644 --- a/openapi_python_client/parser/properties.py +++ b/openapi_python_client/parser/properties.py @@ -64,7 +64,7 @@ def get_imports(self, *, prefix: str) -> Set[str]: back to the root of the generated client. """ if self.nullable or not self.required: - return {"from typing import Optional"} + return {"from typing import Optional", "from typing import cast", f"from {prefix}types import UNSET"} return set() def to_string(self) -> str: @@ -72,12 +72,14 @@ def to_string(self) -> str: if self.default: default = self.default elif not self.required: + default = "cast(None, UNSET)" + elif self.nullable: default = "None" else: default = None if default is not None: - return f"{self.python_name}: {self.get_type_string()} = {self.default}" + return f"{self.python_name}: {self.get_type_string()} = {default}" else: return f"{self.python_name}: {self.get_type_string()}" diff --git a/openapi_python_client/templates/endpoint_macros.pyi b/openapi_python_client/templates/endpoint_macros.pyi index 7fe2ca053..fcfad4981 100644 --- a/openapi_python_client/templates/endpoint_macros.pyi +++ b/openapi_python_client/templates/endpoint_macros.pyi @@ -4,7 +4,7 @@ {% if parameter.required %} headers["{{ parameter.python_name | kebabcase}}"] = {{ parameter.python_name }} {% else %} -if {{ parameter.python_name }} is not None: +if {{ parameter.python_name }} is not UNSET: headers["{{ parameter.python_name | kebabcase}}"] = {{ parameter.python_name }} {% endif %} {% endfor %} @@ -33,7 +33,7 @@ params: Dict[str, Any] = { } {% for property in endpoint.query_parameters %} {% if not property.required %} -if {{ property.python_name }} is not None: +if {{ property.python_name }} is not UNSET: {% if property.template %} params["{{ property.name }}"] = {{ "json_" + property.python_name }} {% else %} diff --git a/openapi_python_client/templates/model.pyi b/openapi_python_client/templates/model.pyi index 899256b2d..5c912f988 100644 --- a/openapi_python_client/templates/model.pyi +++ b/openapi_python_client/templates/model.pyi @@ -1,7 +1,9 @@ -from typing import Any, Dict +from typing import Any, Dict, Optional, Set import attr +from ..types import UNSET + {% for relative in model.relative_imports %} {{ relative }} {% endfor %} @@ -14,7 +16,13 @@ class {{ model.reference.class_name }}: {{ property.to_string() }} {% endfor %} - def to_dict(self) -> Dict[str, Any]: + def to_dict( + self, + include: Optional[Set[str]], + exclude: Optional[Set[str]], + exclude_unset: bool = False, + exclude_none: bool = False, + ) -> Dict[str, Any]: {% for property in model.required_properties + model.optional_properties %} {% if property.template %} {% from "property_templates/" + property.template import transform %} @@ -24,12 +32,26 @@ class {{ model.reference.class_name }}: {% endif %} {% endfor %} - return { + all_properties = { {% for property in model.required_properties + model.optional_properties %} "{{ property.name }}": {{ property.python_name }}, {% endfor %} } + trimmed_properties: Dict[str, Any] = {} + for property_name, property_value in all_properties.items(): + if include is not None and property_name not in include: + continue + if exclude is not None and property_name in exclude: + continue + if exclude_unset and property_value is UNSET: + continue + if exclude_none and property_value is None: + continue + trimmed_properties[property_name] = property_value + + return trimmed_properties + @staticmethod def from_dict(d: Dict[str, Any]) -> "{{ model.reference.class_name }}": {% for property in model.required_properties + model.optional_properties %} diff --git a/openapi_python_client/templates/property_templates/date_property.pyi b/openapi_python_client/templates/property_templates/date_property.pyi index 416acc1e1..66d7afe7a 100644 --- a/openapi_python_client/templates/property_templates/date_property.pyi +++ b/openapi_python_client/templates/property_templates/date_property.pyi @@ -10,8 +10,20 @@ if {{ source }} is not None: {% macro transform(property, source, destination) %} {% if property.required %} +{% if property.nullable %} +{{ destination }} = {{ source }}.isoformat() if {{ source }} else None +{% else %} {{ destination }} = {{ source }}.isoformat() +{% endif %} {% else %} -{{ destination }} = {{ source }}.isoformat() if {{ source }} else None +if {{ source }} is UNSET: + {{ destination }} = UNSET +{% if property.nullable %} +else: + {{ destination }} = {{ source }}.isoformat() if {{ source }} else None +{% else %} +else: + {{ destination }} = {{ source }}.isoformat() +{% endif %} {% endif %} {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/datetime_property.pyi b/openapi_python_client/templates/property_templates/datetime_property.pyi index ff57249a5..534327972 100644 --- a/openapi_python_client/templates/property_templates/datetime_property.pyi +++ b/openapi_python_client/templates/property_templates/datetime_property.pyi @@ -10,8 +10,20 @@ if {{ source }} is not None: {% macro transform(property, source, destination) %} {% if property.required %} +{% if property.nullable %} +{{ destination }} = {{ source }}.isoformat() if {{ source }} else None +{% else %} {{ destination }} = {{ source }}.isoformat() +{% endif %} {% else %} -{{ destination }} = {{ source }}.isoformat() if {{ source }} else None +if {{ source }} is UNSET: + {{ destination }} = UNSET +{% if property.nullable %} +else: + {{ destination }} = {{ source }}.isoformat() if {{ source }} else None +{% else %} +else: + {{ destination }} = {{ source }}.isoformat() +{% endif %} {% endif %} {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/dict_property.pyi b/openapi_python_client/templates/property_templates/dict_property.pyi index 2feabd1d2..62440ebeb 100644 --- a/openapi_python_client/templates/property_templates/dict_property.pyi +++ b/openapi_python_client/templates/property_templates/dict_property.pyi @@ -9,9 +9,9 @@ if {{ source }} is not None: {% endmacro %} {% macro transform(property, source, destination) %} -{% if property.required %} -{{ destination }} = {{ source }} -{% else %} +{% if property.nullable %} {{ destination }} = {{ source }} if {{ source }} else None +{% else %} +{{ destination }} = {{ source }} {% endif %} {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/enum_property.pyi b/openapi_python_client/templates/property_templates/enum_property.pyi index 2cff340c3..3aa22fcae 100644 --- a/openapi_python_client/templates/property_templates/enum_property.pyi +++ b/openapi_python_client/templates/property_templates/enum_property.pyi @@ -10,8 +10,20 @@ if {{ source }} is not None: {% macro transform(property, source, destination) %} {% if property.required %} +{% if property.nullable %} +{{ destination }} = {{ source }}.value if {{ source }} else None +{% else %} {{ destination }} = {{ source }}.value +{% endif %} {% else %} -{{ destination }} = {{ source }}.value if {{ source }} else None +if {{ source }} is UNSET: + {{ destination }} = UNSET +{% if property.nullable %} +else: + {{ destination }} = {{ source }}.value if {{ source }} else None +{% else %} +else: + {{ destination }} = {{ source }}.value +{% endif %} {% endif %} {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/file_property.pyi b/openapi_python_client/templates/property_templates/file_property.pyi index 444c9dbcb..667d98d81 100644 --- a/openapi_python_client/templates/property_templates/file_property.pyi +++ b/openapi_python_client/templates/property_templates/file_property.pyi @@ -5,8 +5,20 @@ {% macro transform(property, source, destination) %} {% if property.required %} +{% if property.nullable %} +{{ destination }} = {{ source }}.to_tuple() if {{ source }} else None +{% else %} {{ destination }} = {{ source }}.to_tuple() +{% endif %} {% else %} -{{ destination }} = {{ source }}.to_tuple() if {{ source }} else None +if {{ source }} is UNSET: + {{ destination }} = UNSET +{% if property.nullable %} +else: + {{ destination }} = {{ source }}.to_tuple() if {{ source }} else None +{% else %} +else: + {{ destination }} = {{ source }}.to_tuple() +{% endif %} {% endif %} {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/list_property.pyi b/openapi_python_client/templates/property_templates/list_property.pyi index 015eb0880..e05d8b8d6 100644 --- a/openapi_python_client/templates/property_templates/list_property.pyi +++ b/openapi_python_client/templates/property_templates/list_property.pyi @@ -33,7 +33,10 @@ for {{ inner_source }} in {{ source }}: {% macro transform(property, source, destination) %} {% set inner_property = property.inner_property %} -{% if not property.required %} + + +{% if property.required %} +{% if property.nullable %} if {{ source }} is None: {{ destination }} = None else: @@ -41,4 +44,16 @@ else: {% else %} {{ _transform(property, source, destination) }} {% endif %} +{% else %} +if {{ source }} is UNSET: + {{ destination }} = UNSET +{% if property.nullable %} +elif {{ source }} is None: + {{ destination }} = None +{% endif %} +else: + {{ _transform(property, source, destination) | indent(4)}} +{% endif %} + + {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/ref_property.pyi b/openapi_python_client/templates/property_templates/ref_property.pyi index c38a5199c..90990a23e 100644 --- a/openapi_python_client/templates/property_templates/ref_property.pyi +++ b/openapi_python_client/templates/property_templates/ref_property.pyi @@ -10,8 +10,20 @@ if {{ source }} is not None: {% macro transform(property, source, destination) %} {% if property.required %} +{% if property.nullable %} +{{ destination }} = {{ source }}.to_dict() if {{ source }} else None +{% else %} {{ destination }} = {{ source }}.to_dict() +{% endif %} {% else %} -{{ destination }} = {{ source }}.to_dict() if {{ source }} else None +if {{ source }} is UNSET: + {{ destination }} = UNSET +{% if property.nullable %} +else: + {{ destination }} = {{ source }}.to_dict() if {{ source }} else None +{% else %} +else: + {{ destination }} = {{ source }}.to_dict() +{% endif %} {% endif %} {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/union_property.pyi b/openapi_python_client/templates/property_templates/union_property.pyi index 1498d68c7..de2d3012c 100644 --- a/openapi_python_client/templates/property_templates/union_property.pyi +++ b/openapi_python_client/templates/property_templates/union_property.pyi @@ -23,11 +23,19 @@ def _parse_{{ property.python_name }}(data: Dict[str, Any]) -> {{ property.get_t {% macro transform(property, source, destination) %} {% if not property.required %} +if {{ source }} is UNSET: + {{ destination }}: {{ property.get_type_string() }} = UNSET +{% endif %} +{% if property.nullable %} +{% if property.required %} if {{ source }} is None: +{% else %}{# There's an if UNSET statement before this } +elif {{ source }} is None: +{% endif %} {{ destination }}: {{ property.get_type_string() }} = None {% endif %} {% for inner_property in property.inner_properties %} - {% if loop.first and property.required %}{# No if None statement before this #} + {% if loop.first and property.required and not property.nullable %}{# No if UNSET or if None statement before this #} if isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True) }}): {% elif not loop.last %} elif isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True) }}): diff --git a/openapi_python_client/templates/types.py b/openapi_python_client/templates/types.py index 951227435..84146cef2 100644 --- a/openapi_python_client/templates/types.py +++ b/openapi_python_client/templates/types.py @@ -1,8 +1,11 @@ """ Contains some shared types for properties """ -from typing import BinaryIO, Generic, MutableMapping, Optional, TextIO, Tuple, TypeVar, Union +from typing import BinaryIO, Generic, MutableMapping, NewType, Optional, TextIO, Tuple, TypeVar, Union import attr +Unset = NewType("Unset", object) +UNSET: Unset = Unset(object()) + @attr.s(auto_attribs=True) class File: From 921f235cad97764f0b5ba66c1bb906a317b5094b Mon Sep 17 00:00:00 2001 From: Ethan Mann Date: Mon, 2 Nov 2020 17:14:36 -0500 Subject: [PATCH 02/13] Fixed typing issues in golden record and added some test endpoints --- .../api/tests/defaults_tests_defaults_post.py | 133 ++++++++++-------- .../tests/json_body_tests_json_body_post.py | 2 +- ...tional_value_tests_optional_query_param.py | 112 +++++++++++++++ .../tests/upload_file_tests_upload_post.py | 14 +- .../my_test_api_client/models/a_model.py | 81 ++++++++--- .../body_upload_file_tests_upload_post.py | 28 +++- .../models/http_validation_error.py | 38 +++-- .../models/validation_error.py | 29 +++- .../golden-record/my_test_api_client/types.py | 5 +- end_to_end_tests/openapi.json | 123 ++++++++++++++-- openapi_python_client/parser/openapi.py | 2 +- openapi_python_client/parser/properties.py | 14 +- openapi_python_client/templates/model.pyi | 6 +- .../property_templates/ref_property.pyi | 8 +- .../property_templates/union_property.pyi | 2 +- openapi_python_client/templates/types.py | 4 +- 16 files changed, 466 insertions(+), 135 deletions(-) create mode 100644 end_to_end_tests/golden-record/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py index 6b284cb9c..15fc274bb 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py @@ -7,33 +7,39 @@ from ...client import Client from ...models.an_enum import AnEnum from ...models.http_validation_error import HTTPValidationError -from ...types import Response +from ...types import UNSET, Response def _get_kwargs( *, client: Client, json_body: Dict[Any, Any], - string_prop: Optional[str] = "the default string", - datetime_prop: Optional[datetime.datetime] = isoparse("1010-10-10T00:00:00"), - date_prop: Optional[datetime.date] = isoparse("1010-10-10").date(), - float_prop: Optional[float] = 3.14, - int_prop: Optional[int] = 7, - boolean_prop: Optional[bool] = False, - list_prop: Optional[List[AnEnum]] = None, - union_prop: Optional[Union[Optional[float], Optional[str]]] = "not a float", - enum_prop: Optional[AnEnum] = None, + string_prop: str = "the default string", + datetime_prop: datetime.datetime = isoparse("1010-10-10T00:00:00"), + date_prop: datetime.date = isoparse("1010-10-10").date(), + float_prop: float = 3.14, + int_prop: int = 7, + boolean_prop: bool = cast(bool, UNSET), + list_prop: List[AnEnum] = cast(List[AnEnum], UNSET), + union_prop: Union[float, str] = "not a float", + enum_prop: AnEnum = cast(AnEnum, UNSET), ) -> Dict[str, Any]: url = "{}/tests/defaults".format(client.base_url) headers: Dict[str, Any] = client.get_headers() - json_datetime_prop = datetime_prop.isoformat() if datetime_prop else None + if datetime_prop is UNSET: + json_datetime_prop = UNSET + else: + json_datetime_prop = datetime_prop.isoformat() - json_date_prop = date_prop.isoformat() if date_prop else None + if date_prop is UNSET: + json_date_prop = UNSET + else: + json_date_prop = date_prop.isoformat() - if list_prop is None: - json_list_prop = None + if list_prop is UNSET: + json_list_prop = UNSET else: json_list_prop = [] for list_prop_item_data in list_prop: @@ -41,33 +47,36 @@ def _get_kwargs( json_list_prop.append(list_prop_item) - if union_prop is None: - json_union_prop: Optional[Union[Optional[float], Optional[str]]] = None + if union_prop is UNSET: + json_union_prop: Union[float, str] = UNSET elif isinstance(union_prop, float): json_union_prop = union_prop else: json_union_prop = union_prop - json_enum_prop = enum_prop.value if enum_prop else None + if enum_prop is UNSET: + json_enum_prop = UNSET + else: + json_enum_prop = enum_prop.value params: Dict[str, Any] = {} - if string_prop is not None: + if string_prop is not UNSET: params["string_prop"] = string_prop - if datetime_prop is not None: + if datetime_prop is not UNSET: params["datetime_prop"] = json_datetime_prop - if date_prop is not None: + if date_prop is not UNSET: params["date_prop"] = json_date_prop - if float_prop is not None: + if float_prop is not UNSET: params["float_prop"] = float_prop - if int_prop is not None: + if int_prop is not UNSET: params["int_prop"] = int_prop - if boolean_prop is not None: + if boolean_prop is not UNSET: params["boolean_prop"] = boolean_prop - if list_prop is not None: + if list_prop is not UNSET: params["list_prop"] = json_list_prop - if union_prop is not None: + if union_prop is not UNSET: params["union_prop"] = json_union_prop - if enum_prop is not None: + if enum_prop is not UNSET: params["enum_prop"] = json_enum_prop json_json_body = json_body @@ -103,15 +112,15 @@ def sync_detailed( *, client: Client, json_body: Dict[Any, Any], - string_prop: Optional[str] = "the default string", - datetime_prop: Optional[datetime.datetime] = isoparse("1010-10-10T00:00:00"), - date_prop: Optional[datetime.date] = isoparse("1010-10-10").date(), - float_prop: Optional[float] = 3.14, - int_prop: Optional[int] = 7, - boolean_prop: Optional[bool] = False, - list_prop: Optional[List[AnEnum]] = None, - union_prop: Optional[Union[Optional[float], Optional[str]]] = "not a float", - enum_prop: Optional[AnEnum] = None, + string_prop: str = "the default string", + datetime_prop: datetime.datetime = isoparse("1010-10-10T00:00:00"), + date_prop: datetime.date = isoparse("1010-10-10").date(), + float_prop: float = 3.14, + int_prop: int = 7, + boolean_prop: bool = cast(bool, UNSET), + list_prop: List[AnEnum] = cast(List[AnEnum], UNSET), + union_prop: Union[float, str] = "not a float", + enum_prop: AnEnum = cast(AnEnum, UNSET), ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, @@ -138,15 +147,15 @@ def sync( *, client: Client, json_body: Dict[Any, Any], - string_prop: Optional[str] = "the default string", - datetime_prop: Optional[datetime.datetime] = isoparse("1010-10-10T00:00:00"), - date_prop: Optional[datetime.date] = isoparse("1010-10-10").date(), - float_prop: Optional[float] = 3.14, - int_prop: Optional[int] = 7, - boolean_prop: Optional[bool] = False, - list_prop: Optional[List[AnEnum]] = None, - union_prop: Optional[Union[Optional[float], Optional[str]]] = "not a float", - enum_prop: Optional[AnEnum] = None, + string_prop: str = "the default string", + datetime_prop: datetime.datetime = isoparse("1010-10-10T00:00:00"), + date_prop: datetime.date = isoparse("1010-10-10").date(), + float_prop: float = 3.14, + int_prop: int = 7, + boolean_prop: bool = cast(bool, UNSET), + list_prop: List[AnEnum] = cast(List[AnEnum], UNSET), + union_prop: Union[float, str] = "not a float", + enum_prop: AnEnum = cast(AnEnum, UNSET), ) -> Optional[Union[None, HTTPValidationError]]: """ """ @@ -169,15 +178,15 @@ async def asyncio_detailed( *, client: Client, json_body: Dict[Any, Any], - string_prop: Optional[str] = "the default string", - datetime_prop: Optional[datetime.datetime] = isoparse("1010-10-10T00:00:00"), - date_prop: Optional[datetime.date] = isoparse("1010-10-10").date(), - float_prop: Optional[float] = 3.14, - int_prop: Optional[int] = 7, - boolean_prop: Optional[bool] = False, - list_prop: Optional[List[AnEnum]] = None, - union_prop: Optional[Union[Optional[float], Optional[str]]] = "not a float", - enum_prop: Optional[AnEnum] = None, + string_prop: str = "the default string", + datetime_prop: datetime.datetime = isoparse("1010-10-10T00:00:00"), + date_prop: datetime.date = isoparse("1010-10-10").date(), + float_prop: float = 3.14, + int_prop: int = 7, + boolean_prop: bool = cast(bool, UNSET), + list_prop: List[AnEnum] = cast(List[AnEnum], UNSET), + union_prop: Union[float, str] = "not a float", + enum_prop: AnEnum = cast(AnEnum, UNSET), ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, @@ -203,15 +212,15 @@ async def asyncio( *, client: Client, json_body: Dict[Any, Any], - string_prop: Optional[str] = "the default string", - datetime_prop: Optional[datetime.datetime] = isoparse("1010-10-10T00:00:00"), - date_prop: Optional[datetime.date] = isoparse("1010-10-10").date(), - float_prop: Optional[float] = 3.14, - int_prop: Optional[int] = 7, - boolean_prop: Optional[bool] = False, - list_prop: Optional[List[AnEnum]] = None, - union_prop: Optional[Union[Optional[float], Optional[str]]] = "not a float", - enum_prop: Optional[AnEnum] = None, + string_prop: str = "the default string", + datetime_prop: datetime.datetime = isoparse("1010-10-10T00:00:00"), + date_prop: datetime.date = isoparse("1010-10-10").date(), + float_prop: float = 3.14, + int_prop: int = 7, + boolean_prop: bool = cast(bool, UNSET), + list_prop: List[AnEnum] = cast(List[AnEnum], UNSET), + union_prop: Union[float, str] = "not a float", + enum_prop: AnEnum = cast(AnEnum, UNSET), ) -> Optional[Union[None, HTTPValidationError]]: """ """ diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py index eb556c5d7..5edf4c025 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py @@ -17,7 +17,7 @@ def _get_kwargs( headers: Dict[str, Any] = client.get_headers() - json_json_body = json_body.to_dict() + json_json_body = json_body.to_dict(exclude_unset=True) return { "url": url, diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py new file mode 100644 index 000000000..91b9fb0b1 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py @@ -0,0 +1,112 @@ +from typing import Any, Dict, List, Optional, Union, cast + +import httpx + +from ...client import Client +from ...models.http_validation_error import HTTPValidationError +from ...types import UNSET, Response + + +def _get_kwargs( + *, + client: Client, + query_param: List[str] = cast(List[str], UNSET), +) -> Dict[str, Any]: + url = "{}/tests/optional_query_param/".format(client.base_url) + + headers: Dict[str, Any] = client.get_headers() + + if query_param is UNSET: + json_query_param = UNSET + else: + json_query_param = query_param + + params: Dict[str, Any] = {} + if query_param is not UNSET: + params["query_param"] = json_query_param + + return { + "url": url, + "headers": headers, + "cookies": client.get_cookies(), + "timeout": client.get_timeout(), + "params": params, + } + + +def _parse_response(*, response: httpx.Response) -> Optional[Union[None, HTTPValidationError]]: + if response.status_code == 200: + return None + if response.status_code == 422: + return HTTPValidationError.from_dict(cast(Dict[str, Any], response.json())) + return None + + +def _build_response(*, response: httpx.Response) -> Response[Union[None, HTTPValidationError]]: + return Response( + status_code=response.status_code, + content=response.content, + headers=response.headers, + parsed=_parse_response(response=response), + ) + + +def sync_detailed( + *, + client: Client, + query_param: List[str] = cast(List[str], UNSET), +) -> Response[Union[None, HTTPValidationError]]: + kwargs = _get_kwargs( + client=client, + query_param=query_param, + ) + + response = httpx.get( + **kwargs, + ) + + return _build_response(response=response) + + +def sync( + *, + client: Client, + query_param: List[str] = cast(List[str], UNSET), +) -> Optional[Union[None, HTTPValidationError]]: + """ Test optional query parameters """ + + return sync_detailed( + client=client, + query_param=query_param, + ).parsed + + +async def asyncio_detailed( + *, + client: Client, + query_param: List[str] = cast(List[str], UNSET), +) -> Response[Union[None, HTTPValidationError]]: + kwargs = _get_kwargs( + client=client, + query_param=query_param, + ) + + async with httpx.AsyncClient() as _client: + response = await _client.get(**kwargs) + + return _build_response(response=response) + + +async def asyncio( + *, + client: Client, + query_param: List[str] = cast(List[str], UNSET), +) -> Optional[Union[None, HTTPValidationError]]: + """ Test optional query parameters """ + + return ( + await asyncio_detailed( + client=client, + query_param=query_param, + ) + ).parsed diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py index 9891feff6..3c228b5db 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py @@ -5,20 +5,20 @@ from ...client import Client from ...models.body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost from ...models.http_validation_error import HTTPValidationError -from ...types import Response +from ...types import UNSET, Response def _get_kwargs( *, client: Client, multipart_data: BodyUploadFileTestsUploadPost, - keep_alive: Optional[bool] = None, + keep_alive: bool = cast(bool, UNSET), ) -> Dict[str, Any]: url = "{}/tests/upload".format(client.base_url) headers: Dict[str, Any] = client.get_headers() - if keep_alive is not None: + if keep_alive is not UNSET: headers["keep-alive"] = keep_alive return { @@ -51,7 +51,7 @@ def sync_detailed( *, client: Client, multipart_data: BodyUploadFileTestsUploadPost, - keep_alive: Optional[bool] = None, + keep_alive: bool = cast(bool, UNSET), ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, @@ -70,7 +70,7 @@ def sync( *, client: Client, multipart_data: BodyUploadFileTestsUploadPost, - keep_alive: Optional[bool] = None, + keep_alive: bool = cast(bool, UNSET), ) -> Optional[Union[None, HTTPValidationError]]: """ Upload a file """ @@ -85,7 +85,7 @@ async def asyncio_detailed( *, client: Client, multipart_data: BodyUploadFileTestsUploadPost, - keep_alive: Optional[bool] = None, + keep_alive: bool = cast(bool, UNSET), ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, @@ -103,7 +103,7 @@ async def asyncio( *, client: Client, multipart_data: BodyUploadFileTestsUploadPost, - keep_alive: Optional[bool] = None, + keep_alive: bool = cast(bool, UNSET), ) -> Optional[Union[None, HTTPValidationError]]: """ Upload a file """ 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 a1a0ace0c..111aeffee 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,11 +1,12 @@ import datetime -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Set, Union, cast import attr from dateutil.parser import isoparse from ..models.an_enum import AnEnum from ..models.different_enum import DifferentEnum +from ..types import UNSET @attr.s(auto_attribs=True) @@ -13,17 +14,25 @@ class AModel: """ A Model for testing all the ways custom objects can be used """ an_enum_value: AnEnum - some_dict: Optional[Dict[Any, Any]] a_camel_date_time: Union[datetime.datetime, datetime.date] a_date: datetime.date - nested_list_of_enums: Optional[List[List[DifferentEnum]]] = None - attr_1_leading_digit: Optional[str] = None - - def to_dict(self) -> Dict[str, Any]: + required_not_nullable: str + nested_list_of_enums: List[List[DifferentEnum]] = cast(List[List[DifferentEnum]], UNSET) + some_dict: Optional[Dict[Any, Any]] = None + attr_1_leading_digit: str = cast(str, UNSET) + required_nullable: Optional[str] = None + not_required_nullable: Optional[str] = cast(Optional[str], UNSET) + not_required_not_nullable: str = cast(str, UNSET) + + def to_dict( + self, + include: Optional[Set[str]] = None, + exclude: Optional[Set[str]] = None, + exclude_unset: bool = False, + exclude_none: bool = False, + ) -> Dict[str, Any]: an_enum_value = self.an_enum_value.value - some_dict = self.some_dict - if isinstance(self.a_camel_date_time, datetime.datetime): a_camel_date_time = self.a_camel_date_time.isoformat() @@ -32,11 +41,14 @@ def to_dict(self) -> Dict[str, Any]: a_date = self.a_date.isoformat() - if self.nested_list_of_enums is None: - nested_list_of_enums = None + required_not_nullable = self.required_not_nullable + + if self.nested_list_of_enums is UNSET: + nested_list_of_enums = UNSET else: nested_list_of_enums = [] for nested_list_of_enums_item_data in self.nested_list_of_enums: + nested_list_of_enums_item = [] for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: nested_list_of_enums_item_item = nested_list_of_enums_item_item_data.value @@ -45,23 +57,44 @@ def to_dict(self) -> Dict[str, Any]: nested_list_of_enums.append(nested_list_of_enums_item) + some_dict = self.some_dict if self.some_dict else None + attr_1_leading_digit = self.attr_1_leading_digit + required_nullable = self.required_nullable + not_required_nullable = self.not_required_nullable + not_required_not_nullable = self.not_required_not_nullable - return { + all_properties = { "an_enum_value": an_enum_value, - "some_dict": some_dict, "aCamelDateTime": a_camel_date_time, "a_date": a_date, + "required_not_nullable": required_not_nullable, "nested_list_of_enums": nested_list_of_enums, + "some_dict": some_dict, "1_leading_digit": attr_1_leading_digit, + "required_nullable": required_nullable, + "not_required_nullable": not_required_nullable, + "not_required_not_nullable": not_required_not_nullable, } + trimmed_properties: Dict[str, Any] = {} + for property_name, property_value in all_properties.items(): + if include is not None and property_name not in include: + continue + if exclude is not None and property_name in exclude: + continue + if exclude_unset and property_value is UNSET: + continue + if exclude_none and property_value is None: + continue + trimmed_properties[property_name] = property_value + + return trimmed_properties + @staticmethod def from_dict(d: Dict[str, Any]) -> "AModel": an_enum_value = AnEnum(d["an_enum_value"]) - some_dict = d["some_dict"] - def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime.datetime, datetime.date]: a_camel_date_time: Union[datetime.datetime, datetime.date] try: @@ -78,8 +111,10 @@ def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime.datetime, d a_date = isoparse(d["a_date"]).date() + required_not_nullable = d["required_not_nullable"] + nested_list_of_enums = [] - for nested_list_of_enums_item_data in d.get("nested_list_of_enums") or []: + for nested_list_of_enums_item_data in d.get("nested_list_of_enums", UNSET) or []: nested_list_of_enums_item = [] for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: nested_list_of_enums_item_item = DifferentEnum(nested_list_of_enums_item_item_data) @@ -88,13 +123,25 @@ def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime.datetime, d nested_list_of_enums.append(nested_list_of_enums_item) - attr_1_leading_digit = d.get("1_leading_digit") + some_dict = d["some_dict"] + + attr_1_leading_digit = d.get("1_leading_digit", UNSET) + + required_nullable = d["required_nullable"] + + not_required_nullable = d.get("not_required_nullable", UNSET) + + not_required_not_nullable = d.get("not_required_not_nullable", UNSET) return AModel( an_enum_value=an_enum_value, - some_dict=some_dict, a_camel_date_time=a_camel_date_time, a_date=a_date, + required_not_nullable=required_not_nullable, nested_list_of_enums=nested_list_of_enums, + some_dict=some_dict, attr_1_leading_digit=attr_1_leading_digit, + required_nullable=required_nullable, + not_required_nullable=not_required_nullable, + not_required_not_nullable=not_required_not_nullable, ) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py index 4fe7f8476..a6899351e 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py @@ -1,8 +1,8 @@ -from typing import Any, Dict +from typing import Any, Dict, Optional, Set import attr -from ..types import File +from ..types import UNSET, File @attr.s(auto_attribs=True) @@ -11,13 +11,33 @@ class BodyUploadFileTestsUploadPost: some_file: File - def to_dict(self) -> Dict[str, Any]: + def to_dict( + self, + include: Optional[Set[str]] = None, + exclude: Optional[Set[str]] = None, + exclude_unset: bool = False, + exclude_none: bool = False, + ) -> Dict[str, Any]: some_file = self.some_file.to_tuple() - return { + all_properties = { "some_file": some_file, } + trimmed_properties: Dict[str, Any] = {} + for property_name, property_value in all_properties.items(): + if include is not None and property_name not in include: + continue + if exclude is not None and property_name in exclude: + continue + if exclude_unset and property_value is UNSET: + continue + if exclude_none and property_value is None: + continue + trimmed_properties[property_name] = property_value + + return trimmed_properties + @staticmethod def from_dict(d: Dict[str, Any]) -> "BodyUploadFileTestsUploadPost": some_file = d["some_file"] diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py b/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py index 90cd71e8c..fe5ec3b99 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py @@ -1,34 +1,56 @@ -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Set, cast import attr from ..models.validation_error import ValidationError +from ..types import UNSET @attr.s(auto_attribs=True) class HTTPValidationError: """ """ - detail: Optional[List[ValidationError]] = None + detail: List[ValidationError] = cast(List[ValidationError], UNSET) - def to_dict(self) -> Dict[str, Any]: - if self.detail is None: - detail = None + def to_dict( + self, + include: Optional[Set[str]] = None, + exclude: Optional[Set[str]] = None, + exclude_unset: bool = False, + exclude_none: bool = False, + ) -> Dict[str, Any]: + + if self.detail is UNSET: + detail = UNSET else: detail = [] for detail_item_data in self.detail: - detail_item = detail_item_data.to_dict() + detail_item = detail_item_data.to_dict(exclude_unset=True) detail.append(detail_item) - return { + all_properties = { "detail": detail, } + trimmed_properties: Dict[str, Any] = {} + for property_name, property_value in all_properties.items(): + if include is not None and property_name not in include: + continue + if exclude is not None and property_name in exclude: + continue + if exclude_unset and property_value is UNSET: + continue + if exclude_none and property_value is None: + continue + trimmed_properties[property_name] = property_value + + return trimmed_properties + @staticmethod def from_dict(d: Dict[str, Any]) -> "HTTPValidationError": detail = [] - for detail_item_data in d.get("detail") or []: + for detail_item_data in d.get("detail", UNSET) or []: detail_item = ValidationError.from_dict(detail_item_data) detail.append(detail_item) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py b/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py index 1e415c476..de98717fc 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py @@ -1,7 +1,9 @@ -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional, Set import attr +from ..types import UNSET + @attr.s(auto_attribs=True) class ValidationError: @@ -11,18 +13,39 @@ class ValidationError: msg: str type: str - def to_dict(self) -> Dict[str, Any]: + def to_dict( + self, + include: Optional[Set[str]] = None, + exclude: Optional[Set[str]] = None, + exclude_unset: bool = False, + exclude_none: bool = False, + ) -> Dict[str, Any]: + loc = self.loc msg = self.msg type = self.type - return { + all_properties = { "loc": loc, "msg": msg, "type": type, } + trimmed_properties: Dict[str, Any] = {} + for property_name, property_value in all_properties.items(): + if include is not None and property_name not in include: + continue + if exclude is not None and property_name in exclude: + continue + if exclude_unset and property_value is UNSET: + continue + if exclude_none and property_value is None: + continue + trimmed_properties[property_name] = property_value + + return trimmed_properties + @staticmethod def from_dict(d: Dict[str, Any]) -> "ValidationError": loc = d["loc"] diff --git a/end_to_end_tests/golden-record/my_test_api_client/types.py b/end_to_end_tests/golden-record/my_test_api_client/types.py index 951227435..4d2d3c4f5 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/types.py +++ b/end_to_end_tests/golden-record/my_test_api_client/types.py @@ -1,8 +1,11 @@ """ Contains some shared types for properties """ -from typing import BinaryIO, Generic, MutableMapping, Optional, TextIO, Tuple, TypeVar, Union +from typing import Any, BinaryIO, Generic, MutableMapping, NewType, Optional, TextIO, Tuple, TypeVar, Union import attr +Unset = NewType("Unset", object) +UNSET: Any = Unset(object()) + @attr.s(auto_attribs=True) class File: diff --git a/end_to_end_tests/openapi.json b/end_to_end_tests/openapi.json index f7a933b42..2bde7a520 100644 --- a/end_to_end_tests/openapi.json +++ b/end_to_end_tests/openapi.json @@ -28,7 +28,9 @@ }, "/tests/": { "get": { - "tags": ["tests"], + "tags": [ + "tests" + ], "summary": "Get List", "description": "Get a list of things ", "operationId": "getUserList", @@ -94,7 +96,9 @@ }, "/tests/basic_lists/strings": { "get": { - "tags": ["tests"], + "tags": [ + "tests" + ], "summary": "Get Basic List Of Strings", "description": "Get a list of strings ", "operationId": "getBasicListOfStrings", @@ -118,7 +122,9 @@ }, "/tests/basic_lists/integers": { "get": { - "tags": ["tests"], + "tags": [ + "tests" + ], "summary": "Get Basic List Of Integers", "description": "Get a list of integers ", "operationId": "getBasicListOfIntegers", @@ -142,7 +148,9 @@ }, "/tests/basic_lists/floats": { "get": { - "tags": ["tests"], + "tags": [ + "tests" + ], "summary": "Get Basic List Of Floats", "description": "Get a list of floats ", "operationId": "getBasicListOfFloats", @@ -166,7 +174,9 @@ }, "/tests/basic_lists/booleans": { "get": { - "tags": ["tests"], + "tags": [ + "tests" + ], "summary": "Get Basic List Of Booleans", "description": "Get a list of booleans ", "operationId": "getBasicListOfBooleans", @@ -190,7 +200,9 @@ }, "/tests/upload": { "post": { - "tags": ["tests"], + "tags": [ + "tests" + ], "summary": "Upload File", "description": "Upload a file ", "operationId": "upload_file_tests_upload_post", @@ -239,7 +251,9 @@ }, "/tests/json_body": { "post": { - "tags": ["tests"], + "tags": [ + "tests" + ], "summary": "Json Body", "description": "Try sending a JSON body ", "operationId": "json_body_tests_json_body_post", @@ -277,7 +291,9 @@ }, "/tests/defaults": { "post": { - "tags": ["tests"], + "tags": [ + "tests" + ], "summary": "Defaults", "operationId": "defaults_tests_defaults_post", "parameters": [ @@ -351,7 +367,10 @@ "items": { "$ref": "#/components/schemas/AnEnum" }, - "default": ["FIRST_VALUE", "SECOND_VALUE"] + "default": [ + "FIRST_VALUE", + "SECOND_VALUE" + ] }, "name": "list_prop", "in": "query" @@ -422,7 +441,9 @@ }, "/tests/octet_stream": { "get": { - "tags": ["tests"], + "tags": [ + "tests" + ], "summary": "Octet Stream", "operationId": "octet_stream_tests_octet_stream_get", "responses": { @@ -442,7 +463,9 @@ }, "/tests/no_response": { "get": { - "tags": ["tests"], + "tags": [ + "tests" + ], "summary": "No Response", "operationId": "no_response_tests_no_response_get", "responses": { @@ -459,7 +482,9 @@ }, "/tests/unsupported_content": { "get": { - "tags": ["tests"], + "tags": [ + "tests" + ], "summary": "Unsupported Content", "operationId": "unsupported_content_tests_unsupported_content_get", "responses": { @@ -482,7 +507,9 @@ }, "/tests/int_enum": { "post": { - "tags": ["tests"], + "tags": [ + "tests" + ], "summary": "Int Enum", "operationId": "int_enum_tests_int_enum_post", "parameters": [ @@ -516,13 +543,61 @@ } } } + }, + "/tests/optional_query_param/": { + "get": { + "tags": [ + "tests" + ], + "summary": "Optional Query Params test", + "description": "Test optional query parameters", + "operationId": "optional_value_tests_optional_query_param", + "parameters": [ + { + "required": false, + "schema": { + "title": "Query Param", + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "string1", + "string2" + ] + }, + "name": "query_param", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } } }, "components": { "schemas": { "AModel": { "title": "AModel", - "required": ["an_enum_value", "some_dict", "aCamelDateTime", "a_date"], + "required": ["an_enum_value", "some_dict", "aCamelDateTime", "a_date", "required_nullable", "required_not_nullable"], "type": "object", "properties": { "an_enum_value": { @@ -568,6 +643,26 @@ "1_leading_digit": { "title": "Leading Digit", "type": "string" + }, + "required_nullable": { + "title": "Required AND Nullable", + "type": "string", + "nullable": true + }, + "required_not_nullable": { + "title": "Required NOT Nullable", + "type": "string", + "nullable": false + }, + "not_required_nullable": { + "title": "NOT Required AND nullable", + "type": "string", + "nullable": true + }, + "not_required_not_nullable": { + "title": "NOT Required AND NOT Nullable", + "type": "string", + "nullable": false } }, "description": "A Model for testing all the ways custom objects can be used " diff --git a/openapi_python_client/parser/openapi.py b/openapi_python_client/parser/openapi.py index 50966f8a0..1626f1541 100644 --- a/openapi_python_client/parser/openapi.py +++ b/openapi_python_client/parser/openapi.py @@ -255,7 +255,7 @@ def from_data(*, data: oai.Schema, name: str) -> Union["Model", ParseError]: p = property_from_data(name=key, required=required, data=value) if isinstance(p, ParseError): return p - if required: + if p.required and not p.nullable: required_properties.append(p) else: optional_properties.append(p) diff --git a/openapi_python_client/parser/properties.py b/openapi_python_client/parser/properties.py index 69ab3e324..78274e348 100644 --- a/openapi_python_client/parser/properties.py +++ b/openapi_python_client/parser/properties.py @@ -51,7 +51,7 @@ def get_type_string(self, no_optional: bool = False) -> str: Args: no_optional: Do not include Optional even if the value is optional (needed for isinstance checks) """ - if no_optional or (self.required and not self.nullable): + if no_optional or not self.nullable: return self._type_string return f"Optional[{self._type_string}]" @@ -64,7 +64,7 @@ def get_imports(self, *, prefix: str) -> Set[str]: back to the root of the generated client. """ if self.nullable or not self.required: - return {"from typing import Optional", "from typing import cast", f"from {prefix}types import UNSET"} + return {"from typing import Any, cast, Optional", f"from {prefix}types import UNSET, Unset"} return set() def to_string(self) -> str: @@ -72,7 +72,7 @@ def to_string(self) -> str: if self.default: default = self.default elif not self.required: - default = "cast(None, UNSET)" + default = f"cast({self.get_type_string()}, UNSET)" elif self.nullable: default = "None" else: @@ -224,7 +224,7 @@ class ListProperty(Property, Generic[InnerProp]): def get_type_string(self, no_optional: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ - if no_optional or (self.required and not self.nullable): + if no_optional or not self.nullable: return f"List[{self.inner_property.get_type_string()}]" return f"Optional[List[{self.inner_property.get_type_string()}]]" @@ -256,7 +256,7 @@ def get_type_string(self, no_optional: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ inner_types = [p.get_type_string() for p in self.inner_properties] inner_prop_string = ", ".join(inner_types) - if no_optional or (self.required and not self.nullable): + if no_optional or not self.nullable: return f"Union[{inner_prop_string}]" return f"Optional[Union[{inner_prop_string}]]" @@ -331,7 +331,7 @@ def get_enum(name: str) -> Optional["EnumProperty"]: def get_type_string(self, no_optional: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ - if no_optional or (self.required and not self.nullable): + if no_optional or not self.nullable: return self.reference.class_name return f"Optional[{self.reference.class_name}]" @@ -392,7 +392,7 @@ def template(self) -> str: # type: ignore def get_type_string(self, no_optional: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ - if no_optional or (self.required and not self.nullable): + if no_optional or not self.nullable: return self.reference.class_name return f"Optional[{self.reference.class_name}]" diff --git a/openapi_python_client/templates/model.pyi b/openapi_python_client/templates/model.pyi index 5c912f988..8583bb3f0 100644 --- a/openapi_python_client/templates/model.pyi +++ b/openapi_python_client/templates/model.pyi @@ -18,8 +18,8 @@ class {{ model.reference.class_name }}: def to_dict( self, - include: Optional[Set[str]], - exclude: Optional[Set[str]], + include: Optional[Set[str]] = None, + exclude: Optional[Set[str]] = None, exclude_unset: bool = False, exclude_none: bool = False, ) -> Dict[str, Any]: @@ -58,7 +58,7 @@ class {{ model.reference.class_name }}: {% if property.required %} {% set property_source = 'd["' + property.name + '"]' %} {% else %} - {% set property_source = 'd.get("' + property.name + '")' %} + {% set property_source = 'd.get("' + property.name + '", UNSET)' %} {% endif %} {% if property.template %} {% from "property_templates/" + property.template import construct %} diff --git a/openapi_python_client/templates/property_templates/ref_property.pyi b/openapi_python_client/templates/property_templates/ref_property.pyi index 90990a23e..b43c8ea71 100644 --- a/openapi_python_client/templates/property_templates/ref_property.pyi +++ b/openapi_python_client/templates/property_templates/ref_property.pyi @@ -11,19 +11,19 @@ if {{ source }} is not None: {% macro transform(property, source, destination) %} {% if property.required %} {% if property.nullable %} -{{ destination }} = {{ source }}.to_dict() if {{ source }} else None +{{ destination }} = {{ source }}.to_dict(exclude_unset=True) if {{ source }} else None {% else %} -{{ destination }} = {{ source }}.to_dict() +{{ destination }} = {{ source }}.to_dict(exclude_unset=True) {% endif %} {% else %} if {{ source }} is UNSET: {{ destination }} = UNSET {% if property.nullable %} else: - {{ destination }} = {{ source }}.to_dict() if {{ source }} else None + {{ destination }} = {{ source }}.to_dict(exclude_unset=True) if {{ source }} else None {% else %} else: - {{ destination }} = {{ source }}.to_dict() + {{ destination }} = {{ source }}.to_dict(exclude_unset=True) {% endif %} {% endif %} {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/union_property.pyi b/openapi_python_client/templates/property_templates/union_property.pyi index de2d3012c..f74604294 100644 --- a/openapi_python_client/templates/property_templates/union_property.pyi +++ b/openapi_python_client/templates/property_templates/union_property.pyi @@ -29,7 +29,7 @@ if {{ source }} is UNSET: {% if property.nullable %} {% if property.required %} if {{ source }} is None: -{% else %}{# There's an if UNSET statement before this } +{% else %}{# There's an if UNSET statement before this #} elif {{ source }} is None: {% endif %} {{ destination }}: {{ property.get_type_string() }} = None diff --git a/openapi_python_client/templates/types.py b/openapi_python_client/templates/types.py index 84146cef2..ad022eb9c 100644 --- a/openapi_python_client/templates/types.py +++ b/openapi_python_client/templates/types.py @@ -1,10 +1,10 @@ """ Contains some shared types for properties """ -from typing import BinaryIO, Generic, MutableMapping, NewType, Optional, TextIO, Tuple, TypeVar, Union +from typing import BinaryIO, Generic, MutableMapping, NewType, Optional, TextIO, Tuple, TypeVar, Union, Any import attr Unset = NewType("Unset", object) -UNSET: Unset = Unset(object()) +UNSET: Any = Unset(object()) @attr.s(auto_attribs=True) From ae9e2b39b923b4893605fc38d3e99e9f4c163e15 Mon Sep 17 00:00:00 2001 From: Ethan Mann Date: Mon, 2 Nov 2020 17:59:57 -0500 Subject: [PATCH 03/13] Brought test coverage back to what it was before --- openapi_python_client/__init__.py | 2 +- openapi_python_client/parser/properties.py | 2 +- openapi_python_client/templates/types.py | 2 +- pyproject.toml | 2 +- tests/test_openapi_parser/test_openapi.py | 2 +- tests/test_openapi_parser/test_properties.py | 144 ++++++++++++++++--- 6 files changed, 128 insertions(+), 26 deletions(-) diff --git a/openapi_python_client/__init__.py b/openapi_python_client/__init__.py index 2fcca23b3..3df7594fc 100644 --- a/openapi_python_client/__init__.py +++ b/openapi_python_client/__init__.py @@ -18,7 +18,7 @@ from .utils import snake_case if sys.version_info.minor < 8: # version did not exist before 3.8, need to use a backport - from importlib_metadata import version + from importlib_metadata import version # pragma: no cover else: from importlib.metadata import version # type: ignore diff --git a/openapi_python_client/parser/properties.py b/openapi_python_client/parser/properties.py index 78274e348..6fd09601f 100644 --- a/openapi_python_client/parser/properties.py +++ b/openapi_python_client/parser/properties.py @@ -64,7 +64,7 @@ def get_imports(self, *, prefix: str) -> Set[str]: back to the root of the generated client. """ if self.nullable or not self.required: - return {"from typing import Any, cast, Optional", f"from {prefix}types import UNSET, Unset"} + return {"from typing import cast, Optional", f"from {prefix}types import UNSET, Unset"} return set() def to_string(self) -> str: diff --git a/openapi_python_client/templates/types.py b/openapi_python_client/templates/types.py index ad022eb9c..4d2d3c4f5 100644 --- a/openapi_python_client/templates/types.py +++ b/openapi_python_client/templates/types.py @@ -1,5 +1,5 @@ """ Contains some shared types for properties """ -from typing import BinaryIO, Generic, MutableMapping, NewType, Optional, TextIO, Tuple, TypeVar, Union, Any +from typing import Any, BinaryIO, Generic, MutableMapping, NewType, Optional, TextIO, Tuple, TypeVar, Union import attr diff --git a/pyproject.toml b/pyproject.toml index 0253d3f3b..68b1c507a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,7 @@ isort .\ && flake8 openapi_python_client\ && safety check --bare\ && mypy openapi_python_client\ - && pytest --cov openapi_python_client tests\ + && pytest --cov openapi_python_client tests --cov-report=term-missing\ """ regen = "python -m end_to_end_tests.regen_golden_record" e2e = "pytest openapi_python_client end_to_end_tests" diff --git a/tests/test_openapi_parser/test_openapi.py b/tests/test_openapi_parser/test_openapi.py index d74548e9a..987dfd763 100644 --- a/tests/test_openapi_parser/test_openapi.py +++ b/tests/test_openapi_parser/test_openapi.py @@ -80,7 +80,7 @@ def test_from_data(self, mocker): "OptionalDateTime": mocker.MagicMock(), }, ) - required_property = mocker.MagicMock(autospec=Property) + required_property = mocker.MagicMock(autospec=Property, required=True, nullable=False) required_imports = mocker.MagicMock() required_property.get_imports.return_value = {required_imports} optional_property = mocker.MagicMock(autospec=Property) diff --git a/tests/test_openapi_parser/test_properties.py b/tests/test_openapi_parser/test_properties.py index dda80620c..ff8332821 100644 --- a/tests/test_openapi_parser/test_properties.py +++ b/tests/test_openapi_parser/test_properties.py @@ -26,7 +26,7 @@ def test_get_type_string(self): assert p.get_type_string() == "TestType" p.required = False - assert p.get_type_string() == "Optional[TestType]" + assert p.get_type_string() == "TestType" assert p.get_type_string(True) == "TestType" p.required = False @@ -41,7 +41,12 @@ def test_to_string(self, mocker): get_type_string = mocker.patch.object(p, "get_type_string") assert p.to_string() == f"{name}: {get_type_string()}" + p.required = False + assert p.to_string() == f"{name}: {get_type_string()} = cast({get_type_string()}, UNSET)" + + p.required = True + p.nullable = True assert p.to_string() == f"{name}: {get_type_string()} = None" p.default = "TEST" @@ -54,7 +59,7 @@ def test_get_imports(self): assert p.get_imports(prefix="") == set() p.required = False - assert p.get_imports(prefix="") == {"from typing import Optional"} + assert p.get_imports(prefix="") == {"from typing import cast, Optional", "from types import UNSET, Unset"} def test__validate_default(self): from openapi_python_client.parser.properties import Property @@ -77,6 +82,8 @@ def test_get_type_string(self): assert p.get_type_string() == "str" p.required = False + assert p.get_type_string() == "str" + p.nullable = True assert p.get_type_string() == "Optional[str]" def test__validate_default(self): @@ -99,10 +106,20 @@ def test_get_imports(self): p.required = False assert p.get_imports(prefix="...") == { - "from typing import Optional", "import datetime", "from typing import cast", "from dateutil.parser import isoparse", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", + } + + p.nullable = True + assert p.get_imports(prefix="...") == { + "import datetime", + "from typing import cast", + "from dateutil.parser import isoparse", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", } def test__validate_default(self): @@ -127,11 +144,21 @@ def test_get_imports(self): } p.required = False - assert p.get_imports(prefix="..") == { - "from typing import Optional", + assert p.get_imports(prefix="...") == { "import datetime", "from typing import cast", "from dateutil.parser import isoparse", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", + } + + p.nullable = True + assert p.get_imports(prefix="...") == { + "import datetime", + "from typing import cast", + "from dateutil.parser import isoparse", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", } def test__validate_default(self): @@ -148,14 +175,24 @@ class TestFileProperty: def test_get_imports(self): from openapi_python_client.parser.properties import FileProperty - prefix = ".." + prefix = "..." p = FileProperty(name="test", required=True, default=None, nullable=False) - assert p.get_imports(prefix=prefix) == {"from ..types import File"} + assert p.get_imports(prefix=prefix) == { + "from ...types import File", + } p.required = False assert p.get_imports(prefix=prefix) == { - "from typing import Optional", - "from ..types import File", + "from ...types import File", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", + } + + p.nullable = True + assert p.get_imports(prefix=prefix) == { + "from ...types import File", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", } def test__validate_default(self): @@ -217,7 +254,11 @@ def test_get_type_string(self, mocker): p = ListProperty(name="test", required=True, default=None, inner_property=inner_property, nullable=False) assert p.get_type_string() == f"List[{inner_type_string}]" + p.required = False + assert p.get_type_string() == f"List[{inner_type_string}]" + + p.nullable = True assert p.get_type_string() == f"Optional[List[{inner_type_string}]]" def test_get_type_imports(self, mocker): @@ -226,18 +267,28 @@ def test_get_type_imports(self, mocker): inner_property = mocker.MagicMock() inner_import = mocker.MagicMock() inner_property.get_imports.return_value = {inner_import} - prefix = mocker.MagicMock() + prefix = "..." p = ListProperty(name="test", required=True, default=None, inner_property=inner_property, nullable=False) assert p.get_imports(prefix=prefix) == { inner_import, "from typing import List", } + p.required = False assert p.get_imports(prefix=prefix) == { inner_import, "from typing import List", - "from typing import Optional", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", + } + + p.nullable = True + assert p.get_imports(prefix=prefix) == { + inner_import, + "from typing import List", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", } def test__validate_default(self, mocker): @@ -266,7 +317,11 @@ def test_get_type_string(self, mocker): ) assert p.get_type_string() == "Union[inner_type_string_1, inner_type_string_2]" + p.required = False + assert p.get_type_string() == "Union[inner_type_string_1, inner_type_string_2]" + + p.nullable = True assert p.get_type_string() == "Optional[Union[inner_type_string_1, inner_type_string_2]]" def test_get_type_imports(self, mocker): @@ -278,7 +333,7 @@ def test_get_type_imports(self, mocker): inner_property_2 = mocker.MagicMock() inner_import_2 = mocker.MagicMock() inner_property_2.get_imports.return_value = {inner_import_2} - prefix = mocker.MagicMock() + prefix = "..." p = UnionProperty( name="test", required=True, @@ -292,12 +347,23 @@ def test_get_type_imports(self, mocker): inner_import_2, "from typing import Union", } + p.required = False assert p.get_imports(prefix=prefix) == { inner_import_1, inner_import_2, "from typing import Union", - "from typing import Optional", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", + } + + p.nullable = True + assert p.get_imports(prefix=prefix) == { + inner_import_1, + inner_import_2, + "from typing import Union", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", } def test__validate_default(self, mocker): @@ -391,14 +457,19 @@ def test_get_type_string(self, mocker): ) assert enum_property.get_type_string() == "MyTestEnum" + enum_property.required = False + assert enum_property.get_type_string() == "MyTestEnum" + + enum_property.nullable = True assert enum_property.get_type_string() == "Optional[MyTestEnum]" + properties._existing_enums = {} def test_get_imports(self, mocker): fake_reference = mocker.MagicMock(class_name="MyTestEnum", module_name="my_test_enum") mocker.patch(f"{MODULE_NAME}.Reference.from_ref", return_value=fake_reference) - prefix = mocker.MagicMock() + prefix = "..." from openapi_python_client.parser import properties @@ -407,14 +478,23 @@ def test_get_imports(self, mocker): ) assert enum_property.get_imports(prefix=prefix) == { - f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}" + f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", } enum_property.required = False assert enum_property.get_imports(prefix=prefix) == { f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", - "from typing import Optional", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", } + + enum_property.nullable = True + assert enum_property.get_imports(prefix=prefix) == { + f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", + } + properties._existing_enums = {} def test_values_from_list(self): @@ -507,11 +587,14 @@ def test_get_type_string(self, mocker): assert ref_property.get_type_string() == "MyRefClass" ref_property.required = False + assert ref_property.get_type_string() == "MyRefClass" + + ref_property.nullable = True assert ref_property.get_type_string() == "Optional[MyRefClass]" def test_get_imports(self, mocker): fake_reference = mocker.MagicMock(class_name="MyRefClass", module_name="my_test_enum") - prefix = mocker.MagicMock() + prefix = "..." from openapi_python_client.parser.properties import RefProperty @@ -528,7 +611,17 @@ def test_get_imports(self, mocker): f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", "from typing import Dict", "from typing import cast", - "from typing import Optional", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", + } + + p.nullable = True + assert p.get_imports(prefix=prefix) == { + f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", + "from typing import Dict", + "from typing import cast", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", } def test__validate_default(self, mocker): @@ -548,7 +641,7 @@ class TestDictProperty: def test_get_imports(self, mocker): from openapi_python_client.parser.properties import DictProperty - prefix = mocker.MagicMock() + prefix = "..." p = DictProperty(name="test", required=True, default=None, nullable=False) assert p.get_imports(prefix=prefix) == { "from typing import Dict", @@ -556,16 +649,25 @@ def test_get_imports(self, mocker): p.required = False assert p.get_imports(prefix=prefix) == { - "from typing import Optional", "from typing import Dict", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", + } + + p.nullable = False + assert p.get_imports(prefix=prefix) == { + "from typing import Dict", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", } p.default = mocker.MagicMock() assert p.get_imports(prefix=prefix) == { - "from typing import Optional", "from typing import Dict", "from typing import cast", "from dataclasses import field", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", } def test__validate_default(self): From 957ce461a31032d4a38d942ebeeec985cff2cb52 Mon Sep 17 00:00:00 2001 From: Ethan Mann Date: Mon, 2 Nov 2020 18:06:50 -0500 Subject: [PATCH 04/13] Updated Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 790f686fb..d9499d687 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Prefix generated identifiers to allow leading digits in field names (#206 - @kalzoo). ### Additions +- Better compatibility for "required" (whether or not the field must be included) and "nullable" (whether or not the field can be null) (#205 & #208). Thanks @bowenwr & @emannguitar! ## 0.6.1 - 2020-09-26 From fb4cb6f0a21af7acd7817a6a5c0b97779eb95669 Mon Sep 17 00:00:00 2001 From: Ethan Mann Date: Tue, 3 Nov 2020 11:39:36 -0500 Subject: [PATCH 05/13] Apply suggestions from code review Co-authored-by: Dylan Anthony <43723790+dbanty@users.noreply.github.com> --- openapi_python_client/__init__.py | 2 +- openapi_python_client/parser/properties.py | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/openapi_python_client/__init__.py b/openapi_python_client/__init__.py index 3df7594fc..2fcca23b3 100644 --- a/openapi_python_client/__init__.py +++ b/openapi_python_client/__init__.py @@ -18,7 +18,7 @@ from .utils import snake_case if sys.version_info.minor < 8: # version did not exist before 3.8, need to use a backport - from importlib_metadata import version # pragma: no cover + from importlib_metadata import version else: from importlib.metadata import version # type: ignore diff --git a/openapi_python_client/parser/properties.py b/openapi_python_client/parser/properties.py index 6fd09601f..adf3083fa 100644 --- a/openapi_python_client/parser/properties.py +++ b/openapi_python_client/parser/properties.py @@ -64,7 +64,7 @@ def get_imports(self, *, prefix: str) -> Set[str]: back to the root of the generated client. """ if self.nullable or not self.required: - return {"from typing import cast, Optional", f"from {prefix}types import UNSET, Unset"} + return {"from typing import Union, Optional", f"from {prefix}types import UNSET, Unset"} return set() def to_string(self) -> str: @@ -72,7 +72,7 @@ def to_string(self) -> str: if self.default: default = self.default elif not self.required: - default = f"cast({self.get_type_string()}, UNSET)" + default = "UNSET" elif self.nullable: default = "None" else: @@ -224,9 +224,12 @@ class ListProperty(Property, Generic[InnerProp]): def get_type_string(self, no_optional: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ - if no_optional or not self.nullable: - return f"List[{self.inner_property.get_type_string()}]" - return f"Optional[List[{self.inner_property.get_type_string()}]]" + type_string = f"List[{self.inner_property.get_type_string()}]" + if not no_optional and self.nullable: + type_string = f"Optional[{type_string}]" + if not self.required: + type_string = f"Union[Unset, {type_string}]" + return type_string def get_imports(self, *, prefix: str) -> Set[str]: """ From 8f7d51b21621b75cc03c5a8e5282915655810d0a Mon Sep 17 00:00:00 2001 From: Ethan Mann Date: Tue, 3 Nov 2020 12:37:44 -0500 Subject: [PATCH 06/13] Removed to_dict params and cleaned up type strings --- .../api/tests/defaults_tests_defaults_post.py | 94 +++++++++---------- .../tests/json_body_tests_json_body_post.py | 2 +- ...tional_value_tests_optional_query_param.py | 12 +-- .../tests/upload_file_tests_upload_post.py | 12 +-- .../my_test_api_client/models/a_model.py | 52 ++++------ .../body_upload_file_tests_upload_post.py | 28 +----- .../models/http_validation_error.py | 38 ++------ .../models/validation_error.py | 28 +----- .../golden-record/my_test_api_client/types.py | 10 +- openapi_python_client/parser/properties.py | 49 ++++++---- openapi_python_client/templates/model.pyi | 35 +++---- .../property_templates/ref_property.pyi | 8 +- .../property_templates/union_property.pyi | 4 +- openapi_python_client/templates/types.py | 5 +- 14 files changed, 157 insertions(+), 220 deletions(-) diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py index 15fc274bb..effc7b57d 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py @@ -7,22 +7,22 @@ from ...client import Client from ...models.an_enum import AnEnum from ...models.http_validation_error import HTTPValidationError -from ...types import UNSET, Response +from ...types import UNSET, Response, Unset def _get_kwargs( *, client: Client, json_body: Dict[Any, Any], - string_prop: str = "the default string", - datetime_prop: datetime.datetime = isoparse("1010-10-10T00:00:00"), - date_prop: datetime.date = isoparse("1010-10-10").date(), - float_prop: float = 3.14, - int_prop: int = 7, - boolean_prop: bool = cast(bool, UNSET), - list_prop: List[AnEnum] = cast(List[AnEnum], UNSET), - union_prop: Union[float, str] = "not a float", - enum_prop: AnEnum = cast(AnEnum, UNSET), + string_prop: Union[Unset, str] = "the default string", + datetime_prop: Union[Unset, datetime.datetime] = isoparse("1010-10-10T00:00:00"), + date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), + float_prop: Union[Unset, float] = 3.14, + int_prop: Union[Unset, int] = 7, + boolean_prop: Union[Unset, bool] = UNSET, + list_prop: Union[Unset, List[AnEnum]] = UNSET, + union_prop: Union[Unset, Union[Union[Unset, float], Union[Unset, str]]] = "not a float", + enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Dict[str, Any]: url = "{}/tests/defaults".format(client.base_url) @@ -48,7 +48,7 @@ def _get_kwargs( json_list_prop.append(list_prop_item) if union_prop is UNSET: - json_union_prop: Union[float, str] = UNSET + json_union_prop: Union[Unset, Union[Union[Unset, float], Union[Unset, str]]] = UNSET elif isinstance(union_prop, float): json_union_prop = union_prop else: @@ -112,15 +112,15 @@ def sync_detailed( *, client: Client, json_body: Dict[Any, Any], - string_prop: str = "the default string", - datetime_prop: datetime.datetime = isoparse("1010-10-10T00:00:00"), - date_prop: datetime.date = isoparse("1010-10-10").date(), - float_prop: float = 3.14, - int_prop: int = 7, - boolean_prop: bool = cast(bool, UNSET), - list_prop: List[AnEnum] = cast(List[AnEnum], UNSET), - union_prop: Union[float, str] = "not a float", - enum_prop: AnEnum = cast(AnEnum, UNSET), + string_prop: Union[Unset, str] = "the default string", + datetime_prop: Union[Unset, datetime.datetime] = isoparse("1010-10-10T00:00:00"), + date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), + float_prop: Union[Unset, float] = 3.14, + int_prop: Union[Unset, int] = 7, + boolean_prop: Union[Unset, bool] = UNSET, + list_prop: Union[Unset, List[AnEnum]] = UNSET, + union_prop: Union[Unset, Union[Union[Unset, float], Union[Unset, str]]] = "not a float", + enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, @@ -147,15 +147,15 @@ def sync( *, client: Client, json_body: Dict[Any, Any], - string_prop: str = "the default string", - datetime_prop: datetime.datetime = isoparse("1010-10-10T00:00:00"), - date_prop: datetime.date = isoparse("1010-10-10").date(), - float_prop: float = 3.14, - int_prop: int = 7, - boolean_prop: bool = cast(bool, UNSET), - list_prop: List[AnEnum] = cast(List[AnEnum], UNSET), - union_prop: Union[float, str] = "not a float", - enum_prop: AnEnum = cast(AnEnum, UNSET), + string_prop: Union[Unset, str] = "the default string", + datetime_prop: Union[Unset, datetime.datetime] = isoparse("1010-10-10T00:00:00"), + date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), + float_prop: Union[Unset, float] = 3.14, + int_prop: Union[Unset, int] = 7, + boolean_prop: Union[Unset, bool] = UNSET, + list_prop: Union[Unset, List[AnEnum]] = UNSET, + union_prop: Union[Unset, Union[Union[Unset, float], Union[Unset, str]]] = "not a float", + enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Optional[Union[None, HTTPValidationError]]: """ """ @@ -178,15 +178,15 @@ async def asyncio_detailed( *, client: Client, json_body: Dict[Any, Any], - string_prop: str = "the default string", - datetime_prop: datetime.datetime = isoparse("1010-10-10T00:00:00"), - date_prop: datetime.date = isoparse("1010-10-10").date(), - float_prop: float = 3.14, - int_prop: int = 7, - boolean_prop: bool = cast(bool, UNSET), - list_prop: List[AnEnum] = cast(List[AnEnum], UNSET), - union_prop: Union[float, str] = "not a float", - enum_prop: AnEnum = cast(AnEnum, UNSET), + string_prop: Union[Unset, str] = "the default string", + datetime_prop: Union[Unset, datetime.datetime] = isoparse("1010-10-10T00:00:00"), + date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), + float_prop: Union[Unset, float] = 3.14, + int_prop: Union[Unset, int] = 7, + boolean_prop: Union[Unset, bool] = UNSET, + list_prop: Union[Unset, List[AnEnum]] = UNSET, + union_prop: Union[Unset, Union[Union[Unset, float], Union[Unset, str]]] = "not a float", + enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, @@ -212,15 +212,15 @@ async def asyncio( *, client: Client, json_body: Dict[Any, Any], - string_prop: str = "the default string", - datetime_prop: datetime.datetime = isoparse("1010-10-10T00:00:00"), - date_prop: datetime.date = isoparse("1010-10-10").date(), - float_prop: float = 3.14, - int_prop: int = 7, - boolean_prop: bool = cast(bool, UNSET), - list_prop: List[AnEnum] = cast(List[AnEnum], UNSET), - union_prop: Union[float, str] = "not a float", - enum_prop: AnEnum = cast(AnEnum, UNSET), + string_prop: Union[Unset, str] = "the default string", + datetime_prop: Union[Unset, datetime.datetime] = isoparse("1010-10-10T00:00:00"), + date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), + float_prop: Union[Unset, float] = 3.14, + int_prop: Union[Unset, int] = 7, + boolean_prop: Union[Unset, bool] = UNSET, + list_prop: Union[Unset, List[AnEnum]] = UNSET, + union_prop: Union[Unset, Union[Union[Unset, float], Union[Unset, str]]] = "not a float", + enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Optional[Union[None, HTTPValidationError]]: """ """ diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py index 5edf4c025..eb556c5d7 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py @@ -17,7 +17,7 @@ def _get_kwargs( headers: Dict[str, Any] = client.get_headers() - json_json_body = json_body.to_dict(exclude_unset=True) + json_json_body = json_body.to_dict() return { "url": url, diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py index 91b9fb0b1..10c9d8d3a 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py @@ -4,13 +4,13 @@ from ...client import Client from ...models.http_validation_error import HTTPValidationError -from ...types import UNSET, Response +from ...types import UNSET, Response, Unset def _get_kwargs( *, client: Client, - query_param: List[str] = cast(List[str], UNSET), + query_param: Union[Unset, List[str]] = UNSET, ) -> Dict[str, Any]: url = "{}/tests/optional_query_param/".format(client.base_url) @@ -54,7 +54,7 @@ def _build_response(*, response: httpx.Response) -> Response[Union[None, HTTPVal def sync_detailed( *, client: Client, - query_param: List[str] = cast(List[str], UNSET), + query_param: Union[Unset, List[str]] = UNSET, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, @@ -71,7 +71,7 @@ def sync_detailed( def sync( *, client: Client, - query_param: List[str] = cast(List[str], UNSET), + query_param: Union[Unset, List[str]] = UNSET, ) -> Optional[Union[None, HTTPValidationError]]: """ Test optional query parameters """ @@ -84,7 +84,7 @@ def sync( async def asyncio_detailed( *, client: Client, - query_param: List[str] = cast(List[str], UNSET), + query_param: Union[Unset, List[str]] = UNSET, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, @@ -100,7 +100,7 @@ async def asyncio_detailed( async def asyncio( *, client: Client, - query_param: List[str] = cast(List[str], UNSET), + query_param: Union[Unset, List[str]] = UNSET, ) -> Optional[Union[None, HTTPValidationError]]: """ Test optional query parameters """ diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py index 3c228b5db..d0b31d9f7 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py @@ -5,14 +5,14 @@ from ...client import Client from ...models.body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost from ...models.http_validation_error import HTTPValidationError -from ...types import UNSET, Response +from ...types import UNSET, Response, Unset def _get_kwargs( *, client: Client, multipart_data: BodyUploadFileTestsUploadPost, - keep_alive: bool = cast(bool, UNSET), + keep_alive: Union[Unset, bool] = UNSET, ) -> Dict[str, Any]: url = "{}/tests/upload".format(client.base_url) @@ -51,7 +51,7 @@ def sync_detailed( *, client: Client, multipart_data: BodyUploadFileTestsUploadPost, - keep_alive: bool = cast(bool, UNSET), + keep_alive: Union[Unset, bool] = UNSET, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, @@ -70,7 +70,7 @@ def sync( *, client: Client, multipart_data: BodyUploadFileTestsUploadPost, - keep_alive: bool = cast(bool, UNSET), + keep_alive: Union[Unset, bool] = UNSET, ) -> Optional[Union[None, HTTPValidationError]]: """ Upload a file """ @@ -85,7 +85,7 @@ async def asyncio_detailed( *, client: Client, multipart_data: BodyUploadFileTestsUploadPost, - keep_alive: bool = cast(bool, UNSET), + keep_alive: Union[Unset, bool] = UNSET, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, @@ -103,7 +103,7 @@ async def asyncio( *, client: Client, multipart_data: BodyUploadFileTestsUploadPost, - keep_alive: bool = cast(bool, UNSET), + keep_alive: Union[Unset, bool] = UNSET, ) -> Optional[Union[None, HTTPValidationError]]: """ Upload a file """ 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 111aeffee..39f6dac0d 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,12 +1,12 @@ import datetime -from typing import Any, Dict, List, Optional, Set, Union, cast +from typing import Any, Dict, List, Optional, Union import attr from dateutil.parser import isoparse from ..models.an_enum import AnEnum from ..models.different_enum import DifferentEnum -from ..types import UNSET +from ..types import UNSET, Unset @attr.s(auto_attribs=True) @@ -17,20 +17,14 @@ class AModel: a_camel_date_time: Union[datetime.datetime, datetime.date] a_date: datetime.date required_not_nullable: str - nested_list_of_enums: List[List[DifferentEnum]] = cast(List[List[DifferentEnum]], UNSET) + nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET some_dict: Optional[Dict[Any, Any]] = None - attr_1_leading_digit: str = cast(str, UNSET) + attr_1_leading_digit: Union[Unset, str] = UNSET required_nullable: Optional[str] = None - not_required_nullable: Optional[str] = cast(Optional[str], UNSET) - not_required_not_nullable: str = cast(str, UNSET) - - def to_dict( - self, - include: Optional[Set[str]] = None, - exclude: Optional[Set[str]] = None, - exclude_unset: bool = False, - exclude_none: bool = False, - ) -> Dict[str, Any]: + not_required_nullable: Union[Unset, Optional[str]] = UNSET + not_required_not_nullable: Union[Unset, str] = UNSET + + def to_dict(self) -> Dict[str, Any]: an_enum_value = self.an_enum_value.value if isinstance(self.a_camel_date_time, datetime.datetime): @@ -64,32 +58,24 @@ def to_dict( not_required_nullable = self.not_required_nullable not_required_not_nullable = self.not_required_not_nullable - all_properties = { + field_dict = { "an_enum_value": an_enum_value, "aCamelDateTime": a_camel_date_time, "a_date": a_date, "required_not_nullable": required_not_nullable, - "nested_list_of_enums": nested_list_of_enums, "some_dict": some_dict, - "1_leading_digit": attr_1_leading_digit, "required_nullable": required_nullable, - "not_required_nullable": not_required_nullable, - "not_required_not_nullable": not_required_not_nullable, } - - trimmed_properties: Dict[str, Any] = {} - for property_name, property_value in all_properties.items(): - if include is not None and property_name not in include: - continue - if exclude is not None and property_name in exclude: - continue - if exclude_unset and property_value is UNSET: - continue - if exclude_none and property_value is None: - continue - trimmed_properties[property_name] = property_value - - return trimmed_properties + if nested_list_of_enums is not UNSET: + field_dict["nested_list_of_enums"] = nested_list_of_enums + if attr_1_leading_digit is not UNSET: + field_dict["1_leading_digit"] = attr_1_leading_digit + if not_required_nullable is not UNSET: + field_dict["not_required_nullable"] = not_required_nullable + if not_required_not_nullable is not UNSET: + field_dict["not_required_not_nullable"] = not_required_not_nullable + + return field_dict @staticmethod def from_dict(d: Dict[str, Any]) -> "AModel": diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py index a6899351e..3435bd290 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py @@ -1,8 +1,8 @@ -from typing import Any, Dict, Optional, Set +from typing import Any, Dict import attr -from ..types import UNSET, File +from ..types import File @attr.s(auto_attribs=True) @@ -11,32 +11,14 @@ class BodyUploadFileTestsUploadPost: some_file: File - def to_dict( - self, - include: Optional[Set[str]] = None, - exclude: Optional[Set[str]] = None, - exclude_unset: bool = False, - exclude_none: bool = False, - ) -> Dict[str, Any]: + def to_dict(self) -> Dict[str, Any]: some_file = self.some_file.to_tuple() - all_properties = { + field_dict = { "some_file": some_file, } - trimmed_properties: Dict[str, Any] = {} - for property_name, property_value in all_properties.items(): - if include is not None and property_name not in include: - continue - if exclude is not None and property_name in exclude: - continue - if exclude_unset and property_value is UNSET: - continue - if exclude_none and property_value is None: - continue - trimmed_properties[property_name] = property_value - - return trimmed_properties + return field_dict @staticmethod def from_dict(d: Dict[str, Any]) -> "BodyUploadFileTestsUploadPost": diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py b/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py index fe5ec3b99..aa0d796ed 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py @@ -1,51 +1,33 @@ -from typing import Any, Dict, List, Optional, Set, cast +from typing import Any, Dict, List, Union import attr from ..models.validation_error import ValidationError -from ..types import UNSET +from ..types import UNSET, Unset @attr.s(auto_attribs=True) class HTTPValidationError: """ """ - detail: List[ValidationError] = cast(List[ValidationError], UNSET) + detail: Union[Unset, List[ValidationError]] = UNSET - def to_dict( - self, - include: Optional[Set[str]] = None, - exclude: Optional[Set[str]] = None, - exclude_unset: bool = False, - exclude_none: bool = False, - ) -> Dict[str, Any]: + def to_dict(self) -> Dict[str, Any]: if self.detail is UNSET: detail = UNSET else: detail = [] for detail_item_data in self.detail: - detail_item = detail_item_data.to_dict(exclude_unset=True) + detail_item = detail_item_data.to_dict() detail.append(detail_item) - all_properties = { - "detail": detail, - } - - trimmed_properties: Dict[str, Any] = {} - for property_name, property_value in all_properties.items(): - if include is not None and property_name not in include: - continue - if exclude is not None and property_name in exclude: - continue - if exclude_unset and property_value is UNSET: - continue - if exclude_none and property_value is None: - continue - trimmed_properties[property_name] = property_value - - return trimmed_properties + field_dict = {} + if detail is not UNSET: + field_dict["detail"] = detail + + return field_dict @staticmethod def from_dict(d: Dict[str, Any]) -> "HTTPValidationError": diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py b/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py index de98717fc..9f5fe0fe0 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py @@ -1,9 +1,7 @@ -from typing import Any, Dict, List, Optional, Set +from typing import Any, Dict, List import attr -from ..types import UNSET - @attr.s(auto_attribs=True) class ValidationError: @@ -13,38 +11,20 @@ class ValidationError: msg: str type: str - def to_dict( - self, - include: Optional[Set[str]] = None, - exclude: Optional[Set[str]] = None, - exclude_unset: bool = False, - exclude_none: bool = False, - ) -> Dict[str, Any]: + def to_dict(self) -> Dict[str, Any]: loc = self.loc msg = self.msg type = self.type - all_properties = { + field_dict = { "loc": loc, "msg": msg, "type": type, } - trimmed_properties: Dict[str, Any] = {} - for property_name, property_value in all_properties.items(): - if include is not None and property_name not in include: - continue - if exclude is not None and property_name in exclude: - continue - if exclude_unset and property_value is UNSET: - continue - if exclude_none and property_value is None: - continue - trimmed_properties[property_name] = property_value - - return trimmed_properties + return field_dict @staticmethod def from_dict(d: Dict[str, Any]) -> "ValidationError": diff --git a/end_to_end_tests/golden-record/my_test_api_client/types.py b/end_to_end_tests/golden-record/my_test_api_client/types.py index 4d2d3c4f5..7f0f544d8 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/types.py +++ b/end_to_end_tests/golden-record/my_test_api_client/types.py @@ -1,10 +1,14 @@ """ Contains some shared types for properties """ -from typing import Any, BinaryIO, Generic, MutableMapping, NewType, Optional, TextIO, Tuple, TypeVar, Union +from typing import BinaryIO, Generic, MutableMapping, Optional, TextIO, Tuple, TypeVar, Union import attr -Unset = NewType("Unset", object) -UNSET: Any = Unset(object()) + +class Unset: + pass + + +UNSET: Unset = Unset() @attr.s(auto_attribs=True) diff --git a/openapi_python_client/parser/properties.py b/openapi_python_client/parser/properties.py index adf3083fa..80690a659 100644 --- a/openapi_python_client/parser/properties.py +++ b/openapi_python_client/parser/properties.py @@ -44,16 +44,19 @@ def _validate_default(self, default: Any) -> Any: """ Check that the default value is valid for the property's type + perform any necessary sanitization """ raise ValidationError - def get_type_string(self, no_optional: bool = False) -> str: + def get_type_string(self, no_optional: bool = False, no_unset: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property Args: no_optional: Do not include Optional even if the value is optional (needed for isinstance checks) """ - if no_optional or not self.nullable: - return self._type_string - return f"Optional[{self._type_string}]" + type_string = self._type_string + if not no_optional and self.nullable: + type_string = f"Optional[{type_string}]" + if not no_unset and not self.required: + type_string = f"Union[Unset, {type_string}]" + return type_string def get_imports(self, *, prefix: str) -> Set[str]: """ @@ -222,12 +225,12 @@ class ListProperty(Property, Generic[InnerProp]): inner_property: InnerProp template: ClassVar[str] = "list_property.pyi" - def get_type_string(self, no_optional: bool = False) -> str: + def get_type_string(self, no_optional: bool = False, no_unset: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ type_string = f"List[{self.inner_property.get_type_string()}]" if not no_optional and self.nullable: type_string = f"Optional[{type_string}]" - if not self.required: + if not no_unset and not self.required: type_string = f"Union[Unset, {type_string}]" return type_string @@ -255,13 +258,16 @@ class UnionProperty(Property): inner_properties: List[Property] template: ClassVar[str] = "union_property.pyi" - def get_type_string(self, no_optional: bool = False) -> str: + def get_type_string(self, no_optional: bool = False, no_unset: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ inner_types = [p.get_type_string() for p in self.inner_properties] inner_prop_string = ", ".join(inner_types) - if no_optional or not self.nullable: - return f"Union[{inner_prop_string}]" - return f"Optional[Union[{inner_prop_string}]]" + type_string = f"Union[{inner_prop_string}]" + if not no_optional and self.nullable: + type_string = f"Optional[{type_string}]" + if not no_unset and not self.required: + type_string = f"Union[Unset, {type_string}]" + return type_string def get_imports(self, *, prefix: str) -> Set[str]: """ @@ -331,12 +337,14 @@ def get_enum(name: str) -> Optional["EnumProperty"]: """ Get all the EnumProperties that have been registered keyed by class name """ return _existing_enums.get(name) - def get_type_string(self, no_optional: bool = False) -> str: + def get_type_string(self, no_optional: bool = False, no_unset: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ - - if no_optional or not self.nullable: - return self.reference.class_name - return f"Optional[{self.reference.class_name}]" + type_string = self.reference.class_name + if not no_optional and self.nullable: + type_string = f"Optional[{type_string}]" + if not no_unset and not self.required: + type_string = f"Union[Unset, {type_string}]" + return type_string def get_imports(self, *, prefix: str) -> Set[str]: """ @@ -393,11 +401,14 @@ def template(self) -> str: # type: ignore return "enum_property.pyi" return "ref_property.pyi" - def get_type_string(self, no_optional: bool = False) -> str: + def get_type_string(self, no_optional: bool = False, no_unset: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ - if no_optional or not self.nullable: - return self.reference.class_name - return f"Optional[{self.reference.class_name}]" + type_string = self.reference.class_name + if not no_optional and self.nullable: + type_string = f"Optional[{type_string}]" + if not no_unset and not self.required: + type_string = f"Union[Unset, {type_string}]" + return type_string def get_imports(self, *, prefix: str) -> Set[str]: """ diff --git a/openapi_python_client/templates/model.pyi b/openapi_python_client/templates/model.pyi index 8583bb3f0..1bfcc81f0 100644 --- a/openapi_python_client/templates/model.pyi +++ b/openapi_python_client/templates/model.pyi @@ -2,7 +2,7 @@ from typing import Any, Dict, Optional, Set import attr -from ..types import UNSET +from ..types import UNSET, Unset {% for relative in model.relative_imports %} {{ relative }} @@ -16,13 +16,7 @@ class {{ model.reference.class_name }}: {{ property.to_string() }} {% endfor %} - def to_dict( - self, - include: Optional[Set[str]] = None, - exclude: Optional[Set[str]] = None, - exclude_unset: bool = False, - exclude_none: bool = False, - ) -> Dict[str, Any]: + def to_dict(self) -> Dict[str, Any]: {% for property in model.required_properties + model.optional_properties %} {% if property.template %} {% from "property_templates/" + property.template import transform %} @@ -32,25 +26,22 @@ class {{ model.reference.class_name }}: {% endif %} {% endfor %} - all_properties = { + + field_dict = { {% for property in model.required_properties + model.optional_properties %} + {% if property.required %} "{{ property.name }}": {{ property.python_name }}, + {% endif %} {% endfor %} } + {% for property in model.optional_properties %} + {% if not property.required %} + if {{ property.python_name }} is not UNSET: + field_dict["{{ property.name }}"] = {{ property.python_name }} + {% endif %} + {% endfor %} - trimmed_properties: Dict[str, Any] = {} - for property_name, property_value in all_properties.items(): - if include is not None and property_name not in include: - continue - if exclude is not None and property_name in exclude: - continue - if exclude_unset and property_value is UNSET: - continue - if exclude_none and property_value is None: - continue - trimmed_properties[property_name] = property_value - - return trimmed_properties + return field_dict @staticmethod def from_dict(d: Dict[str, Any]) -> "{{ model.reference.class_name }}": diff --git a/openapi_python_client/templates/property_templates/ref_property.pyi b/openapi_python_client/templates/property_templates/ref_property.pyi index b43c8ea71..90990a23e 100644 --- a/openapi_python_client/templates/property_templates/ref_property.pyi +++ b/openapi_python_client/templates/property_templates/ref_property.pyi @@ -11,19 +11,19 @@ if {{ source }} is not None: {% macro transform(property, source, destination) %} {% if property.required %} {% if property.nullable %} -{{ destination }} = {{ source }}.to_dict(exclude_unset=True) if {{ source }} else None +{{ destination }} = {{ source }}.to_dict() if {{ source }} else None {% else %} -{{ destination }} = {{ source }}.to_dict(exclude_unset=True) +{{ destination }} = {{ source }}.to_dict() {% endif %} {% else %} if {{ source }} is UNSET: {{ destination }} = UNSET {% if property.nullable %} else: - {{ destination }} = {{ source }}.to_dict(exclude_unset=True) if {{ source }} else None + {{ destination }} = {{ source }}.to_dict() if {{ source }} else None {% else %} else: - {{ destination }} = {{ source }}.to_dict(exclude_unset=True) + {{ destination }} = {{ source }}.to_dict() {% endif %} {% endif %} {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/union_property.pyi b/openapi_python_client/templates/property_templates/union_property.pyi index f74604294..710e2d1bb 100644 --- a/openapi_python_client/templates/property_templates/union_property.pyi +++ b/openapi_python_client/templates/property_templates/union_property.pyi @@ -36,9 +36,9 @@ elif {{ source }} is None: {% endif %} {% for inner_property in property.inner_properties %} {% if loop.first and property.required and not property.nullable %}{# No if UNSET or if None statement before this #} -if isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True) }}): +if isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True, no_unset=True) }}): {% elif not loop.last %} -elif isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True) }}): +elif isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True, no_unset=True) }}): {% else %} else: {% endif %} diff --git a/openapi_python_client/templates/types.py b/openapi_python_client/templates/types.py index 4d2d3c4f5..99a344e09 100644 --- a/openapi_python_client/templates/types.py +++ b/openapi_python_client/templates/types.py @@ -3,8 +3,9 @@ import attr -Unset = NewType("Unset", object) -UNSET: Any = Unset(object()) +class Unset: + pass +UNSET: Unset = Unset() @attr.s(auto_attribs=True) From 961f6df374a9568253aa065e126dac7f718b4a58 Mon Sep 17 00:00:00 2001 From: Ethan Mann Date: Wed, 4 Nov 2020 13:29:40 -0500 Subject: [PATCH 07/13] Fixed typing issues (again) --- .../api/tests/defaults_tests_defaults_post.py | 35 +++-- ...tional_value_tests_optional_query_param.py | 5 +- .../my_test_api_client/models/a_model.py | 7 +- .../models/http_validation_error.py | 6 +- .../models/validation_error.py | 1 - openapi_python_client/parser/properties.py | 4 +- .../property_templates/date_property.pyi | 6 +- .../property_templates/datetime_property.pyi | 6 +- .../property_templates/enum_property.pyi | 6 +- .../property_templates/file_property.pyi | 6 +- .../property_templates/list_property.pyi | 16 +-- .../property_templates/ref_property.pyi | 6 +- .../property_templates/union_property.pyi | 5 +- openapi_python_client/templates/types.py | 5 +- tests/test_openapi_parser/test_properties.py | 129 +++++++++++------- 15 files changed, 129 insertions(+), 114 deletions(-) diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py index effc7b57d..a251e4a9e 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py @@ -21,42 +21,39 @@ def _get_kwargs( int_prop: Union[Unset, int] = 7, boolean_prop: Union[Unset, bool] = UNSET, list_prop: Union[Unset, List[AnEnum]] = UNSET, - union_prop: Union[Unset, Union[Union[Unset, float], Union[Unset, str]]] = "not a float", + union_prop: Union[Unset, Union[float, str]] = "not a float", enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Dict[str, Any]: url = "{}/tests/defaults".format(client.base_url) headers: Dict[str, Any] = client.get_headers() - if datetime_prop is UNSET: - json_datetime_prop = UNSET - else: + json_datetime_prop: Union[Unset, str] = UNSET + if not isinstance(datetime_prop, Unset): json_datetime_prop = datetime_prop.isoformat() - if date_prop is UNSET: - json_date_prop = UNSET - else: + json_date_prop: Union[Unset, str] = UNSET + if not isinstance(date_prop, Unset): json_date_prop = date_prop.isoformat() - if list_prop is UNSET: - json_list_prop = UNSET - else: + json_list_prop: Union[Unset, List[Any]] = UNSET + if not isinstance(list_prop, Unset): json_list_prop = [] for list_prop_item_data in list_prop: list_prop_item = list_prop_item_data.value json_list_prop.append(list_prop_item) - if union_prop is UNSET: - json_union_prop: Union[Unset, Union[Union[Unset, float], Union[Unset, str]]] = UNSET + json_union_prop: Union[Unset, Union[float, str]] + if isinstance(union_prop, Unset): + json_union_prop = UNSET elif isinstance(union_prop, float): json_union_prop = union_prop else: json_union_prop = union_prop - if enum_prop is UNSET: - json_enum_prop = UNSET - else: + json_enum_prop: Union[Unset, AnEnum] = UNSET + if not isinstance(enum_prop, Unset): json_enum_prop = enum_prop.value params: Dict[str, Any] = {} @@ -119,7 +116,7 @@ def sync_detailed( int_prop: Union[Unset, int] = 7, boolean_prop: Union[Unset, bool] = UNSET, list_prop: Union[Unset, List[AnEnum]] = UNSET, - union_prop: Union[Unset, Union[Union[Unset, float], Union[Unset, str]]] = "not a float", + union_prop: Union[Unset, Union[float, str]] = "not a float", enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( @@ -154,7 +151,7 @@ def sync( int_prop: Union[Unset, int] = 7, boolean_prop: Union[Unset, bool] = UNSET, list_prop: Union[Unset, List[AnEnum]] = UNSET, - union_prop: Union[Unset, Union[Union[Unset, float], Union[Unset, str]]] = "not a float", + union_prop: Union[Unset, Union[float, str]] = "not a float", enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Optional[Union[None, HTTPValidationError]]: """ """ @@ -185,7 +182,7 @@ async def asyncio_detailed( int_prop: Union[Unset, int] = 7, boolean_prop: Union[Unset, bool] = UNSET, list_prop: Union[Unset, List[AnEnum]] = UNSET, - union_prop: Union[Unset, Union[Union[Unset, float], Union[Unset, str]]] = "not a float", + union_prop: Union[Unset, Union[float, str]] = "not a float", enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( @@ -219,7 +216,7 @@ async def asyncio( int_prop: Union[Unset, int] = 7, boolean_prop: Union[Unset, bool] = UNSET, list_prop: Union[Unset, List[AnEnum]] = UNSET, - union_prop: Union[Unset, Union[Union[Unset, float], Union[Unset, str]]] = "not a float", + union_prop: Union[Unset, Union[float, str]] = "not a float", enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Optional[Union[None, HTTPValidationError]]: """ """ diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py index 10c9d8d3a..519c543ac 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py @@ -16,9 +16,8 @@ def _get_kwargs( headers: Dict[str, Any] = client.get_headers() - if query_param is UNSET: - json_query_param = UNSET - else: + json_query_param: Union[Unset, List[Any]] = UNSET + if not isinstance(query_param, Unset): json_query_param = query_param params: Dict[str, Any] = {} 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 39f6dac0d..e223bd393 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 @@ -36,13 +36,10 @@ def to_dict(self) -> Dict[str, Any]: a_date = self.a_date.isoformat() required_not_nullable = self.required_not_nullable - - if self.nested_list_of_enums is UNSET: - nested_list_of_enums = UNSET - else: + nested_list_of_enums: Union[Unset, List[Any]] = UNSET + if not isinstance(self.nested_list_of_enums, Unset): nested_list_of_enums = [] for nested_list_of_enums_item_data in self.nested_list_of_enums: - nested_list_of_enums_item = [] for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: nested_list_of_enums_item_item = nested_list_of_enums_item_item_data.value diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py b/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py index aa0d796ed..9d29faa4d 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py @@ -13,10 +13,8 @@ class HTTPValidationError: detail: Union[Unset, List[ValidationError]] = UNSET def to_dict(self) -> Dict[str, Any]: - - if self.detail is UNSET: - detail = UNSET - else: + detail: Union[Unset, List[Any]] = UNSET + if not isinstance(self.detail, Unset): detail = [] for detail_item_data in self.detail: detail_item = detail_item_data.to_dict() diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py b/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py index 9f5fe0fe0..77b9239ef 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py @@ -12,7 +12,6 @@ class ValidationError: type: str def to_dict(self) -> Dict[str, Any]: - loc = self.loc msg = self.msg diff --git a/openapi_python_client/parser/properties.py b/openapi_python_client/parser/properties.py index 80690a659..603337f92 100644 --- a/openapi_python_client/parser/properties.py +++ b/openapi_python_client/parser/properties.py @@ -229,7 +229,7 @@ def get_type_string(self, no_optional: bool = False, no_unset: bool = False) -> """ Get a string representation of type that should be used when declaring this property """ type_string = f"List[{self.inner_property.get_type_string()}]" if not no_optional and self.nullable: - type_string = f"Optional[{type_string}]" + type_string = f"Optional[{type_string}]" if not no_unset and not self.required: type_string = f"Union[Unset, {type_string}]" return type_string @@ -260,7 +260,7 @@ class UnionProperty(Property): def get_type_string(self, no_optional: bool = False, no_unset: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ - inner_types = [p.get_type_string() for p in self.inner_properties] + inner_types = [p.get_type_string(no_unset=True) for p in self.inner_properties] inner_prop_string = ", ".join(inner_types) type_string = f"Union[{inner_prop_string}]" if not no_optional and self.nullable: diff --git a/openapi_python_client/templates/property_templates/date_property.pyi b/openapi_python_client/templates/property_templates/date_property.pyi index 66d7afe7a..39985f1eb 100644 --- a/openapi_python_client/templates/property_templates/date_property.pyi +++ b/openapi_python_client/templates/property_templates/date_property.pyi @@ -16,13 +16,11 @@ if {{ source }} is not None: {{ destination }} = {{ source }}.isoformat() {% endif %} {% else %} -if {{ source }} is UNSET: - {{ destination }} = UNSET +{{ destination }}: Union[Unset, str] = UNSET +if not isinstance({{ source }}, Unset): {% if property.nullable %} -else: {{ destination }} = {{ source }}.isoformat() if {{ source }} else None {% else %} -else: {{ destination }} = {{ source }}.isoformat() {% endif %} {% endif %} diff --git a/openapi_python_client/templates/property_templates/datetime_property.pyi b/openapi_python_client/templates/property_templates/datetime_property.pyi index 534327972..6eb772c54 100644 --- a/openapi_python_client/templates/property_templates/datetime_property.pyi +++ b/openapi_python_client/templates/property_templates/datetime_property.pyi @@ -16,13 +16,11 @@ if {{ source }} is not None: {{ destination }} = {{ source }}.isoformat() {% endif %} {% else %} -if {{ source }} is UNSET: - {{ destination }} = UNSET +{{ destination }}: Union[Unset, str] = UNSET +if not isinstance({{ source }}, Unset): {% if property.nullable %} -else: {{ destination }} = {{ source }}.isoformat() if {{ source }} else None {% else %} -else: {{ destination }} = {{ source }}.isoformat() {% endif %} {% endif %} diff --git a/openapi_python_client/templates/property_templates/enum_property.pyi b/openapi_python_client/templates/property_templates/enum_property.pyi index 3aa22fcae..f5a1f6aba 100644 --- a/openapi_python_client/templates/property_templates/enum_property.pyi +++ b/openapi_python_client/templates/property_templates/enum_property.pyi @@ -16,13 +16,11 @@ if {{ source }} is not None: {{ destination }} = {{ source }}.value {% endif %} {% else %} -if {{ source }} is UNSET: - {{ destination }} = UNSET +{{ destination }}: {{ property.get_type_string() }} = UNSET +if not isinstance({{ source }}, Unset): {% if property.nullable %} -else: {{ destination }} = {{ source }}.value if {{ source }} else None {% else %} -else: {{ destination }} = {{ source }}.value {% endif %} {% endif %} diff --git a/openapi_python_client/templates/property_templates/file_property.pyi b/openapi_python_client/templates/property_templates/file_property.pyi index 667d98d81..a66e81bd0 100644 --- a/openapi_python_client/templates/property_templates/file_property.pyi +++ b/openapi_python_client/templates/property_templates/file_property.pyi @@ -11,13 +11,11 @@ {{ destination }} = {{ source }}.to_tuple() {% endif %} {% else %} -if {{ source }} is UNSET: - {{ destination }} = UNSET +{{ destination }}: {{ property.get_type_string() }} = UNSET +if not isinstance({{ source }}, Unset): {% if property.nullable %} -else: {{ destination }} = {{ source }}.to_tuple() if {{ source }} else None {% else %} -else: {{ destination }} = {{ source }}.to_tuple() {% endif %} {% endif %} diff --git a/openapi_python_client/templates/property_templates/list_property.pyi b/openapi_python_client/templates/property_templates/list_property.pyi index e05d8b8d6..f0ad1f0b3 100644 --- a/openapi_python_client/templates/property_templates/list_property.pyi +++ b/openapi_python_client/templates/property_templates/list_property.pyi @@ -33,8 +33,6 @@ for {{ inner_source }} in {{ source }}: {% macro transform(property, source, destination) %} {% set inner_property = property.inner_property %} - - {% if property.required %} {% if property.nullable %} if {{ source }} is None: @@ -45,15 +43,17 @@ else: {{ _transform(property, source, destination) }} {% endif %} {% else %} -if {{ source }} is UNSET: - {{ destination }} = UNSET +{{ destination }}: Union[Unset, List[Any]] = UNSET +if not isinstance({{ source }}, Unset): {% if property.nullable %} -elif {{ source }} is None: - {{ destination }} = None -{% endif %} -else: + if {{ source }} is None: + {{ destination }} = None + else: + {{ _transform(property, source, destination) | indent(4)}} +{% else %} {{ _transform(property, source, destination) | indent(4)}} {% endif %} +{% endif %} {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/ref_property.pyi b/openapi_python_client/templates/property_templates/ref_property.pyi index 90990a23e..c1fa0a3bb 100644 --- a/openapi_python_client/templates/property_templates/ref_property.pyi +++ b/openapi_python_client/templates/property_templates/ref_property.pyi @@ -16,13 +16,11 @@ if {{ source }} is not None: {{ destination }} = {{ source }}.to_dict() {% endif %} {% else %} -if {{ source }} is UNSET: - {{ destination }} = UNSET +{{ destination }}: {{ property.get_type_string() }} = UNSET +if not isinstance({{ source }}, Unset): {% if property.nullable %} -else: {{ destination }} = {{ source }}.to_dict() if {{ source }} else None {% else %} -else: {{ destination }} = {{ source }}.to_dict() {% endif %} {% endif %} diff --git a/openapi_python_client/templates/property_templates/union_property.pyi b/openapi_python_client/templates/property_templates/union_property.pyi index 710e2d1bb..036025079 100644 --- a/openapi_python_client/templates/property_templates/union_property.pyi +++ b/openapi_python_client/templates/property_templates/union_property.pyi @@ -23,8 +23,9 @@ def _parse_{{ property.python_name }}(data: Dict[str, Any]) -> {{ property.get_t {% macro transform(property, source, destination) %} {% if not property.required %} -if {{ source }} is UNSET: - {{ destination }}: {{ property.get_type_string() }} = UNSET +{{ destination }}: {{ property.get_type_string() }} +if isinstance({{ source }}, Unset): + {{ destination }} = UNSET {% endif %} {% if property.nullable %} {% if property.required %} diff --git a/openapi_python_client/templates/types.py b/openapi_python_client/templates/types.py index 99a344e09..7f0f544d8 100644 --- a/openapi_python_client/templates/types.py +++ b/openapi_python_client/templates/types.py @@ -1,10 +1,13 @@ """ Contains some shared types for properties """ -from typing import Any, BinaryIO, Generic, MutableMapping, NewType, Optional, TextIO, Tuple, TypeVar, Union +from typing import BinaryIO, Generic, MutableMapping, Optional, TextIO, Tuple, TypeVar, Union import attr + class Unset: pass + + UNSET: Unset = Unset() diff --git a/tests/test_openapi_parser/test_properties.py b/tests/test_openapi_parser/test_properties.py index ff8332821..f666b4774 100644 --- a/tests/test_openapi_parser/test_properties.py +++ b/tests/test_openapi_parser/test_properties.py @@ -24,14 +24,18 @@ def test_get_type_string(self): p = Property(name="test", required=True, default=None, nullable=False) p._type_string = "TestType" - assert p.get_type_string() == "TestType" - p.required = False - assert p.get_type_string() == "TestType" - assert p.get_type_string(True) == "TestType" + base_type_string = f"TestType" + + assert p.get_type_string() == base_type_string - p.required = False p.nullable = True - assert p.get_type_string() == "Optional[TestType]" + assert p.get_type_string() == f"Optional[{base_type_string}]" + + p.required = False + assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" + + p.nullable = False + assert p.get_type_string() == f"Union[Unset, {base_type_string}]" def test_to_string(self, mocker): from openapi_python_client.parser.properties import Property @@ -43,7 +47,7 @@ def test_to_string(self, mocker): assert p.to_string() == f"{name}: {get_type_string()}" p.required = False - assert p.to_string() == f"{name}: {get_type_string()} = cast({get_type_string()}, UNSET)" + assert p.to_string() == f"{name}: {get_type_string()} = UNSET" p.required = True p.nullable = True @@ -59,7 +63,7 @@ def test_get_imports(self): assert p.get_imports(prefix="") == set() p.required = False - assert p.get_imports(prefix="") == {"from typing import cast, Optional", "from types import UNSET, Unset"} + assert p.get_imports(prefix="") == {"from typing import Union, Optional", "from types import UNSET, Unset"} def test__validate_default(self): from openapi_python_client.parser.properties import Property @@ -80,11 +84,18 @@ def test_get_type_string(self): p = StringProperty(name="test", required=True, default=None, nullable=False) - assert p.get_type_string() == "str" - p.required = False - assert p.get_type_string() == "str" + base_type_string = f"str" + + assert p.get_type_string() == base_type_string + p.nullable = True - assert p.get_type_string() == "Optional[str]" + assert p.get_type_string() == f"Optional[{base_type_string}]" + + p.required = False + assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" + + p.nullable = False + assert p.get_type_string() == f"Union[Unset, {base_type_string}]" def test__validate_default(self): from openapi_python_client.parser.properties import StringProperty @@ -109,7 +120,7 @@ def test_get_imports(self): "import datetime", "from typing import cast", "from dateutil.parser import isoparse", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } @@ -118,7 +129,7 @@ def test_get_imports(self): "import datetime", "from typing import cast", "from dateutil.parser import isoparse", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } @@ -148,7 +159,7 @@ def test_get_imports(self): "import datetime", "from typing import cast", "from dateutil.parser import isoparse", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } @@ -157,7 +168,7 @@ def test_get_imports(self): "import datetime", "from typing import cast", "from dateutil.parser import isoparse", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } @@ -184,14 +195,14 @@ def test_get_imports(self): p.required = False assert p.get_imports(prefix=prefix) == { "from ...types import File", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } p.nullable = True assert p.get_imports(prefix=prefix) == { "from ...types import File", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } @@ -253,13 +264,18 @@ def test_get_type_string(self, mocker): inner_property.get_type_string.return_value = inner_type_string p = ListProperty(name="test", required=True, default=None, inner_property=inner_property, nullable=False) - assert p.get_type_string() == f"List[{inner_type_string}]" + base_type_string = f"List[{inner_type_string}]" - p.required = False - assert p.get_type_string() == f"List[{inner_type_string}]" + assert p.get_type_string() == base_type_string p.nullable = True - assert p.get_type_string() == f"Optional[List[{inner_type_string}]]" + assert p.get_type_string() == f"Optional[{base_type_string}]" + + p.required = False + assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" + + p.nullable = False + assert p.get_type_string() == f"Union[Unset, {base_type_string}]" def test_get_type_imports(self, mocker): from openapi_python_client.parser.properties import ListProperty @@ -279,7 +295,7 @@ def test_get_type_imports(self, mocker): assert p.get_imports(prefix=prefix) == { inner_import, "from typing import List", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } @@ -287,7 +303,7 @@ def test_get_type_imports(self, mocker): assert p.get_imports(prefix=prefix) == { inner_import, "from typing import List", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } @@ -316,13 +332,18 @@ def test_get_type_string(self, mocker): nullable=False, ) - assert p.get_type_string() == "Union[inner_type_string_1, inner_type_string_2]" + base_type_string = f"Union[inner_type_string_1, inner_type_string_2]" - p.required = False - assert p.get_type_string() == "Union[inner_type_string_1, inner_type_string_2]" + assert p.get_type_string() == base_type_string p.nullable = True - assert p.get_type_string() == "Optional[Union[inner_type_string_1, inner_type_string_2]]" + assert p.get_type_string() == f"Optional[{base_type_string}]" + + p.required = False + assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" + + p.nullable = False + assert p.get_type_string() == f"Union[Unset, {base_type_string}]" def test_get_type_imports(self, mocker): from openapi_python_client.parser.properties import UnionProperty @@ -353,7 +374,7 @@ def test_get_type_imports(self, mocker): inner_import_1, inner_import_2, "from typing import Union", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } @@ -362,7 +383,7 @@ def test_get_type_imports(self, mocker): inner_import_1, inner_import_2, "from typing import Union", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } @@ -452,17 +473,22 @@ def test_get_type_string(self, mocker): from openapi_python_client.parser import properties - enum_property = properties.EnumProperty( + p = properties.EnumProperty( name="test", required=True, default=None, values={}, title="a_title", nullable=False ) - assert enum_property.get_type_string() == "MyTestEnum" + base_type_string = f"MyTestEnum" - enum_property.required = False - assert enum_property.get_type_string() == "MyTestEnum" + assert p.get_type_string() == base_type_string - enum_property.nullable = True - assert enum_property.get_type_string() == "Optional[MyTestEnum]" + p.nullable = True + assert p.get_type_string() == f"Optional[{base_type_string}]" + + p.required = False + assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" + + p.nullable = False + assert p.get_type_string() == f"Union[Unset, {base_type_string}]" properties._existing_enums = {} @@ -484,14 +510,14 @@ def test_get_imports(self, mocker): enum_property.required = False assert enum_property.get_imports(prefix=prefix) == { f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } enum_property.nullable = True assert enum_property.get_imports(prefix=prefix) == { f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } @@ -576,7 +602,7 @@ def test_template(self, mocker): def test_get_type_string(self, mocker): from openapi_python_client.parser.properties import RefProperty - ref_property = RefProperty( + p = RefProperty( name="test", required=True, default=None, @@ -584,13 +610,18 @@ def test_get_type_string(self, mocker): nullable=False, ) - assert ref_property.get_type_string() == "MyRefClass" + base_type_string = f"MyRefClass" - ref_property.required = False - assert ref_property.get_type_string() == "MyRefClass" + assert p.get_type_string() == base_type_string - ref_property.nullable = True - assert ref_property.get_type_string() == "Optional[MyRefClass]" + p.nullable = True + assert p.get_type_string() == f"Optional[{base_type_string}]" + + p.required = False + assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" + + p.nullable = False + assert p.get_type_string() == f"Union[Unset, {base_type_string}]" def test_get_imports(self, mocker): fake_reference = mocker.MagicMock(class_name="MyRefClass", module_name="my_test_enum") @@ -611,7 +642,7 @@ def test_get_imports(self, mocker): f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", "from typing import Dict", "from typing import cast", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } @@ -620,7 +651,7 @@ def test_get_imports(self, mocker): f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", "from typing import Dict", "from typing import cast", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } @@ -650,14 +681,14 @@ def test_get_imports(self, mocker): p.required = False assert p.get_imports(prefix=prefix) == { "from typing import Dict", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } p.nullable = False assert p.get_imports(prefix=prefix) == { "from typing import Dict", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } @@ -666,7 +697,7 @@ def test_get_imports(self, mocker): "from typing import Dict", "from typing import cast", "from dataclasses import field", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } From 0ab3ddfc6109f7e65f65013b2780eed14d428a48 Mon Sep 17 00:00:00 2001 From: Ethan Mann Date: Fri, 6 Nov 2020 09:23:37 -0500 Subject: [PATCH 08/13] Apply suggestions from code review Co-authored-by: Dylan Anthony <43723790+dbanty@users.noreply.github.com> --- openapi_python_client/parser/properties.py | 38 +++++++++++-------- openapi_python_client/templates/model.pyi | 2 +- .../property_templates/union_property.pyi | 4 +- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/openapi_python_client/parser/properties.py b/openapi_python_client/parser/properties.py index 603337f92..7666827d4 100644 --- a/openapi_python_client/parser/properties.py +++ b/openapi_python_client/parser/properties.py @@ -44,17 +44,19 @@ def _validate_default(self, default: Any) -> Any: """ Check that the default value is valid for the property's type + perform any necessary sanitization """ raise ValidationError - def get_type_string(self, no_optional: bool = False, no_unset: bool = False) -> str: + def get_type_string(self, no_optional: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property Args: - no_optional: Do not include Optional even if the value is optional (needed for isinstance checks) + no_optional: Do not include Optional or Unset even if the value is optional (needed for isinstance checks) """ type_string = self._type_string - if not no_optional and self.nullable: + if no_optional: + return type_string + if self.nullable: type_string = f"Optional[{type_string}]" - if not no_unset and not self.required: + if not self.required: type_string = f"Union[Unset, {type_string}]" return type_string @@ -225,12 +227,14 @@ class ListProperty(Property, Generic[InnerProp]): inner_property: InnerProp template: ClassVar[str] = "list_property.pyi" - def get_type_string(self, no_optional: bool = False, no_unset: bool = False) -> str: + def get_type_string(self, no_optional: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ type_string = f"List[{self.inner_property.get_type_string()}]" - if not no_optional and self.nullable: + if no_optional: + return type_string + if self.nullable: type_string = f"Optional[{type_string}]" - if not no_unset and not self.required: + if not self.required: type_string = f"Union[Unset, {type_string}]" return type_string @@ -258,9 +262,9 @@ class UnionProperty(Property): inner_properties: List[Property] template: ClassVar[str] = "union_property.pyi" - def get_type_string(self, no_optional: bool = False, no_unset: bool = False) -> str: + def get_type_string(self, no_optional: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ - inner_types = [p.get_type_string(no_unset=True) for p in self.inner_properties] + inner_types = [p.get_type_string(no_optional=True) for p in self.inner_properties] inner_prop_string = ", ".join(inner_types) type_string = f"Union[{inner_prop_string}]" if not no_optional and self.nullable: @@ -337,12 +341,14 @@ def get_enum(name: str) -> Optional["EnumProperty"]: """ Get all the EnumProperties that have been registered keyed by class name """ return _existing_enums.get(name) - def get_type_string(self, no_optional: bool = False, no_unset: bool = False) -> str: + def get_type_string(self, no_optional: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ type_string = self.reference.class_name - if not no_optional and self.nullable: + if no_optional: + return type_string + if self.nullable: type_string = f"Optional[{type_string}]" - if not no_unset and not self.required: + if not self.required: type_string = f"Union[Unset, {type_string}]" return type_string @@ -401,12 +407,14 @@ def template(self) -> str: # type: ignore return "enum_property.pyi" return "ref_property.pyi" - def get_type_string(self, no_optional: bool = False, no_unset: bool = False) -> str: + def get_type_string(self, no_optional: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ type_string = self.reference.class_name - if not no_optional and self.nullable: + if no_optional: + return type_string + if self.nullable: type_string = f"Optional[{type_string}]" - if not no_unset and not self.required: + if not self.required: type_string = f"Union[Unset, {type_string}]" return type_string diff --git a/openapi_python_client/templates/model.pyi b/openapi_python_client/templates/model.pyi index 1bfcc81f0..df100e69d 100644 --- a/openapi_python_client/templates/model.pyi +++ b/openapi_python_client/templates/model.pyi @@ -1,4 +1,4 @@ -from typing import Any, Dict, Optional, Set +from typing import Any, Dict import attr diff --git a/openapi_python_client/templates/property_templates/union_property.pyi b/openapi_python_client/templates/property_templates/union_property.pyi index 036025079..ba53528d6 100644 --- a/openapi_python_client/templates/property_templates/union_property.pyi +++ b/openapi_python_client/templates/property_templates/union_property.pyi @@ -37,9 +37,9 @@ elif {{ source }} is None: {% endif %} {% for inner_property in property.inner_properties %} {% if loop.first and property.required and not property.nullable %}{# No if UNSET or if None statement before this #} -if isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True, no_unset=True) }}): +if isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True) }}): {% elif not loop.last %} -elif isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True, no_unset=True) }}): +elif isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True) }}): {% else %} else: {% endif %} From 9e9ebbe6adb0d12be94cef532f6cd49cf8991bda Mon Sep 17 00:00:00 2001 From: Ethan Mann Date: Fri, 6 Nov 2020 09:51:24 -0500 Subject: [PATCH 09/13] Cleaned up union type strings when property is not required + required but nullable properties no longer have a default value --- .../api/tests/defaults_tests_defaults_post.py | 22 +++++++++---------- .../my_test_api_client/models/a_model.py | 4 ++-- openapi_python_client/parser/properties.py | 12 +++++----- openapi_python_client/templates/model.pyi | 7 ++++++ tests/test_openapi_parser/test_properties.py | 22 ++++++++++++++++--- 5 files changed, 45 insertions(+), 22 deletions(-) diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py index a251e4a9e..0991b7f73 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py @@ -19,9 +19,9 @@ def _get_kwargs( date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), float_prop: Union[Unset, float] = 3.14, int_prop: Union[Unset, int] = 7, - boolean_prop: Union[Unset, bool] = UNSET, + boolean_prop: Union[Unset, bool] = False, list_prop: Union[Unset, List[AnEnum]] = UNSET, - union_prop: Union[Unset, Union[float, str]] = "not a float", + union_prop: Union[Unset, float, str] = "not a float", enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Dict[str, Any]: url = "{}/tests/defaults".format(client.base_url) @@ -44,7 +44,7 @@ def _get_kwargs( json_list_prop.append(list_prop_item) - json_union_prop: Union[Unset, Union[float, str]] + json_union_prop: Union[Unset, float, str] if isinstance(union_prop, Unset): json_union_prop = UNSET elif isinstance(union_prop, float): @@ -114,9 +114,9 @@ def sync_detailed( date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), float_prop: Union[Unset, float] = 3.14, int_prop: Union[Unset, int] = 7, - boolean_prop: Union[Unset, bool] = UNSET, + boolean_prop: Union[Unset, bool] = False, list_prop: Union[Unset, List[AnEnum]] = UNSET, - union_prop: Union[Unset, Union[float, str]] = "not a float", + union_prop: Union[Unset, float, str] = "not a float", enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( @@ -149,9 +149,9 @@ def sync( date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), float_prop: Union[Unset, float] = 3.14, int_prop: Union[Unset, int] = 7, - boolean_prop: Union[Unset, bool] = UNSET, + boolean_prop: Union[Unset, bool] = False, list_prop: Union[Unset, List[AnEnum]] = UNSET, - union_prop: Union[Unset, Union[float, str]] = "not a float", + union_prop: Union[Unset, float, str] = "not a float", enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Optional[Union[None, HTTPValidationError]]: """ """ @@ -180,9 +180,9 @@ async def asyncio_detailed( date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), float_prop: Union[Unset, float] = 3.14, int_prop: Union[Unset, int] = 7, - boolean_prop: Union[Unset, bool] = UNSET, + boolean_prop: Union[Unset, bool] = False, list_prop: Union[Unset, List[AnEnum]] = UNSET, - union_prop: Union[Unset, Union[float, str]] = "not a float", + union_prop: Union[Unset, float, str] = "not a float", enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( @@ -214,9 +214,9 @@ async def asyncio( date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), float_prop: Union[Unset, float] = 3.14, int_prop: Union[Unset, int] = 7, - boolean_prop: Union[Unset, bool] = UNSET, + boolean_prop: Union[Unset, bool] = False, list_prop: Union[Unset, List[AnEnum]] = UNSET, - union_prop: Union[Unset, Union[float, str]] = "not a float", + union_prop: Union[Unset, float, str] = "not a float", enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Optional[Union[None, HTTPValidationError]]: """ """ 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 e223bd393..a2fd94276 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,10 +17,10 @@ class AModel: a_camel_date_time: Union[datetime.datetime, datetime.date] a_date: datetime.date required_not_nullable: str + some_dict: Optional[Dict[Any, Any]] + required_nullable: Optional[str] nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET - some_dict: Optional[Dict[Any, Any]] = None attr_1_leading_digit: Union[Unset, str] = UNSET - required_nullable: Optional[str] = None not_required_nullable: Union[Unset, Optional[str]] = UNSET not_required_not_nullable: Union[Unset, str] = UNSET diff --git a/openapi_python_client/parser/properties.py b/openapi_python_client/parser/properties.py index 7666827d4..8fc4ac605 100644 --- a/openapi_python_client/parser/properties.py +++ b/openapi_python_client/parser/properties.py @@ -74,12 +74,10 @@ def get_imports(self, *, prefix: str) -> Set[str]: def to_string(self) -> str: """ How this should be declared in a dataclass """ - if self.default: + if self.default is not None: default = self.default elif not self.required: default = "UNSET" - elif self.nullable: - default = "None" else: default = None @@ -267,10 +265,12 @@ def get_type_string(self, no_optional: bool = False) -> str: inner_types = [p.get_type_string(no_optional=True) for p in self.inner_properties] inner_prop_string = ", ".join(inner_types) type_string = f"Union[{inner_prop_string}]" - if not no_optional and self.nullable: + if no_optional: + return type_string + if not self.required: + type_string = f"Union[Unset, {inner_prop_string}]" + if self.nullable: type_string = f"Optional[{type_string}]" - if not no_unset and not self.required: - type_string = f"Union[Unset, {type_string}]" return type_string def get_imports(self, *, prefix: str) -> Set[str]: diff --git a/openapi_python_client/templates/model.pyi b/openapi_python_client/templates/model.pyi index df100e69d..cbed730f3 100644 --- a/openapi_python_client/templates/model.pyi +++ b/openapi_python_client/templates/model.pyi @@ -13,7 +13,14 @@ from ..types import UNSET, Unset class {{ model.reference.class_name }}: """ {{ model.description }} """ {% for property in model.required_properties + model.optional_properties %} + {% if property.default is none and property.required %} {{ property.to_string() }} + {% endif %} + {% endfor %} + {% for property in model.required_properties + model.optional_properties %} + {% if property.default is not none or not property.required %} + {{ property.to_string() }} + {% endif %} {% endfor %} def to_dict(self) -> Dict[str, Any]: diff --git a/tests/test_openapi_parser/test_properties.py b/tests/test_openapi_parser/test_properties.py index f666b4774..892f4ec07 100644 --- a/tests/test_openapi_parser/test_properties.py +++ b/tests/test_openapi_parser/test_properties.py @@ -30,12 +30,15 @@ def test_get_type_string(self): p.nullable = True assert p.get_type_string() == f"Optional[{base_type_string}]" + assert p.get_type_string(no_optional=True) == base_type_string p.required = False assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" + assert p.get_type_string(no_optional=True) == base_type_string p.nullable = False assert p.get_type_string() == f"Union[Unset, {base_type_string}]" + assert p.get_type_string(no_optional=True) == base_type_string def test_to_string(self, mocker): from openapi_python_client.parser.properties import Property @@ -51,7 +54,7 @@ def test_to_string(self, mocker): p.required = True p.nullable = True - assert p.to_string() == f"{name}: {get_type_string()} = None" + assert p.to_string() == f"{name}: {get_type_string()}" p.default = "TEST" assert p.to_string() == f"{name}: {get_type_string()} = TEST" @@ -270,12 +273,15 @@ def test_get_type_string(self, mocker): p.nullable = True assert p.get_type_string() == f"Optional[{base_type_string}]" + assert p.get_type_string(no_optional=True) == base_type_string p.required = False assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" + assert p.get_type_string(no_optional=True) == base_type_string p.nullable = False assert p.get_type_string() == f"Union[Unset, {base_type_string}]" + assert p.get_type_string(no_optional=True) == base_type_string def test_get_type_imports(self, mocker): from openapi_python_client.parser.properties import ListProperty @@ -338,12 +344,16 @@ def test_get_type_string(self, mocker): p.nullable = True assert p.get_type_string() == f"Optional[{base_type_string}]" + assert p.get_type_string(no_optional=True) == base_type_string + base_type_string_with_unset = f"Union[Unset, inner_type_string_1, inner_type_string_2]" p.required = False - assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" + assert p.get_type_string() == f"Optional[{base_type_string_with_unset}]" + assert p.get_type_string(no_optional=True) == base_type_string p.nullable = False - assert p.get_type_string() == f"Union[Unset, {base_type_string}]" + assert p.get_type_string() == base_type_string_with_unset + assert p.get_type_string(no_optional=True) == base_type_string def test_get_type_imports(self, mocker): from openapi_python_client.parser.properties import UnionProperty @@ -483,12 +493,15 @@ def test_get_type_string(self, mocker): p.nullable = True assert p.get_type_string() == f"Optional[{base_type_string}]" + assert p.get_type_string(no_optional=True) == base_type_string p.required = False assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" + assert p.get_type_string(no_optional=True) == base_type_string p.nullable = False assert p.get_type_string() == f"Union[Unset, {base_type_string}]" + assert p.get_type_string(no_optional=True) == base_type_string properties._existing_enums = {} @@ -616,12 +629,15 @@ def test_get_type_string(self, mocker): p.nullable = True assert p.get_type_string() == f"Optional[{base_type_string}]" + assert p.get_type_string(no_optional=True) == base_type_string p.required = False assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" + assert p.get_type_string(no_optional=True) == base_type_string p.nullable = False assert p.get_type_string() == f"Union[Unset, {base_type_string}]" + assert p.get_type_string(no_optional=True) == base_type_string def test_get_imports(self, mocker): fake_reference = mocker.MagicMock(class_name="MyRefClass", module_name="my_test_enum") From a669ba633f2816a58519779b36cc779cb839b53b Mon Sep 17 00:00:00 2001 From: Ethan Mann Date: Fri, 6 Nov 2020 09:55:45 -0500 Subject: [PATCH 10/13] Regenerated the golden record --- .../api/tests/defaults_tests_defaults_post.py | 59 +++++++++-------- ...tional_value_tests_optional_query_param.py | 50 +++++++++++++++ .../tests/upload_file_tests_upload_post.py | 7 +- .../my_test_api_client/models/a_model.py | 64 ++++++++++++++----- .../body_upload_file_tests_upload_post.py | 4 +- .../models/http_validation_error.py | 20 +++--- .../models/validation_error.py | 4 +- .../my_test_api_client/types.py | 7 ++ 8 files changed, 158 insertions(+), 57 deletions(-) create mode 100644 end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/defaults_tests_defaults_post.py b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/defaults_tests_defaults_post.py index 84347423e..ed054315d 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/defaults_tests_defaults_post.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/defaults_tests_defaults_post.py @@ -11,6 +11,7 @@ from ...models.an_enum import AnEnum from ...models.http_validation_error import HTTPValidationError +from ...types import UNSET, Unset def _parse_response(*, response: httpx.Response) -> Optional[Union[None, HTTPValidationError]]: @@ -34,57 +35,63 @@ def httpx_request( *, client: Client, json_body: Dict[Any, Any], - string_prop: Optional[str] = "the default string", - datetime_prop: Optional[datetime.datetime] = isoparse("1010-10-10T00:00:00"), - date_prop: Optional[datetime.date] = isoparse("1010-10-10").date(), - float_prop: Optional[float] = 3.14, - int_prop: Optional[int] = 7, - boolean_prop: Optional[bool] = False, - list_prop: Optional[List[AnEnum]] = None, - union_prop: Optional[Union[Optional[float], Optional[str]]] = "not a float", - enum_prop: Optional[AnEnum] = None, + string_prop: Union[Unset, str] = "the default string", + datetime_prop: Union[Unset, datetime.datetime] = isoparse("1010-10-10T00:00:00"), + date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), + float_prop: Union[Unset, float] = 3.14, + int_prop: Union[Unset, int] = 7, + boolean_prop: Union[Unset, bool] = False, + list_prop: Union[Unset, List[AnEnum]] = UNSET, + union_prop: Union[Unset, float, str] = "not a float", + enum_prop: Union[Unset, AnEnum] = UNSET, ) -> httpx.Response[Union[None, HTTPValidationError]]: - json_datetime_prop = datetime_prop.isoformat() if datetime_prop else None + json_datetime_prop: Union[Unset, str] = UNSET + if not isinstance(datetime_prop, Unset): + json_datetime_prop = datetime_prop.isoformat() - json_date_prop = date_prop.isoformat() if date_prop else None + json_date_prop: Union[Unset, str] = UNSET + if not isinstance(date_prop, Unset): + json_date_prop = date_prop.isoformat() - if list_prop is None: - json_list_prop = None - else: + json_list_prop: Union[Unset, List[Any]] = UNSET + if not isinstance(list_prop, Unset): json_list_prop = [] for list_prop_item_data in list_prop: list_prop_item = list_prop_item_data.value json_list_prop.append(list_prop_item) - if union_prop is None: - json_union_prop: Optional[Union[Optional[float], Optional[str]]] = None + json_union_prop: Union[Unset, float, str] + if isinstance(union_prop, Unset): + json_union_prop = UNSET elif isinstance(union_prop, float): json_union_prop = union_prop else: json_union_prop = union_prop - json_enum_prop = enum_prop.value if enum_prop else None + json_enum_prop: Union[Unset, AnEnum] = UNSET + if not isinstance(enum_prop, Unset): + json_enum_prop = enum_prop.value params: Dict[str, Any] = {} - if string_prop is not None: + if string_prop is not UNSET: params["string_prop"] = string_prop - if datetime_prop is not None: + if datetime_prop is not UNSET: params["datetime_prop"] = json_datetime_prop - if date_prop is not None: + if date_prop is not UNSET: params["date_prop"] = json_date_prop - if float_prop is not None: + if float_prop is not UNSET: params["float_prop"] = float_prop - if int_prop is not None: + if int_prop is not UNSET: params["int_prop"] = int_prop - if boolean_prop is not None: + if boolean_prop is not UNSET: params["boolean_prop"] = boolean_prop - if list_prop is not None: + if list_prop is not UNSET: params["list_prop"] = json_list_prop - if union_prop is not None: + if union_prop is not UNSET: params["union_prop"] = json_union_prop - if enum_prop is not None: + if enum_prop is not UNSET: params["enum_prop"] = json_enum_prop json_json_body = json_body diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py new file mode 100644 index 000000000..bce044ca0 --- /dev/null +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py @@ -0,0 +1,50 @@ +from typing import Optional + +import httpx + +Client = httpx.Client + +from typing import List, Optional, Union + +from ...models.http_validation_error import HTTPValidationError +from ...types import UNSET, Unset + + +def _parse_response(*, response: httpx.Response) -> Optional[Union[None, HTTPValidationError]]: + if response.status_code == 200: + return None + if response.status_code == 422: + return HTTPValidationError.from_dict(cast(Dict[str, Any], response.json())) + return None + + +def _build_response(*, response: httpx.Response) -> httpx.Response[Union[None, HTTPValidationError]]: + return httpx.Response( + status_code=response.status_code, + content=response.content, + headers=response.headers, + parsed=_parse_response(response=response), + ) + + +def httpx_request( + *, + client: Client, + query_param: Union[Unset, List[str]] = UNSET, +) -> httpx.Response[Union[None, HTTPValidationError]]: + + json_query_param: Union[Unset, List[Any]] = UNSET + if not isinstance(query_param, Unset): + json_query_param = query_param + + params: Dict[str, Any] = {} + if query_param is not UNSET: + params["query_param"] = json_query_param + + response = client.request( + "get", + "/tests/optional_query_param/", + params=params, + ) + + return _build_response(response=response) diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/upload_file_tests_upload_post.py b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/upload_file_tests_upload_post.py index 1ef04185b..e294e6fae 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/upload_file_tests_upload_post.py @@ -4,10 +4,11 @@ Client = httpx.Client -from typing import Optional +from typing import Optional, Union from ...models.body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost from ...models.http_validation_error import HTTPValidationError +from ...types import UNSET, Unset def _parse_response(*, response: httpx.Response) -> Optional[Union[ @@ -37,12 +38,12 @@ def _build_response(*, response: httpx.Response) -> httpx.Response[Union[ def httpx_request(*, client: Client, multipart_data: BodyUploadFileTestsUploadPost, - keep_alive: Optional[bool] = None, + keep_alive: Union[Unset, bool] = UNSET, ) -> httpx.Response[Union[ None, HTTPValidationError ]]: - if keep_alive is not None: + if keep_alive is not UNSET: headers["keep-alive"] = keep_alive diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/a_model.py b/end_to_end_tests/golden-record-custom/my_test_api_client/models/a_model.py index a1a0ace0c..a2fd94276 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/models/a_model.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/models/a_model.py @@ -6,6 +6,7 @@ from ..models.an_enum import AnEnum from ..models.different_enum import DifferentEnum +from ..types import UNSET, Unset @attr.s(auto_attribs=True) @@ -13,17 +14,19 @@ class AModel: """ A Model for testing all the ways custom objects can be used """ an_enum_value: AnEnum - some_dict: Optional[Dict[Any, Any]] a_camel_date_time: Union[datetime.datetime, datetime.date] a_date: datetime.date - nested_list_of_enums: Optional[List[List[DifferentEnum]]] = None - attr_1_leading_digit: Optional[str] = None + required_not_nullable: str + some_dict: Optional[Dict[Any, Any]] + required_nullable: Optional[str] + nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET + attr_1_leading_digit: Union[Unset, str] = UNSET + not_required_nullable: Union[Unset, Optional[str]] = UNSET + not_required_not_nullable: Union[Unset, str] = UNSET def to_dict(self) -> Dict[str, Any]: an_enum_value = self.an_enum_value.value - some_dict = self.some_dict - if isinstance(self.a_camel_date_time, datetime.datetime): a_camel_date_time = self.a_camel_date_time.isoformat() @@ -32,9 +35,9 @@ def to_dict(self) -> Dict[str, Any]: a_date = self.a_date.isoformat() - if self.nested_list_of_enums is None: - nested_list_of_enums = None - else: + 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): nested_list_of_enums = [] for nested_list_of_enums_item_data in self.nested_list_of_enums: nested_list_of_enums_item = [] @@ -45,23 +48,36 @@ def to_dict(self) -> Dict[str, Any]: nested_list_of_enums.append(nested_list_of_enums_item) + some_dict = self.some_dict if self.some_dict else None + attr_1_leading_digit = self.attr_1_leading_digit + required_nullable = self.required_nullable + not_required_nullable = self.not_required_nullable + not_required_not_nullable = self.not_required_not_nullable - return { + field_dict = { "an_enum_value": an_enum_value, - "some_dict": some_dict, "aCamelDateTime": a_camel_date_time, "a_date": a_date, - "nested_list_of_enums": nested_list_of_enums, - "1_leading_digit": attr_1_leading_digit, + "required_not_nullable": required_not_nullable, + "some_dict": some_dict, + "required_nullable": required_nullable, } + if nested_list_of_enums is not UNSET: + field_dict["nested_list_of_enums"] = nested_list_of_enums + if attr_1_leading_digit is not UNSET: + field_dict["1_leading_digit"] = attr_1_leading_digit + if not_required_nullable is not UNSET: + field_dict["not_required_nullable"] = not_required_nullable + if not_required_not_nullable is not UNSET: + field_dict["not_required_not_nullable"] = not_required_not_nullable + + return field_dict @staticmethod def from_dict(d: Dict[str, Any]) -> "AModel": an_enum_value = AnEnum(d["an_enum_value"]) - some_dict = d["some_dict"] - def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime.datetime, datetime.date]: a_camel_date_time: Union[datetime.datetime, datetime.date] try: @@ -78,8 +94,10 @@ def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime.datetime, d a_date = isoparse(d["a_date"]).date() + required_not_nullable = d["required_not_nullable"] + nested_list_of_enums = [] - for nested_list_of_enums_item_data in d.get("nested_list_of_enums") or []: + for nested_list_of_enums_item_data in d.get("nested_list_of_enums", UNSET) or []: nested_list_of_enums_item = [] for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: nested_list_of_enums_item_item = DifferentEnum(nested_list_of_enums_item_item_data) @@ -88,13 +106,25 @@ def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime.datetime, d nested_list_of_enums.append(nested_list_of_enums_item) - attr_1_leading_digit = d.get("1_leading_digit") + some_dict = d["some_dict"] + + attr_1_leading_digit = d.get("1_leading_digit", UNSET) + + required_nullable = d["required_nullable"] + + not_required_nullable = d.get("not_required_nullable", UNSET) + + not_required_not_nullable = d.get("not_required_not_nullable", UNSET) return AModel( an_enum_value=an_enum_value, - some_dict=some_dict, a_camel_date_time=a_camel_date_time, a_date=a_date, + required_not_nullable=required_not_nullable, nested_list_of_enums=nested_list_of_enums, + some_dict=some_dict, attr_1_leading_digit=attr_1_leading_digit, + required_nullable=required_nullable, + not_required_nullable=not_required_nullable, + not_required_not_nullable=not_required_not_nullable, ) diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/body_upload_file_tests_upload_post.py b/end_to_end_tests/golden-record-custom/my_test_api_client/models/body_upload_file_tests_upload_post.py index 4fe7f8476..3435bd290 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/models/body_upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/models/body_upload_file_tests_upload_post.py @@ -14,10 +14,12 @@ class BodyUploadFileTestsUploadPost: def to_dict(self) -> Dict[str, Any]: some_file = self.some_file.to_tuple() - return { + field_dict = { "some_file": some_file, } + return field_dict + @staticmethod def from_dict(d: Dict[str, Any]) -> "BodyUploadFileTestsUploadPost": some_file = d["some_file"] diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/http_validation_error.py b/end_to_end_tests/golden-record-custom/my_test_api_client/models/http_validation_error.py index 90cd71e8c..9d29faa4d 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/models/http_validation_error.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/models/http_validation_error.py @@ -1,34 +1,36 @@ -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Union import attr from ..models.validation_error import ValidationError +from ..types import UNSET, Unset @attr.s(auto_attribs=True) class HTTPValidationError: """ """ - detail: Optional[List[ValidationError]] = None + detail: Union[Unset, List[ValidationError]] = UNSET def to_dict(self) -> Dict[str, Any]: - if self.detail is None: - detail = None - else: + detail: Union[Unset, List[Any]] = UNSET + if not isinstance(self.detail, Unset): detail = [] for detail_item_data in self.detail: detail_item = detail_item_data.to_dict() detail.append(detail_item) - return { - "detail": detail, - } + field_dict = {} + if detail is not UNSET: + field_dict["detail"] = detail + + return field_dict @staticmethod def from_dict(d: Dict[str, Any]) -> "HTTPValidationError": detail = [] - for detail_item_data in d.get("detail") or []: + for detail_item_data in d.get("detail", UNSET) or []: detail_item = ValidationError.from_dict(detail_item_data) detail.append(detail_item) diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/validation_error.py b/end_to_end_tests/golden-record-custom/my_test_api_client/models/validation_error.py index 1e415c476..77b9239ef 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/models/validation_error.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/models/validation_error.py @@ -17,12 +17,14 @@ def to_dict(self) -> Dict[str, Any]: msg = self.msg type = self.type - return { + field_dict = { "loc": loc, "msg": msg, "type": type, } + return field_dict + @staticmethod def from_dict(d: Dict[str, Any]) -> "ValidationError": loc = d["loc"] diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/types.py b/end_to_end_tests/golden-record-custom/my_test_api_client/types.py index 951227435..7f0f544d8 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/types.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/types.py @@ -4,6 +4,13 @@ import attr +class Unset: + pass + + +UNSET: Unset = Unset() + + @attr.s(auto_attribs=True) class File: """ Contains information for file uploads """ From 65cd3678262b21f1326ef48b872632118e01e5d4 Mon Sep 17 00:00:00 2001 From: Ethan Mann Date: Fri, 6 Nov 2020 11:01:21 -0500 Subject: [PATCH 11/13] UNSET is now a falsey value --- CHANGELOG.md | 8 +++++++- openapi_python_client/templates/types.py | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e964d216d..f4cbb5ac8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## 0.7.0 - Unreleased +### Breaking Changes + +- `UNSET`, the value given to any request/response field that is not `required` and wasn't specified, is falsey. + Anywhere that such a value may arise (such as a response that has non-required fields) must be carefully checked as + `if my_model.not_required_bool` could be False even though a concrete value wasn't actually returned. + ### Additions - Added a `--custom-template-path` option for providing custom jinja2 templates (#231 - Thanks @erichulburd!). +- Better compatibility for "required" (whether or not the field must be included) and "nullable" (whether or not the field can be null) (#205 & #208). Thanks @bowenwr & @emannguitar! ## 0.6.2 - 2020-11-03 @@ -20,7 +27,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update minimum Pydantic version to support Python 3.9 ### Additions -- Better compatibility for "required" (whether or not the field must be included) and "nullable" (whether or not the field can be null) (#205 & #208). Thanks @bowenwr & @emannguitar! - Allow specifying the generated client's version using `package_version_override` in a config file. (#225 - Thanks @fyhertz!) diff --git a/openapi_python_client/templates/types.py b/openapi_python_client/templates/types.py index 7f0f544d8..4f5a5abd2 100644 --- a/openapi_python_client/templates/types.py +++ b/openapi_python_client/templates/types.py @@ -5,7 +5,8 @@ class Unset: - pass + def __bool__(self): + return False UNSET: Unset = Unset() From 9926753e6b6206e145e158d2c2d4d4885bc9f36c Mon Sep 17 00:00:00 2001 From: Ethan Mann Date: Fri, 6 Nov 2020 11:26:28 -0500 Subject: [PATCH 12/13] Fixed changelog entry --- CHANGELOG.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4cbb5ac8..07ba58b7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Breaking Changes -- `UNSET`, the value given to any request/response field that is not `required` and wasn't specified, is falsey. - Anywhere that such a value may arise (such as a response that has non-required fields) must be carefully checked as - `if my_model.not_required_bool` could be False even though a concrete value wasn't actually returned. +- Any request/response field that is not `required` and wasn't specified is now set to `UNSET` instead of `None`. +- Values that are `UNSET` will not be sent along in API calls ### Additions From 2a8c51f30e99ad2ec71ef6dbe3cc8ebc8341b717 Mon Sep 17 00:00:00 2001 From: Ethan Mann Date: Fri, 6 Nov 2020 11:36:21 -0500 Subject: [PATCH 13/13] Regenerated e2e tests --- .../golden-record-custom/my_test_api_client/types.py | 3 ++- end_to_end_tests/golden-record/my_test_api_client/types.py | 3 ++- openapi_python_client/templates/types.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/types.py b/end_to_end_tests/golden-record-custom/my_test_api_client/types.py index 7f0f544d8..2061b9f08 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/types.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/types.py @@ -5,7 +5,8 @@ class Unset: - pass + def __bool__(self) -> bool: + return False UNSET: Unset = Unset() diff --git a/end_to_end_tests/golden-record/my_test_api_client/types.py b/end_to_end_tests/golden-record/my_test_api_client/types.py index 7f0f544d8..2061b9f08 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/types.py +++ b/end_to_end_tests/golden-record/my_test_api_client/types.py @@ -5,7 +5,8 @@ class Unset: - pass + def __bool__(self) -> bool: + return False UNSET: Unset = Unset() diff --git a/openapi_python_client/templates/types.py b/openapi_python_client/templates/types.py index 4f5a5abd2..2061b9f08 100644 --- a/openapi_python_client/templates/types.py +++ b/openapi_python_client/templates/types.py @@ -5,7 +5,7 @@ class Unset: - def __bool__(self): + def __bool__(self) -> bool: return False