diff --git a/CHANGELOG.md b/CHANGELOG.md index b2945c052..07ba58b7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## 0.7.0 - Unreleased +### Breaking Changes + +- 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 - 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 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..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 @@ -4,6 +4,14 @@ import attr +class Unset: + def __bool__(self) -> bool: + return False + + +UNSET: Unset = Unset() + + @attr.s(auto_attribs=True) class File: """ Contains information for file uploads """ 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..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 @@ -7,67 +7,73 @@ 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, Unset 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: 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, ) -> 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 + 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 @@ -103,15 +109,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: 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, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, @@ -138,15 +144,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: 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, ) -> Optional[Union[None, HTTPValidationError]]: """ """ @@ -169,15 +175,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: 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, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, @@ -203,15 +209,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: 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, ) -> 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 new file mode 100644 index 000000000..519c543ac --- /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,111 @@ +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, Unset + + +def _get_kwargs( + *, + client: Client, + query_param: Union[Unset, List[str]] = UNSET, +) -> Dict[str, Any]: + url = "{}/tests/optional_query_param/".format(client.base_url) + + headers: Dict[str, Any] = client.get_headers() + + 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 + + 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: Union[Unset, 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: Union[Unset, 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: Union[Unset, 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: Union[Unset, 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..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,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, Unset def _get_kwargs( *, client: Client, multipart_data: BodyUploadFileTestsUploadPost, - keep_alive: Optional[bool] = None, + keep_alive: Union[Unset, 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: 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: Optional[bool] = None, + 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: Optional[bool] = None, + 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: Optional[bool] = None, + 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 a1a0ace0c..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 @@ -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/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..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 @@ -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/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..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 @@ -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/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..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 @@ -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/my_test_api_client/types.py b/end_to_end_tests/golden-record/my_test_api_client/types.py index 951227435..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 @@ -4,6 +4,14 @@ import attr +class Unset: + def __bool__(self) -> bool: + return False + + +UNSET: Unset = Unset() + + @attr.s(auto_attribs=True) class File: """ Contains information for file uploads """ 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 afc1711ec..8fc4ac605 100644 --- a/openapi_python_client/parser/properties.py +++ b/openapi_python_client/parser/properties.py @@ -49,11 +49,16 @@ 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) """ - if no_optional or (self.required and not self.nullable): - return self._type_string - return f"Optional[{self._type_string}]" + type_string = self._type_string + if no_optional: + return type_string + if 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]: """ @@ -64,20 +69,20 @@ 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 Union, Optional", f"from {prefix}types import UNSET, Unset"} return set() 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 = "None" + default = "UNSET" 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()}" @@ -222,9 +227,14 @@ 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): - 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 no_optional: + return type_string + if 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]: """ @@ -252,11 +262,16 @@ class UnionProperty(Property): 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_types = [p.get_type_string(no_optional=True) for p in self.inner_properties] inner_prop_string = ", ".join(inner_types) - if no_optional or (self.required and not self.nullable): - return f"Union[{inner_prop_string}]" - return f"Optional[Union[{inner_prop_string}]]" + type_string = f"Union[{inner_prop_string}]" + 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}]" + return type_string def get_imports(self, *, prefix: str) -> Set[str]: """ @@ -328,10 +343,14 @@ 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): - return self.reference.class_name - return f"Optional[{self.reference.class_name}]" + type_string = self.reference.class_name + if no_optional: + return type_string + if 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]: """ @@ -390,9 +409,14 @@ 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): - return self.reference.class_name - return f"Optional[{self.reference.class_name}]" + type_string = self.reference.class_name + if no_optional: + return type_string + if 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]: """ 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..cbed730f3 100644 --- a/openapi_python_client/templates/model.pyi +++ b/openapi_python_client/templates/model.pyi @@ -2,6 +2,8 @@ from typing import Any, Dict import attr +from ..types import UNSET, Unset + {% for relative in model.relative_imports %} {{ relative }} {% endfor %} @@ -11,7 +13,14 @@ import attr 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]: @@ -24,11 +33,22 @@ class {{ model.reference.class_name }}: {% endif %} {% endfor %} - return { + + 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 %} + + return field_dict @staticmethod def from_dict(d: Dict[str, Any]) -> "{{ model.reference.class_name }}": @@ -36,7 +56,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/date_property.pyi b/openapi_python_client/templates/property_templates/date_property.pyi index 416acc1e1..39985f1eb 100644 --- a/openapi_python_client/templates/property_templates/date_property.pyi +++ b/openapi_python_client/templates/property_templates/date_property.pyi @@ -10,8 +10,18 @@ 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 +{{ destination }}: Union[Unset, str] = UNSET +if not isinstance({{ source }}, Unset): +{% if property.nullable %} + {{ destination }} = {{ source }}.isoformat() if {{ source }} else None +{% 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..6eb772c54 100644 --- a/openapi_python_client/templates/property_templates/datetime_property.pyi +++ b/openapi_python_client/templates/property_templates/datetime_property.pyi @@ -10,8 +10,18 @@ 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 +{{ destination }}: Union[Unset, str] = UNSET +if not isinstance({{ source }}, Unset): +{% if property.nullable %} + {{ destination }} = {{ source }}.isoformat() if {{ source }} else None +{% 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..f5a1f6aba 100644 --- a/openapi_python_client/templates/property_templates/enum_property.pyi +++ b/openapi_python_client/templates/property_templates/enum_property.pyi @@ -10,8 +10,18 @@ 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 +{{ destination }}: {{ property.get_type_string() }} = UNSET +if not isinstance({{ source }}, Unset): +{% if property.nullable %} + {{ destination }} = {{ source }}.value if {{ source }} else None +{% 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..a66e81bd0 100644 --- a/openapi_python_client/templates/property_templates/file_property.pyi +++ b/openapi_python_client/templates/property_templates/file_property.pyi @@ -5,8 +5,18 @@ {% 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 +{{ destination }}: {{ property.get_type_string() }} = UNSET +if not isinstance({{ source }}, Unset): +{% if property.nullable %} + {{ destination }} = {{ source }}.to_tuple() if {{ source }} else None +{% 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..f0ad1f0b3 100644 --- a/openapi_python_client/templates/property_templates/list_property.pyi +++ b/openapi_python_client/templates/property_templates/list_property.pyi @@ -33,7 +33,8 @@ 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 +42,18 @@ else: {% else %} {{ _transform(property, source, destination) }} {% endif %} +{% else %} +{{ destination }}: Union[Unset, List[Any]] = UNSET +if not isinstance({{ source }}, Unset): +{% if property.nullable %} + 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 c38a5199c..c1fa0a3bb 100644 --- a/openapi_python_client/templates/property_templates/ref_property.pyi +++ b/openapi_python_client/templates/property_templates/ref_property.pyi @@ -10,8 +10,18 @@ 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 +{{ destination }}: {{ property.get_type_string() }} = UNSET +if not isinstance({{ source }}, Unset): +{% if property.nullable %} + {{ destination }} = {{ source }}.to_dict() if {{ source }} else None +{% 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..ba53528d6 100644 --- a/openapi_python_client/templates/property_templates/union_property.pyi +++ b/openapi_python_client/templates/property_templates/union_property.pyi @@ -23,11 +23,20 @@ def _parse_{{ property.python_name }}(data: Dict[str, Any]) -> {{ property.get_t {% macro transform(property, source, destination) %} {% if not property.required %} +{{ destination }}: {{ property.get_type_string() }} +if isinstance({{ source }}, Unset): + {{ destination }} = 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..2061b9f08 100644 --- a/openapi_python_client/templates/types.py +++ b/openapi_python_client/templates/types.py @@ -4,6 +4,14 @@ import attr +class Unset: + def __bool__(self) -> bool: + return False + + +UNSET: Unset = Unset() + + @attr.s(auto_attribs=True) class File: """ Contains information for file uploads """ diff --git a/pyproject.toml b/pyproject.toml index 2d96aee0a..e699327a7 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" regen_custom = "python -m end_to_end_tests.regen_golden_record custom" 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..892f4ec07 100644 --- a/tests/test_openapi_parser/test_properties.py +++ b/tests/test_openapi_parser/test_properties.py @@ -24,14 +24,21 @@ 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() == "Optional[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}]" + 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 @@ -41,8 +48,13 @@ 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()} = None" + assert p.to_string() == f"{name}: {get_type_string()} = UNSET" + + p.required = True + p.nullable = True + assert p.to_string() == f"{name}: {get_type_string()}" p.default = "TEST" assert p.to_string() == f"{name}: {get_type_string()} = TEST" @@ -54,7 +66,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 Union, Optional", "from types import UNSET, Unset"} def test__validate_default(self): from openapi_python_client.parser.properties import Property @@ -75,9 +87,18 @@ def test_get_type_string(self): p = StringProperty(name="test", required=True, default=None, nullable=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() == f"Optional[{base_type_string}]" + p.required = False - assert p.get_type_string() == "Optional[str]" + 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 @@ -99,10 +120,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 Union, 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 Union, Optional", + "from ...types import UNSET, Unset", } def test__validate_default(self): @@ -127,11 +158,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 Union, 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 Union, Optional", + "from ...types import UNSET, Unset", } def test__validate_default(self): @@ -148,14 +189,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 Union, Optional", + "from ...types import UNSET, Unset", + } + + p.nullable = True + assert p.get_imports(prefix=prefix) == { + "from ...types import File", + "from typing import Union, Optional", + "from ...types import UNSET, Unset", } def test__validate_default(self): @@ -216,9 +267,21 @@ 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}]" + + assert p.get_type_string() == base_type_string + + 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"Optional[List[{inner_type_string}]]" + 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 @@ -226,18 +289,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 Union, Optional", + "from ...types import UNSET, Unset", + } + + p.nullable = True + assert p.get_imports(prefix=prefix) == { + inner_import, + "from typing import List", + "from typing import Union, Optional", + "from ...types import UNSET, Unset", } def test__validate_default(self, mocker): @@ -265,9 +338,22 @@ 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]" + + assert p.get_type_string() == base_type_string + + 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() == "Optional[Union[inner_type_string_1, inner_type_string_2]]" + 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() == 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 @@ -278,7 +364,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 +378,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 Union, 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 Union, Optional", + "from ...types import UNSET, Unset", } def test__validate_default(self, mocker): @@ -386,19 +483,32 @@ 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" - enum_property.required = False - assert enum_property.get_type_string() == "Optional[MyTestEnum]" + base_type_string = f"MyTestEnum" + + assert p.get_type_string() == base_type_string + + 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 = {} 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 +517,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 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 Union, Optional", + "from ...types import UNSET, Unset", } + properties._existing_enums = {} def test_values_from_list(self): @@ -496,7 +615,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, @@ -504,14 +623,25 @@ def test_get_type_string(self, mocker): nullable=False, ) - assert ref_property.get_type_string() == "MyRefClass" + base_type_string = f"MyRefClass" + + assert p.get_type_string() == base_type_string + + 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 - ref_property.required = False - assert ref_property.get_type_string() == "Optional[MyRefClass]" + 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") - prefix = mocker.MagicMock() + prefix = "..." from openapi_python_client.parser.properties import RefProperty @@ -528,7 +658,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 Union, 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 Union, Optional", + "from ...types import UNSET, Unset", } def test__validate_default(self, mocker): @@ -548,7 +688,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 +696,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 Union, Optional", + "from ...types import UNSET, Unset", + } + + p.nullable = False + assert p.get_imports(prefix=prefix) == { + "from typing import Dict", + "from typing import Union, 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 Union, Optional", + "from ...types import UNSET, Unset", } def test__validate_default(self):