diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/defaults_tests_defaults_post.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/defaults_tests_defaults_post.py index 574d5018b..f87ad3e1b 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/defaults_tests_defaults_post.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/defaults_tests_defaults_post.py @@ -13,6 +13,7 @@ from ...models.an_enum import AnEnum from ...models.http_validation_error import HTTPValidationError +from ...models.model_with_union_property import ModelWithUnionProperty from ...types import UNSET, Unset @@ -50,6 +51,7 @@ def httpx_request( union_prop: Union[Unset, float, str] = "not a float", union_prop_with_ref: Union[Unset, float, AnEnum] = 0.6, enum_prop: Union[Unset, AnEnum] = UNSET, + model_prop: Union[ModelWithUnionProperty, Unset] = UNSET, ) -> Response[Union[None, HTTPValidationError]]: json_datetime_prop: Union[Unset, str] = UNSET @@ -89,27 +91,33 @@ def httpx_request( if not isinstance(enum_prop, Unset): json_enum_prop = enum_prop + json_model_prop: Union[Unset, Dict[str, Any]] = UNSET + if not isinstance(model_prop, Unset): + json_model_prop = model_prop.to_dict() + params: Dict[str, Any] = {} - if string_prop is not UNSET: + if not isinstance(string_prop, Unset) and string_prop is not None: params["string_prop"] = string_prop - if datetime_prop is not UNSET: + if not isinstance(json_datetime_prop, Unset) and json_datetime_prop is not None: params["datetime_prop"] = json_datetime_prop - if date_prop is not UNSET: + if not isinstance(json_date_prop, Unset) and json_date_prop is not None: params["date_prop"] = json_date_prop - if float_prop is not UNSET: + if not isinstance(float_prop, Unset) and float_prop is not None: params["float_prop"] = float_prop - if int_prop is not UNSET: + if not isinstance(int_prop, Unset) and int_prop is not None: params["int_prop"] = int_prop - if boolean_prop is not UNSET: + if not isinstance(boolean_prop, Unset) and boolean_prop is not None: params["boolean_prop"] = boolean_prop - if list_prop is not UNSET: + if not isinstance(json_list_prop, Unset) and json_list_prop is not None: params["list_prop"] = json_list_prop - if union_prop is not UNSET: + if not isinstance(json_union_prop, Unset) and json_union_prop is not None: params["union_prop"] = json_union_prop - if union_prop_with_ref is not UNSET: + if not isinstance(json_union_prop_with_ref, Unset) and json_union_prop_with_ref is not None: params["union_prop_with_ref"] = json_union_prop_with_ref - if enum_prop is not UNSET: + if not isinstance(json_enum_prop, Unset) and json_enum_prop is not None: params["enum_prop"] = json_enum_prop + if not isinstance(json_model_prop, Unset) and json_model_prop is not None: + params.update(json_model_prop) response = client.request( "post", diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/optional_value_tests_optional_query_param.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/optional_value_tests_optional_query_param.py index bff43cc10..616dc4252 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/optional_value_tests_optional_query_param.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/optional_value_tests_optional_query_param.py @@ -44,7 +44,7 @@ def httpx_request( json_query_param = query_param params: Dict[str, Any] = {} - if query_param is not UNSET: + if not isinstance(json_query_param, Unset) and json_query_param is not None: params["query_param"] = json_query_param response = client.request( diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/models/__init__.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/__init__.py index d3ca924b3..6f5ac7423 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/models/__init__.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/models/__init__.py @@ -1,6 +1,10 @@ """ Contains all the data models used in inputs/outputs """ from .a_model import AModel +from .a_model_model import AModelModel +from .a_model_not_required_model import AModelNotRequiredModel +from .a_model_not_required_nullable_model import AModelNotRequiredNullableModel +from .a_model_nullable_model import AModelNullableModel from .an_enum import AnEnum from .an_int_enum import AnIntEnum from .body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/models/a_model.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/a_model.py index 2bf3e140d..15a7cc68c 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/models/a_model.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/models/a_model.py @@ -4,6 +4,10 @@ import attr from dateutil.parser import isoparse +from ..models.a_model_model import AModelModel +from ..models.a_model_not_required_model import AModelNotRequiredModel +from ..models.a_model_not_required_nullable_model import AModelNotRequiredNullableModel +from ..models.a_model_nullable_model import AModelNullableModel from ..models.an_enum import AnEnum from ..models.different_enum import DifferentEnum from ..types import UNSET, Unset @@ -17,12 +21,16 @@ class AModel: a_camel_date_time: Union[datetime.datetime, datetime.date] a_date: datetime.date required_not_nullable: str + model: AModelModel a_nullable_date: Optional[datetime.date] required_nullable: Optional[str] + nullable_model: Optional[AModelNullableModel] 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 + not_required_model: Union[AModelNotRequiredModel, Unset] = UNSET + not_required_nullable_model: Union[Optional[AModelNotRequiredNullableModel], Unset] = UNSET def to_dict(self) -> Dict[str, Any]: an_enum_value = self.an_enum_value.value @@ -35,6 +43,8 @@ def to_dict(self) -> Dict[str, Any]: a_date = self.a_date.isoformat() required_not_nullable = self.required_not_nullable + model = self.model.to_dict() + nested_list_of_enums: Union[Unset, List[Any]] = UNSET if not isinstance(self.nested_list_of_enums, Unset): nested_list_of_enums = [] @@ -52,6 +62,17 @@ def to_dict(self) -> Dict[str, Any]: required_nullable = self.required_nullable not_required_nullable = self.not_required_nullable not_required_not_nullable = self.not_required_not_nullable + nullable_model = self.nullable_model.to_dict() if self.nullable_model else None + + not_required_model: Union[Unset, Dict[str, Any]] = UNSET + if not isinstance(self.not_required_model, Unset): + not_required_model = self.not_required_model.to_dict() + + not_required_nullable_model: Union[None, Unset, Dict[str, Any]] = UNSET + if not isinstance(self.not_required_nullable_model, Unset): + not_required_nullable_model = ( + self.not_required_nullable_model.to_dict() if self.not_required_nullable_model else None + ) field_dict: Dict[str, Any] = {} field_dict.update( @@ -60,8 +81,10 @@ def to_dict(self) -> Dict[str, Any]: "aCamelDateTime": a_camel_date_time, "a_date": a_date, "required_not_nullable": required_not_nullable, + "model": model, "a_nullable_date": a_nullable_date, "required_nullable": required_nullable, + "nullable_model": nullable_model, } ) if nested_list_of_enums is not UNSET: @@ -72,6 +95,10 @@ def to_dict(self) -> Dict[str, Any]: 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 + if not_required_model is not UNSET: + field_dict["not_required_model"] = not_required_model + if not_required_nullable_model is not UNSET: + field_dict["not_required_nullable_model"] = not_required_nullable_model return field_dict @@ -99,6 +126,8 @@ def _parse_a_camel_date_time(data: Any) -> Union[datetime.datetime, datetime.dat required_not_nullable = d.pop("required_not_nullable") + model = AModelModel.from_dict(d.pop("model")) + nested_list_of_enums = [] _nested_list_of_enums = d.pop("nested_list_of_enums", UNSET) for nested_list_of_enums_item_data in _nested_list_of_enums or []: @@ -124,17 +153,38 @@ def _parse_a_camel_date_time(data: Any) -> Union[datetime.datetime, datetime.dat not_required_not_nullable = d.pop("not_required_not_nullable", UNSET) + nullable_model = None + _nullable_model = d.pop("nullable_model") + if _nullable_model is not None: + nullable_model = AModelNullableModel.from_dict(cast(Dict[str, Any], _nullable_model)) + + not_required_model: Union[AModelNotRequiredModel, Unset] = UNSET + _not_required_model = d.pop("not_required_model", UNSET) + if not isinstance(_not_required_model, Unset): + not_required_model = AModelNotRequiredModel.from_dict(cast(Dict[str, Any], _not_required_model)) + + not_required_nullable_model = None + _not_required_nullable_model = d.pop("not_required_nullable_model", UNSET) + if _not_required_nullable_model is not None and not isinstance(_not_required_nullable_model, Unset): + not_required_nullable_model = AModelNotRequiredNullableModel.from_dict( + cast(Dict[str, Any], _not_required_nullable_model) + ) + a_model = AModel( an_enum_value=an_enum_value, a_camel_date_time=a_camel_date_time, a_date=a_date, required_not_nullable=required_not_nullable, + model=model, nested_list_of_enums=nested_list_of_enums, a_nullable_date=a_nullable_date, attr_1_leading_digit=attr_1_leading_digit, required_nullable=required_nullable, not_required_nullable=not_required_nullable, not_required_not_nullable=not_required_not_nullable, + nullable_model=nullable_model, + not_required_model=not_required_model, + not_required_nullable_model=not_required_nullable_model, ) return a_model diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/models/a_model_model.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/a_model_model.py new file mode 100644 index 000000000..c1a00c152 --- /dev/null +++ b/end_to_end_tests/golden-record-custom/custom_e2e/models/a_model_model.py @@ -0,0 +1,85 @@ +from typing import Any, Dict, List, Union + +import attr + +from ..models.an_enum import AnEnum +from ..models.an_int_enum import AnIntEnum +from ..types import UNSET, Unset + + +@attr.s(auto_attribs=True) +class AModelModel: + """ """ + + a_property: Union[Unset, AnEnum, AnIntEnum] = UNSET + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + a_property: Union[Unset, AnEnum, AnIntEnum] + if isinstance(self.a_property, Unset): + a_property = UNSET + elif isinstance(self.a_property, AnEnum): + a_property = UNSET + if not isinstance(self.a_property, Unset): + a_property = self.a_property + + else: + a_property = UNSET + if not isinstance(self.a_property, Unset): + a_property = self.a_property + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if a_property is not UNSET: + field_dict["a_property"] = a_property + + return field_dict + + @staticmethod + def from_dict(src_dict: Dict[str, Any]) -> "AModelModel": + d = src_dict.copy() + + def _parse_a_property(data: Any) -> Union[Unset, AnEnum, AnIntEnum]: + data = None if isinstance(data, Unset) else data + a_property: Union[Unset, AnEnum, AnIntEnum] + try: + a_property = UNSET + _a_property = data + if _a_property is not None: + a_property = AnEnum(_a_property) + + return a_property + except: # noqa: E722 + pass + a_property = UNSET + _a_property = data + if _a_property is not None: + a_property = AnIntEnum(_a_property) + + return a_property + + a_property = _parse_a_property(d.pop("a_property", UNSET)) + + a_model_model = AModelModel( + a_property=a_property, + ) + + a_model_model.additional_properties = d + return a_model_model + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/models/a_model_not_required_model.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/a_model_not_required_model.py new file mode 100644 index 000000000..adecb4225 --- /dev/null +++ b/end_to_end_tests/golden-record-custom/custom_e2e/models/a_model_not_required_model.py @@ -0,0 +1,85 @@ +from typing import Any, Dict, List, Union + +import attr + +from ..models.an_enum import AnEnum +from ..models.an_int_enum import AnIntEnum +from ..types import UNSET, Unset + + +@attr.s(auto_attribs=True) +class AModelNotRequiredModel: + """ """ + + a_property: Union[Unset, AnEnum, AnIntEnum] = UNSET + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + a_property: Union[Unset, AnEnum, AnIntEnum] + if isinstance(self.a_property, Unset): + a_property = UNSET + elif isinstance(self.a_property, AnEnum): + a_property = UNSET + if not isinstance(self.a_property, Unset): + a_property = self.a_property + + else: + a_property = UNSET + if not isinstance(self.a_property, Unset): + a_property = self.a_property + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if a_property is not UNSET: + field_dict["a_property"] = a_property + + return field_dict + + @staticmethod + def from_dict(src_dict: Dict[str, Any]) -> "AModelNotRequiredModel": + d = src_dict.copy() + + def _parse_a_property(data: Any) -> Union[Unset, AnEnum, AnIntEnum]: + data = None if isinstance(data, Unset) else data + a_property: Union[Unset, AnEnum, AnIntEnum] + try: + a_property = UNSET + _a_property = data + if _a_property is not None: + a_property = AnEnum(_a_property) + + return a_property + except: # noqa: E722 + pass + a_property = UNSET + _a_property = data + if _a_property is not None: + a_property = AnIntEnum(_a_property) + + return a_property + + a_property = _parse_a_property(d.pop("a_property", UNSET)) + + a_model_not_required_model = AModelNotRequiredModel( + a_property=a_property, + ) + + a_model_not_required_model.additional_properties = d + return a_model_not_required_model + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/models/a_model_not_required_nullable_model.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/a_model_not_required_nullable_model.py new file mode 100644 index 000000000..9de2e3798 --- /dev/null +++ b/end_to_end_tests/golden-record-custom/custom_e2e/models/a_model_not_required_nullable_model.py @@ -0,0 +1,85 @@ +from typing import Any, Dict, List, Union + +import attr + +from ..models.an_enum import AnEnum +from ..models.an_int_enum import AnIntEnum +from ..types import UNSET, Unset + + +@attr.s(auto_attribs=True) +class AModelNotRequiredNullableModel: + """ """ + + a_property: Union[Unset, AnEnum, AnIntEnum] = UNSET + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + a_property: Union[Unset, AnEnum, AnIntEnum] + if isinstance(self.a_property, Unset): + a_property = UNSET + elif isinstance(self.a_property, AnEnum): + a_property = UNSET + if not isinstance(self.a_property, Unset): + a_property = self.a_property + + else: + a_property = UNSET + if not isinstance(self.a_property, Unset): + a_property = self.a_property + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if a_property is not UNSET: + field_dict["a_property"] = a_property + + return field_dict + + @staticmethod + def from_dict(src_dict: Dict[str, Any]) -> "AModelNotRequiredNullableModel": + d = src_dict.copy() + + def _parse_a_property(data: Any) -> Union[Unset, AnEnum, AnIntEnum]: + data = None if isinstance(data, Unset) else data + a_property: Union[Unset, AnEnum, AnIntEnum] + try: + a_property = UNSET + _a_property = data + if _a_property is not None: + a_property = AnEnum(_a_property) + + return a_property + except: # noqa: E722 + pass + a_property = UNSET + _a_property = data + if _a_property is not None: + a_property = AnIntEnum(_a_property) + + return a_property + + a_property = _parse_a_property(d.pop("a_property", UNSET)) + + a_model_not_required_nullable_model = AModelNotRequiredNullableModel( + a_property=a_property, + ) + + a_model_not_required_nullable_model.additional_properties = d + return a_model_not_required_nullable_model + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/models/a_model_nullable_model.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/a_model_nullable_model.py new file mode 100644 index 000000000..cbcf120f8 --- /dev/null +++ b/end_to_end_tests/golden-record-custom/custom_e2e/models/a_model_nullable_model.py @@ -0,0 +1,85 @@ +from typing import Any, Dict, List, Union + +import attr + +from ..models.an_enum import AnEnum +from ..models.an_int_enum import AnIntEnum +from ..types import UNSET, Unset + + +@attr.s(auto_attribs=True) +class AModelNullableModel: + """ """ + + a_property: Union[Unset, AnEnum, AnIntEnum] = UNSET + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + a_property: Union[Unset, AnEnum, AnIntEnum] + if isinstance(self.a_property, Unset): + a_property = UNSET + elif isinstance(self.a_property, AnEnum): + a_property = UNSET + if not isinstance(self.a_property, Unset): + a_property = self.a_property + + else: + a_property = UNSET + if not isinstance(self.a_property, Unset): + a_property = self.a_property + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if a_property is not UNSET: + field_dict["a_property"] = a_property + + return field_dict + + @staticmethod + def from_dict(src_dict: Dict[str, Any]) -> "AModelNullableModel": + d = src_dict.copy() + + def _parse_a_property(data: Any) -> Union[Unset, AnEnum, AnIntEnum]: + data = None if isinstance(data, Unset) else data + a_property: Union[Unset, AnEnum, AnIntEnum] + try: + a_property = UNSET + _a_property = data + if _a_property is not None: + a_property = AnEnum(_a_property) + + return a_property + except: # noqa: E722 + pass + a_property = UNSET + _a_property = data + if _a_property is not None: + a_property = AnIntEnum(_a_property) + + return a_property + + a_property = _parse_a_property(d.pop("a_property", UNSET)) + + a_model_nullable_model = AModelNullableModel( + a_property=a_property, + ) + + a_model_nullable_model.additional_properties = d + return a_model_nullable_model + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/models/model_with_primitive_additional_properties.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/model_with_primitive_additional_properties.py index 47d65d90b..797c1ce31 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/models/model_with_primitive_additional_properties.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/models/model_with_primitive_additional_properties.py @@ -33,7 +33,7 @@ def from_dict(src_dict: Dict[str, Any]) -> "ModelWithPrimitiveAdditionalProperti d = src_dict.copy() a_date_holder: Union[ModelWithPrimitiveAdditionalPropertiesADateHolder, Unset] = UNSET _a_date_holder = d.pop("a_date_holder", UNSET) - if _a_date_holder is not None and not isinstance(_a_date_holder, Unset): + if not isinstance(_a_date_holder, Unset): a_date_holder = ModelWithPrimitiveAdditionalPropertiesADateHolder.from_dict( cast(Dict[str, Any], _a_date_holder) ) 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 9242cddaa..ff6ef2409 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,6 +7,7 @@ from ...client import Client from ...models.an_enum import AnEnum from ...models.http_validation_error import HTTPValidationError +from ...models.model_with_union_property import ModelWithUnionProperty from ...types import UNSET, Response, Unset @@ -23,6 +24,7 @@ def _get_kwargs( union_prop: Union[Unset, float, str] = "not a float", union_prop_with_ref: Union[Unset, float, AnEnum] = 0.6, enum_prop: Union[Unset, AnEnum] = UNSET, + model_prop: Union[ModelWithUnionProperty, Unset] = UNSET, ) -> Dict[str, Any]: url = "{}/tests/defaults".format(client.base_url) @@ -65,27 +67,33 @@ def _get_kwargs( if not isinstance(enum_prop, Unset): json_enum_prop = enum_prop + json_model_prop: Union[Unset, Dict[str, Any]] = UNSET + if not isinstance(model_prop, Unset): + json_model_prop = model_prop.to_dict() + params: Dict[str, Any] = {} - if string_prop is not UNSET: + if not isinstance(string_prop, Unset) and string_prop is not None: params["string_prop"] = string_prop - if datetime_prop is not UNSET: + if not isinstance(json_datetime_prop, Unset) and json_datetime_prop is not None: params["datetime_prop"] = json_datetime_prop - if date_prop is not UNSET: + if not isinstance(json_date_prop, Unset) and json_date_prop is not None: params["date_prop"] = json_date_prop - if float_prop is not UNSET: + if not isinstance(float_prop, Unset) and float_prop is not None: params["float_prop"] = float_prop - if int_prop is not UNSET: + if not isinstance(int_prop, Unset) and int_prop is not None: params["int_prop"] = int_prop - if boolean_prop is not UNSET: + if not isinstance(boolean_prop, Unset) and boolean_prop is not None: params["boolean_prop"] = boolean_prop - if list_prop is not UNSET: + if not isinstance(json_list_prop, Unset) and json_list_prop is not None: params["list_prop"] = json_list_prop - if union_prop is not UNSET: + if not isinstance(json_union_prop, Unset) and json_union_prop is not None: params["union_prop"] = json_union_prop - if union_prop_with_ref is not UNSET: + if not isinstance(json_union_prop_with_ref, Unset) and json_union_prop_with_ref is not None: params["union_prop_with_ref"] = json_union_prop_with_ref - if enum_prop is not UNSET: + if not isinstance(json_enum_prop, Unset) and json_enum_prop is not None: params["enum_prop"] = json_enum_prop + if not isinstance(json_model_prop, Unset) and json_model_prop is not None: + params.update(json_model_prop) return { "url": url, @@ -130,6 +138,7 @@ def sync_detailed( union_prop: Union[Unset, float, str] = "not a float", union_prop_with_ref: Union[Unset, float, AnEnum] = 0.6, enum_prop: Union[Unset, AnEnum] = UNSET, + model_prop: Union[ModelWithUnionProperty, Unset] = UNSET, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, @@ -143,6 +152,7 @@ def sync_detailed( union_prop=union_prop, union_prop_with_ref=union_prop_with_ref, enum_prop=enum_prop, + model_prop=model_prop, ) response = httpx.post( @@ -165,6 +175,7 @@ def sync( union_prop: Union[Unset, float, str] = "not a float", union_prop_with_ref: Union[Unset, float, AnEnum] = 0.6, enum_prop: Union[Unset, AnEnum] = UNSET, + model_prop: Union[ModelWithUnionProperty, Unset] = UNSET, ) -> Optional[Union[None, HTTPValidationError]]: """ """ @@ -180,6 +191,7 @@ def sync( union_prop=union_prop, union_prop_with_ref=union_prop_with_ref, enum_prop=enum_prop, + model_prop=model_prop, ).parsed @@ -196,6 +208,7 @@ async def asyncio_detailed( union_prop: Union[Unset, float, str] = "not a float", union_prop_with_ref: Union[Unset, float, AnEnum] = 0.6, enum_prop: Union[Unset, AnEnum] = UNSET, + model_prop: Union[ModelWithUnionProperty, Unset] = UNSET, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, @@ -209,6 +222,7 @@ async def asyncio_detailed( union_prop=union_prop, union_prop_with_ref=union_prop_with_ref, enum_prop=enum_prop, + model_prop=model_prop, ) async with httpx.AsyncClient() as _client: @@ -230,6 +244,7 @@ async def asyncio( union_prop: Union[Unset, float, str] = "not a float", union_prop_with_ref: Union[Unset, float, AnEnum] = 0.6, enum_prop: Union[Unset, AnEnum] = UNSET, + model_prop: Union[ModelWithUnionProperty, Unset] = UNSET, ) -> Optional[Union[None, HTTPValidationError]]: """ """ @@ -246,5 +261,6 @@ async def asyncio( union_prop=union_prop, union_prop_with_ref=union_prop_with_ref, enum_prop=enum_prop, + model_prop=model_prop, ) ).parsed 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 751f48e03..576a770fe 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 @@ -21,7 +21,7 @@ def _get_kwargs( json_query_param = query_param params: Dict[str, Any] = {} - if query_param is not UNSET: + if not isinstance(json_query_param, Unset) and json_query_param is not None: params["query_param"] = json_query_param return { diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py index d3ca924b3..6f5ac7423 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py @@ -1,6 +1,10 @@ """ Contains all the data models used in inputs/outputs """ from .a_model import AModel +from .a_model_model import AModelModel +from .a_model_not_required_model import AModelNotRequiredModel +from .a_model_not_required_nullable_model import AModelNotRequiredNullableModel +from .a_model_nullable_model import AModelNullableModel from .an_enum import AnEnum from .an_int_enum import AnIntEnum from .body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost 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 2bf3e140d..15a7cc68c 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 @@ -4,6 +4,10 @@ import attr from dateutil.parser import isoparse +from ..models.a_model_model import AModelModel +from ..models.a_model_not_required_model import AModelNotRequiredModel +from ..models.a_model_not_required_nullable_model import AModelNotRequiredNullableModel +from ..models.a_model_nullable_model import AModelNullableModel from ..models.an_enum import AnEnum from ..models.different_enum import DifferentEnum from ..types import UNSET, Unset @@ -17,12 +21,16 @@ class AModel: a_camel_date_time: Union[datetime.datetime, datetime.date] a_date: datetime.date required_not_nullable: str + model: AModelModel a_nullable_date: Optional[datetime.date] required_nullable: Optional[str] + nullable_model: Optional[AModelNullableModel] 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 + not_required_model: Union[AModelNotRequiredModel, Unset] = UNSET + not_required_nullable_model: Union[Optional[AModelNotRequiredNullableModel], Unset] = UNSET def to_dict(self) -> Dict[str, Any]: an_enum_value = self.an_enum_value.value @@ -35,6 +43,8 @@ def to_dict(self) -> Dict[str, Any]: a_date = self.a_date.isoformat() required_not_nullable = self.required_not_nullable + model = self.model.to_dict() + nested_list_of_enums: Union[Unset, List[Any]] = UNSET if not isinstance(self.nested_list_of_enums, Unset): nested_list_of_enums = [] @@ -52,6 +62,17 @@ def to_dict(self) -> Dict[str, Any]: required_nullable = self.required_nullable not_required_nullable = self.not_required_nullable not_required_not_nullable = self.not_required_not_nullable + nullable_model = self.nullable_model.to_dict() if self.nullable_model else None + + not_required_model: Union[Unset, Dict[str, Any]] = UNSET + if not isinstance(self.not_required_model, Unset): + not_required_model = self.not_required_model.to_dict() + + not_required_nullable_model: Union[None, Unset, Dict[str, Any]] = UNSET + if not isinstance(self.not_required_nullable_model, Unset): + not_required_nullable_model = ( + self.not_required_nullable_model.to_dict() if self.not_required_nullable_model else None + ) field_dict: Dict[str, Any] = {} field_dict.update( @@ -60,8 +81,10 @@ def to_dict(self) -> Dict[str, Any]: "aCamelDateTime": a_camel_date_time, "a_date": a_date, "required_not_nullable": required_not_nullable, + "model": model, "a_nullable_date": a_nullable_date, "required_nullable": required_nullable, + "nullable_model": nullable_model, } ) if nested_list_of_enums is not UNSET: @@ -72,6 +95,10 @@ def to_dict(self) -> Dict[str, Any]: 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 + if not_required_model is not UNSET: + field_dict["not_required_model"] = not_required_model + if not_required_nullable_model is not UNSET: + field_dict["not_required_nullable_model"] = not_required_nullable_model return field_dict @@ -99,6 +126,8 @@ def _parse_a_camel_date_time(data: Any) -> Union[datetime.datetime, datetime.dat required_not_nullable = d.pop("required_not_nullable") + model = AModelModel.from_dict(d.pop("model")) + nested_list_of_enums = [] _nested_list_of_enums = d.pop("nested_list_of_enums", UNSET) for nested_list_of_enums_item_data in _nested_list_of_enums or []: @@ -124,17 +153,38 @@ def _parse_a_camel_date_time(data: Any) -> Union[datetime.datetime, datetime.dat not_required_not_nullable = d.pop("not_required_not_nullable", UNSET) + nullable_model = None + _nullable_model = d.pop("nullable_model") + if _nullable_model is not None: + nullable_model = AModelNullableModel.from_dict(cast(Dict[str, Any], _nullable_model)) + + not_required_model: Union[AModelNotRequiredModel, Unset] = UNSET + _not_required_model = d.pop("not_required_model", UNSET) + if not isinstance(_not_required_model, Unset): + not_required_model = AModelNotRequiredModel.from_dict(cast(Dict[str, Any], _not_required_model)) + + not_required_nullable_model = None + _not_required_nullable_model = d.pop("not_required_nullable_model", UNSET) + if _not_required_nullable_model is not None and not isinstance(_not_required_nullable_model, Unset): + not_required_nullable_model = AModelNotRequiredNullableModel.from_dict( + cast(Dict[str, Any], _not_required_nullable_model) + ) + a_model = AModel( an_enum_value=an_enum_value, a_camel_date_time=a_camel_date_time, a_date=a_date, required_not_nullable=required_not_nullable, + model=model, nested_list_of_enums=nested_list_of_enums, a_nullable_date=a_nullable_date, attr_1_leading_digit=attr_1_leading_digit, required_nullable=required_nullable, not_required_nullable=not_required_nullable, not_required_not_nullable=not_required_not_nullable, + nullable_model=nullable_model, + not_required_model=not_required_model, + not_required_nullable_model=not_required_nullable_model, ) return a_model diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_model_model.py b/end_to_end_tests/golden-record/my_test_api_client/models/a_model_model.py new file mode 100644 index 000000000..c1a00c152 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/a_model_model.py @@ -0,0 +1,85 @@ +from typing import Any, Dict, List, Union + +import attr + +from ..models.an_enum import AnEnum +from ..models.an_int_enum import AnIntEnum +from ..types import UNSET, Unset + + +@attr.s(auto_attribs=True) +class AModelModel: + """ """ + + a_property: Union[Unset, AnEnum, AnIntEnum] = UNSET + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + a_property: Union[Unset, AnEnum, AnIntEnum] + if isinstance(self.a_property, Unset): + a_property = UNSET + elif isinstance(self.a_property, AnEnum): + a_property = UNSET + if not isinstance(self.a_property, Unset): + a_property = self.a_property + + else: + a_property = UNSET + if not isinstance(self.a_property, Unset): + a_property = self.a_property + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if a_property is not UNSET: + field_dict["a_property"] = a_property + + return field_dict + + @staticmethod + def from_dict(src_dict: Dict[str, Any]) -> "AModelModel": + d = src_dict.copy() + + def _parse_a_property(data: Any) -> Union[Unset, AnEnum, AnIntEnum]: + data = None if isinstance(data, Unset) else data + a_property: Union[Unset, AnEnum, AnIntEnum] + try: + a_property = UNSET + _a_property = data + if _a_property is not None: + a_property = AnEnum(_a_property) + + return a_property + except: # noqa: E722 + pass + a_property = UNSET + _a_property = data + if _a_property is not None: + a_property = AnIntEnum(_a_property) + + return a_property + + a_property = _parse_a_property(d.pop("a_property", UNSET)) + + a_model_model = AModelModel( + a_property=a_property, + ) + + a_model_model.additional_properties = d + return a_model_model + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_model_not_required_model.py b/end_to_end_tests/golden-record/my_test_api_client/models/a_model_not_required_model.py new file mode 100644 index 000000000..adecb4225 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/a_model_not_required_model.py @@ -0,0 +1,85 @@ +from typing import Any, Dict, List, Union + +import attr + +from ..models.an_enum import AnEnum +from ..models.an_int_enum import AnIntEnum +from ..types import UNSET, Unset + + +@attr.s(auto_attribs=True) +class AModelNotRequiredModel: + """ """ + + a_property: Union[Unset, AnEnum, AnIntEnum] = UNSET + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + a_property: Union[Unset, AnEnum, AnIntEnum] + if isinstance(self.a_property, Unset): + a_property = UNSET + elif isinstance(self.a_property, AnEnum): + a_property = UNSET + if not isinstance(self.a_property, Unset): + a_property = self.a_property + + else: + a_property = UNSET + if not isinstance(self.a_property, Unset): + a_property = self.a_property + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if a_property is not UNSET: + field_dict["a_property"] = a_property + + return field_dict + + @staticmethod + def from_dict(src_dict: Dict[str, Any]) -> "AModelNotRequiredModel": + d = src_dict.copy() + + def _parse_a_property(data: Any) -> Union[Unset, AnEnum, AnIntEnum]: + data = None if isinstance(data, Unset) else data + a_property: Union[Unset, AnEnum, AnIntEnum] + try: + a_property = UNSET + _a_property = data + if _a_property is not None: + a_property = AnEnum(_a_property) + + return a_property + except: # noqa: E722 + pass + a_property = UNSET + _a_property = data + if _a_property is not None: + a_property = AnIntEnum(_a_property) + + return a_property + + a_property = _parse_a_property(d.pop("a_property", UNSET)) + + a_model_not_required_model = AModelNotRequiredModel( + a_property=a_property, + ) + + a_model_not_required_model.additional_properties = d + return a_model_not_required_model + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_model_not_required_nullable_model.py b/end_to_end_tests/golden-record/my_test_api_client/models/a_model_not_required_nullable_model.py new file mode 100644 index 000000000..9de2e3798 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/a_model_not_required_nullable_model.py @@ -0,0 +1,85 @@ +from typing import Any, Dict, List, Union + +import attr + +from ..models.an_enum import AnEnum +from ..models.an_int_enum import AnIntEnum +from ..types import UNSET, Unset + + +@attr.s(auto_attribs=True) +class AModelNotRequiredNullableModel: + """ """ + + a_property: Union[Unset, AnEnum, AnIntEnum] = UNSET + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + a_property: Union[Unset, AnEnum, AnIntEnum] + if isinstance(self.a_property, Unset): + a_property = UNSET + elif isinstance(self.a_property, AnEnum): + a_property = UNSET + if not isinstance(self.a_property, Unset): + a_property = self.a_property + + else: + a_property = UNSET + if not isinstance(self.a_property, Unset): + a_property = self.a_property + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if a_property is not UNSET: + field_dict["a_property"] = a_property + + return field_dict + + @staticmethod + def from_dict(src_dict: Dict[str, Any]) -> "AModelNotRequiredNullableModel": + d = src_dict.copy() + + def _parse_a_property(data: Any) -> Union[Unset, AnEnum, AnIntEnum]: + data = None if isinstance(data, Unset) else data + a_property: Union[Unset, AnEnum, AnIntEnum] + try: + a_property = UNSET + _a_property = data + if _a_property is not None: + a_property = AnEnum(_a_property) + + return a_property + except: # noqa: E722 + pass + a_property = UNSET + _a_property = data + if _a_property is not None: + a_property = AnIntEnum(_a_property) + + return a_property + + a_property = _parse_a_property(d.pop("a_property", UNSET)) + + a_model_not_required_nullable_model = AModelNotRequiredNullableModel( + a_property=a_property, + ) + + a_model_not_required_nullable_model.additional_properties = d + return a_model_not_required_nullable_model + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_model_nullable_model.py b/end_to_end_tests/golden-record/my_test_api_client/models/a_model_nullable_model.py new file mode 100644 index 000000000..cbcf120f8 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/a_model_nullable_model.py @@ -0,0 +1,85 @@ +from typing import Any, Dict, List, Union + +import attr + +from ..models.an_enum import AnEnum +from ..models.an_int_enum import AnIntEnum +from ..types import UNSET, Unset + + +@attr.s(auto_attribs=True) +class AModelNullableModel: + """ """ + + a_property: Union[Unset, AnEnum, AnIntEnum] = UNSET + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + a_property: Union[Unset, AnEnum, AnIntEnum] + if isinstance(self.a_property, Unset): + a_property = UNSET + elif isinstance(self.a_property, AnEnum): + a_property = UNSET + if not isinstance(self.a_property, Unset): + a_property = self.a_property + + else: + a_property = UNSET + if not isinstance(self.a_property, Unset): + a_property = self.a_property + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if a_property is not UNSET: + field_dict["a_property"] = a_property + + return field_dict + + @staticmethod + def from_dict(src_dict: Dict[str, Any]) -> "AModelNullableModel": + d = src_dict.copy() + + def _parse_a_property(data: Any) -> Union[Unset, AnEnum, AnIntEnum]: + data = None if isinstance(data, Unset) else data + a_property: Union[Unset, AnEnum, AnIntEnum] + try: + a_property = UNSET + _a_property = data + if _a_property is not None: + a_property = AnEnum(_a_property) + + return a_property + except: # noqa: E722 + pass + a_property = UNSET + _a_property = data + if _a_property is not None: + a_property = AnIntEnum(_a_property) + + return a_property + + a_property = _parse_a_property(d.pop("a_property", UNSET)) + + a_model_nullable_model = AModelNullableModel( + a_property=a_property, + ) + + a_model_nullable_model.additional_properties = d + return a_model_nullable_model + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_primitive_additional_properties.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_primitive_additional_properties.py index 47d65d90b..797c1ce31 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_primitive_additional_properties.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_primitive_additional_properties.py @@ -33,7 +33,7 @@ def from_dict(src_dict: Dict[str, Any]) -> "ModelWithPrimitiveAdditionalProperti d = src_dict.copy() a_date_holder: Union[ModelWithPrimitiveAdditionalPropertiesADateHolder, Unset] = UNSET _a_date_holder = d.pop("a_date_holder", UNSET) - if _a_date_holder is not None and not isinstance(_a_date_holder, Unset): + if not isinstance(_a_date_holder, Unset): a_date_holder = ModelWithPrimitiveAdditionalPropertiesADateHolder.from_dict( cast(Dict[str, Any], _a_date_holder) ) diff --git a/end_to_end_tests/openapi.json b/end_to_end_tests/openapi.json index 9e3d78908..931fc481e 100644 --- a/end_to_end_tests/openapi.json +++ b/end_to_end_tests/openapi.json @@ -290,12 +290,49 @@ { "required": false, "schema": { - "title": "Datetime Prop", + "title": "Not Required, Not Nullable Datetime Prop", + "nullable": false, "type": "string", "format": "date-time", "default": "1010-10-10T00:00:00" }, - "name": "datetime_prop", + "name": "not_required_not_nullable_datetime_prop", + "in": "query" + }, + { + "required": false, + "schema": { + "title": "Not Required, Nullable Datetime Prop", + "nullable": true, + "type": "string", + "format": "date-time", + "default": "1010-10-10T00:00:00" + }, + "name": "not_required_nullable_datetime_prop", + "in": "query" + }, + { + "required": true, + "schema": { + "title": "Required, Not Nullable Datetime Prop", + "nullable": false, + "type": "string", + "format": "date-time", + "default": "1010-10-10T00:00:00" + }, + "name": "required_not_nullable_datetime_prop", + "in": "query" + }, + { + "required": true, + "schema": { + "title": "Required, Nullable Datetime Prop", + "nullable": true, + "type": "string", + "format": "date-time", + "default": "1010-10-10T00:00:00" + }, + "name": "required_nullable_datetime_prop", "in": "query" }, { diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index 427276692..e3a3f69b7 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -19,6 +19,7 @@ class NoneProperty(Property): """ A property that is always None (used for empty schemas) """ _type_string: ClassVar[str] = "None" + _json_type_string: ClassVar[str] = "None" template: ClassVar[Optional[str]] = "none_property.pyi" @@ -29,6 +30,7 @@ class StringProperty(Property): max_length: Optional[int] = None pattern: Optional[str] = None _type_string: ClassVar[str] = "str" + _json_type_string: ClassVar[str] = "str" @attr.s(auto_attribs=True, frozen=True) @@ -38,6 +40,7 @@ class DateTimeProperty(Property): """ _type_string: ClassVar[str] = "datetime.datetime" + _json_type_string: ClassVar[str] = "str" template: ClassVar[str] = "datetime_property.pyi" def get_imports(self, *, prefix: str) -> Set[str]: @@ -58,6 +61,7 @@ class DateProperty(Property): """ A property of type datetime.date """ _type_string: ClassVar[str] = "datetime.date" + _json_type_string: ClassVar[str] = "str" template: ClassVar[str] = "date_property.pyi" def get_imports(self, *, prefix: str) -> Set[str]: @@ -78,6 +82,8 @@ class FileProperty(Property): """ A property used for uploading files """ _type_string: ClassVar[str] = "File" + # Return type of File.to_tuple() + _json_type_string: ClassVar[str] = "Tuple[Optional[str], Union[BinaryIO, TextIO], Optional[str]]" template: ClassVar[str] = "file_property.pyi" def get_imports(self, *, prefix: str) -> Set[str]: @@ -98,6 +104,7 @@ class FloatProperty(Property): """ A property of type float """ _type_string: ClassVar[str] = "float" + _json_type_string: ClassVar[str] = "float" @attr.s(auto_attribs=True, frozen=True) @@ -105,6 +112,7 @@ class IntProperty(Property): """ A property of type int """ _type_string: ClassVar[str] = "int" + _json_type_string: ClassVar[str] = "int" @attr.s(auto_attribs=True, frozen=True) @@ -112,6 +120,7 @@ class BooleanProperty(Property): """ Property for bool """ _type_string: ClassVar[str] = "bool" + _json_type_string: ClassVar[str] = "bool" InnerProp = TypeVar("InnerProp", bound=Property) @@ -122,18 +131,11 @@ class ListProperty(Property, Generic[InnerProp]): """ A property representing a list (array) of other properties """ inner_property: InnerProp + _json_type_string: ClassVar[str] = "List[Any]" template: ClassVar[str] = "list_property.pyi" - 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 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_base_type_string(self) -> str: + return f"List[{self.inner_property.get_type_string()}]" def get_instance_type_string(self) -> str: """Get a string representation of runtime type that should be used for `isinstance` checks""" @@ -167,18 +169,38 @@ def __attrs_post_init__(self) -> None: self, "has_properties_without_templates", any(prop.template is None for prop in self.inner_properties) ) - 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_optional=True) for p in self.inner_properties] - inner_prop_string = ", ".join(inner_types) - type_string = f"Union[{inner_prop_string}]" + def _get_inner_prop_string(self, json: bool = False) -> str: + inner_types = [p.get_type_string(no_optional=True, json=json) for p in self.inner_properties] + unique_inner_types = list(dict.fromkeys(inner_types)) + return ", ".join(unique_inner_types) + + def get_base_type_string(self, json: bool = False) -> str: + return f"Union[{self._get_inner_prop_string(json=json)}]" + + def get_type_string(self, no_optional: bool = False, query_parameter: bool = False, json: bool = False) -> str: + """ + Get a string representation of type that should be used when declaring this property. + + This implementation differs slightly from `Property.get_type_string` in order to collapse + nested union types. + """ + type_string = self.get_base_type_string(json=json) 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 + if self.required: + if self.nullable: + return f"Union[None, {self._get_inner_prop_string(json=json)}]" + else: + return type_string + else: + if self.nullable: + return f"Union[Unset, None, {self._get_inner_prop_string(json=json)}]" + else: + if query_parameter: + # For query parameters, None has the same meaning as Unset + return f"Union[Unset, None, {self._get_inner_prop_string(json=json)}]" + else: + return f"Union[Unset, {self._get_inner_prop_string(json=json)}]" def get_imports(self, *, prefix: str) -> Set[str]: """ diff --git a/openapi_python_client/parser/properties/enum_property.py b/openapi_python_client/parser/properties/enum_property.py index 1217f23ee..6938dd716 100644 --- a/openapi_python_client/parser/properties/enum_property.py +++ b/openapi_python_client/parser/properties/enum_property.py @@ -18,21 +18,13 @@ class EnumProperty(Property): values: Dict[str, ValueType] reference: Reference value_type: Type[ValueType] + _json_type_string: ClassVar[str] = "int" default: Optional[Any] = attr.ib() template: ClassVar[str] = "enum_property.pyi" - 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 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_base_type_string(self) -> str: + return self.reference.class_name def get_imports(self, *, prefix: str) -> Set[str]: """ diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index 084017a41..3ab145af4 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -17,19 +17,12 @@ class ModelProperty(Property): description: str relative_imports: Set[str] additional_properties: Union[bool, Property] + _json_type_string: ClassVar[str] = "Dict[str, Any]" template: ClassVar[str] = "model_property.pyi" - 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 no_optional: - return type_string - if self.nullable: - type_string = f"Optional[{type_string}]" - if not self.required: - type_string = f"Union[{type_string}, Unset]" - return type_string + def get_base_type_string(self) -> str: + return self.reference.class_name def get_imports(self, *, prefix: str) -> Set[str]: """ diff --git a/openapi_python_client/parser/properties/property.py b/openapi_python_client/parser/properties/property.py index 0b7047551..651370ae2 100644 --- a/openapi_python_client/parser/properties/property.py +++ b/openapi_python_client/parser/properties/property.py @@ -24,6 +24,7 @@ class Property: required: bool nullable: bool _type_string: ClassVar[str] = "" + _json_type_string: ClassVar[str] = "" # Type of the property after JSON serialization default: Optional[str] = attr.ib() python_name: str = attr.ib(init=False) @@ -32,21 +33,39 @@ class Property: def __attrs_post_init__(self) -> None: object.__setattr__(self, "python_name", utils.to_valid_python_identifier(utils.snake_case(self.name))) - def get_type_string(self, no_optional: bool = False) -> str: + def get_base_type_string(self) -> str: + return self._type_string + + def get_type_string(self, no_optional: bool = False, query_parameter: bool = False, json: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property Args: no_optional: Do not include Optional or Unset even if the value is optional (needed for isinstance checks) + query_parameter: True if the property's type is being used for a query parameter + json: True if the type refers to the property after JSON serialization """ - type_string = self._type_string + if json: + type_string = self._json_type_string + else: + type_string = self.get_base_type_string() + if no_optional: - return self._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 + return type_string + if self.required: + if self.nullable: + return f"Optional[{type_string}]" + else: + return type_string + else: + if self.nullable: + return f"Union[Unset, None, {type_string}]" + else: + if query_parameter: + # For query parameters, None has the same meaning as Unset + return f"Union[Unset, None, {type_string}]" + else: + return f"Union[Unset, {type_string}]" def get_instance_type_string(self) -> str: """Get a string representation of runtime type that should be used for `isinstance` checks""" @@ -69,8 +88,13 @@ def get_imports(self, *, prefix: str) -> Set[str]: imports.add(f"from {prefix}types import UNSET, Unset") return imports - def to_string(self) -> str: - """ How this should be declared in a dataclass """ + def to_string(self, query_parameter: bool = False) -> str: + """ + How this should be declared in a dataclass + + Args: + query_parameter: True if the property's type is being used for a query parameter + """ default: Optional[str] if self.default is not None: default = self.default @@ -80,6 +104,6 @@ def to_string(self) -> str: default = None if default is not None: - return f"{self.python_name}: {self.get_type_string()} = {default}" + return f"{self.python_name}: {self.get_type_string(query_parameter=query_parameter)} = {default}" else: - return f"{self.python_name}: {self.get_type_string()}" + return f"{self.python_name}: {self.get_type_string(query_parameter=query_parameter)}" diff --git a/openapi_python_client/templates/endpoint_macros.pyi b/openapi_python_client/templates/endpoint_macros.pyi index 5819714d8..7a1862be3 100644 --- a/openapi_python_client/templates/endpoint_macros.pyi +++ b/openapi_python_client/templates/endpoint_macros.pyi @@ -17,12 +17,12 @@ if {{ parameter.python_name }} is not UNSET: {% set destination = "json_" + property.python_name %} {% if property.template %} {% from "property_templates/" + property.template import transform %} -{{ transform(property, property.python_name, destination) }} +{{ transform(property, property.python_name, destination, query_parameter=True) }} {% endif %} {% endfor %} params: Dict[str, Any] = { {% for property in endpoint.query_parameters %} - {% if property.required %} + {% if property.required and not property.nullable %} {% if property.template %} "{{ property.name }}": {{ "json_" + property.python_name }}, {% else %} @@ -32,8 +32,8 @@ params: Dict[str, Any] = { {% endfor %} } {% for property in endpoint.query_parameters %} - {% if not property.required %} -if {{ property.python_name }} is not UNSET: + {% if not property.required or property.nullable %} +if {% if not property.required %}{{ property.python_name }} is not UNSET and {% endif %}{{ property.python_name }} is not None: {% if property.template %} params["{{ property.name }}"] = {{ "json_" + property.python_name }} {% else %} @@ -96,7 +96,7 @@ json_body: {{ endpoint.json_body.get_type_string() }}, {% endif %} {# query parameters #} {% for parameter in endpoint.query_parameters %} -{{ parameter.to_string() }}, +{{ parameter.to_string(query_parameter=True) }}, {% endfor %} {% for parameter in endpoint.header_parameters %} {{ parameter.to_string() }}, diff --git a/openapi_python_client/templates/property_templates/date_property.pyi b/openapi_python_client/templates/property_templates/date_property.pyi index a3a980c8f..bc2cae912 100644 --- a/openapi_python_client/templates/property_templates/date_property.pyi +++ b/openapi_python_client/templates/property_templates/date_property.pyi @@ -9,13 +9,13 @@ if _{{ property.python_name }} is not None: {% endif %} {% endmacro %} -{% macro transform(property, source, destination, declare_type=True) %} +{% macro transform(property, source, destination, declare_type=True, query_parameter=False) %} {% if property.required %} {{ destination }} = {{ source }}.isoformat() {% if property.nullable %}if {{ source }} else None {%endif%} {% else %} -{{ destination }}{% if declare_type %}: Union[Unset, str]{% endif %} = UNSET +{{ destination }}{% if declare_type %}: {{ property.get_type_string(query_parameter=query_parameter, json=True) }}{% endif %} = UNSET if not isinstance({{ source }}, Unset): -{% if property.nullable %} +{% if property.nullable or query_parameter %} {{ destination }} = {{ source }}.isoformat() if {{ source }} else None {% else %} {{ destination }} = {{ source }}.isoformat() diff --git a/openapi_python_client/templates/property_templates/datetime_property.pyi b/openapi_python_client/templates/property_templates/datetime_property.pyi index b8e1b8ff0..91ce0cacc 100644 --- a/openapi_python_client/templates/property_templates/datetime_property.pyi +++ b/openapi_python_client/templates/property_templates/datetime_property.pyi @@ -14,7 +14,7 @@ if _{{ property.python_name }} is not None: {% endif %} {% endmacro %} -{% macro transform(property, source, destination, declare_type=True) %} +{% macro transform(property, source, destination, declare_type=True, query_parameter=False) %} {% if property.required %} {% if property.nullable %} {{ destination }} = {{ source }}.isoformat() if {{ source }} else None @@ -22,9 +22,9 @@ if _{{ property.python_name }} is not None: {{ destination }} = {{ source }}.isoformat() {% endif %} {% else %} -{{ destination }}{% if declare_type %}: Union[Unset, str]{% endif %} = UNSET +{{ destination }}{% if declare_type %}: {{ property.get_type_string(query_parameter=query_parameter, json=True) }}{% endif %} = UNSET if not isinstance({{ source }}, Unset): -{% if property.nullable %} +{% if property.nullable or query_parameter %} {{ destination }} = {{ source }}.isoformat() if {{ source }} else None {% else %} {{ destination }} = {{ source }}.isoformat() diff --git a/openapi_python_client/templates/property_templates/enum_property.pyi b/openapi_python_client/templates/property_templates/enum_property.pyi index 4765a6fd5..831f633d5 100644 --- a/openapi_python_client/templates/property_templates/enum_property.pyi +++ b/openapi_python_client/templates/property_templates/enum_property.pyi @@ -9,7 +9,7 @@ if _{{ property.python_name }} is not None: {% endif %} {% endmacro %} -{% macro transform(property, source, destination, declare_type=True) %} +{% macro transform(property, source, destination, declare_type=True, query_parameter=False) %} {% if property.required %} {% if property.nullable %} {{ destination }} = {{ source }}.value if {{ source }} else None @@ -17,12 +17,12 @@ if _{{ property.python_name }} is not None: {{ destination }} = {{ source }}.value {% endif %} {% else %} -{{ destination }}{% if declare_type %}: {{ property.get_type_string() }}{% endif %} = UNSET +{{ destination }}{% if declare_type %}: {{ property.get_type_string(query_parameter=query_parameter, json=True) }}{% endif %} = UNSET if not isinstance({{ source }}, Unset): -{% if property.nullable %} - {{ destination }} = {{ source }} if {{ source }} else None +{% if property.nullable or query_parameter %} + {{ destination }} = {{ source }}.value if {{ source }} else None {% else %} - {{ destination }} = {{ source }} + {{ 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 ffa3c20d9..50a331851 100644 --- a/openapi_python_client/templates/property_templates/file_property.pyi +++ b/openapi_python_client/templates/property_templates/file_property.pyi @@ -4,7 +4,7 @@ ) {% endmacro %} -{% macro transform(property, source, destination, declare_type=True) %} +{% macro transform(property, source, destination, declare_type=True, query_parameter=False) %} {% if property.required %} {% if property.nullable %} {{ destination }} = {{ source }}.to_tuple() if {{ source }} else None @@ -12,9 +12,9 @@ {{ destination }} = {{ source }}.to_tuple() {% endif %} {% else %} -{{ destination }}{% if declare_type %}: {{ property.get_type_string() }}{% endif %} = UNSET +{{ destination }}{% if declare_type %}: {{ property.get_type_string(query_parameter=query_parameter, json=True) }}{% endif %} = UNSET if not isinstance({{ source }}, Unset): -{% if property.nullable %} +{% if property.nullable or query_parameter %} {{ destination }} = {{ source }}.to_tuple() if {{ source }} else None {% else %} {{ destination }} = {{ source }}.to_tuple() diff --git a/openapi_python_client/templates/property_templates/list_property.pyi b/openapi_python_client/templates/property_templates/list_property.pyi index d05a13960..5f58bcd30 100644 --- a/openapi_python_client/templates/property_templates/list_property.pyi +++ b/openapi_python_client/templates/property_templates/list_property.pyi @@ -32,7 +32,7 @@ for {{ inner_source }} in {{ source }}: {% endmacro %} -{% macro transform(property, source, destination, declare_type=True) %} +{% macro transform(property, source, destination, declare_type=True, query_parameter=False) %} {% set inner_property = property.inner_property %} {% if property.required %} {% if property.nullable %} @@ -44,13 +44,13 @@ else: {{ _transform(property, source, destination) }} {% endif %} {% else %} -{{ destination }}{% if declare_type %}: Union[Unset, List[Any]]{% endif %} = UNSET +{{ destination }}{% if declare_type %}: {{ property.get_type_string(query_parameter=query_parameter, json=True) }}{% endif %} = UNSET if not isinstance({{ source }}, Unset): -{% if property.nullable %} +{% if property.nullable or query_parameter %} if {{ source }} is None: {{ destination }} = None else: - {{ _transform(property, source, destination) | indent(4)}} + {{ _transform(property, source, destination) | indent(8)}} {% else %} {{ _transform(property, source, destination) | indent(4)}} {% endif %} diff --git a/openapi_python_client/templates/property_templates/model_property.pyi b/openapi_python_client/templates/property_templates/model_property.pyi index e6746cb24..dfda61d97 100644 --- a/openapi_python_client/templates/property_templates/model_property.pyi +++ b/openapi_python_client/templates/property_templates/model_property.pyi @@ -15,7 +15,7 @@ if _{{ property.python_name }} is not None and not isinstance(_{{ property.pytho {% endif %} {% endmacro %} -{% macro transform(property, source, destination, declare_type=True) %} +{% macro transform(property, source, destination, declare_type=True, query_parameter=False) %} {% if property.required %} {% if property.nullable %} {{ destination }} = {{ source }}.to_dict() if {{ source }} else None @@ -23,9 +23,9 @@ if _{{ property.python_name }} is not None and not isinstance(_{{ property.pytho {{ destination }} = {{ source }}.to_dict() {% endif %} {% else %} -{{ destination }}{% if declare_type %}: Union[{% if property.nullable %}None, {% endif %}Unset, Dict[str, Any]]{% endif %} = UNSET +{{ destination }}{% if declare_type %}: {{ property.get_type_string(query_parameter=query_parameter, json=True) }}{% endif %} = UNSET if not isinstance({{ source }}, Unset): -{% if property.nullable %} +{% if property.nullable or query_parameter %} {{ destination }} = {{ source }}.to_dict() if {{ source }} else None {% else %} {{ destination }} = {{ source }}.to_dict() diff --git a/openapi_python_client/templates/property_templates/union_property.pyi b/openapi_python_client/templates/property_templates/union_property.pyi index 4c632c60a..509c7f34e 100644 --- a/openapi_python_client/templates/property_templates/union_property.pyi +++ b/openapi_python_client/templates/property_templates/union_property.pyi @@ -24,20 +24,20 @@ def _parse_{{ property.python_name }}(data: Any) -> {{ property.get_type_string( {{ property.python_name }} = _parse_{{ property.python_name }}({{ source }}) {% endmacro %} -{% macro transform(property, source, destination, declare_type=True) %} -{% if not property.required %} -{{ destination }}{% if declare_type %}: {{ property.get_type_string() }}{% endif %} +{% macro transform(property, source, destination, declare_type=True, query_parameter=False) %} +{% if not property.required or property.nullable %} +{{ destination }}{% if declare_type %}: {{ property.get_type_string(query_parameter=query_parameter, json=True) }}{% endif %} if isinstance({{ source }}, Unset): {{ destination }} = UNSET {% endif %} -{% if property.nullable %} +{% if property.nullable or (query_parameter and not property.required) %} {% if property.required %} if {{ source }} is None: {% else %}{# There's an if UNSET statement before this #} elif {{ source }} is None: {% endif %} - {{ destination }}{% if declare_type %}: {{ property.get_type_string() }}{% endif %} = None + {{ destination }} = None {% endif %} {% for inner_property in property.inner_properties_with_template() %} {% if loop.first and property.required and not property.nullable %}{# No if UNSET or if None statement before this #} diff --git a/openapi_python_client/templates/types.py b/openapi_python_client/templates/types.py index 2061b9f08..a354a2192 100644 --- a/openapi_python_client/templates/types.py +++ b/openapi_python_client/templates/types.py @@ -11,6 +11,9 @@ def __bool__(self) -> bool: UNSET: Unset = Unset() +# Used as `FileProperty._json_type_string` +FileJsonType = Tuple[Optional[str], Union[BinaryIO, TextIO], Optional[str]] + @attr.s(auto_attribs=True) class File: @@ -20,7 +23,7 @@ class File: file_name: Optional[str] = None mime_type: Optional[str] = None - def to_tuple(self) -> Tuple[Optional[str], Union[BinaryIO, TextIO], Optional[str]]: + def to_tuple(self) -> FileJsonType: """ Return a tuple representation that httpx will accept for multipart/form-data """ return self.file_name, self.payload, self.mime_type diff --git a/pyproject.toml b/pyproject.toml index a1de2316c..4be8d93db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ safety = "*" pytest-cov = "*" python-multipart = "*" flake8 = "*" +typer-cli = "^0.0.11" [tool.taskipy.tasks] check = """ @@ -55,8 +56,9 @@ isort .\ && flake8 openapi_python_client\ && safety check --bare\ && mypy openapi_python_client\ - && pytest --cov openapi_python_client tests --cov-report=term-missing\ + && task unit\ """ +unit = "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" e2e = "pytest openapi_python_client end_to_end_tests/test_end_to_end.py" @@ -65,6 +67,7 @@ task regen\ && task regen_custom\ && task e2e\ """ +docs = "typer openapi_python_client/cli.py utils docs > usage.md" [tool.black] line-length = 120 diff --git a/setup.py b/setup.py new file mode 100644 index 000000000..c38ef4ea0 --- /dev/null +++ b/setup.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +from setuptools import setup + +packages = [ + "openapi_python_client", + "openapi_python_client.parser", + "openapi_python_client.parser.properties", + "openapi_python_client.schema", + "openapi_python_client.templates", +] + +package_data = {"": ["*"], "openapi_python_client.templates": ["property_templates/*"]} + +install_requires = [ + "attrs>=20.1.0,<21.0.0", + "autoflake>=1.4,<2.0", + "black>=20.8b1", + "httpx>=0.15.4,<0.17.0", + "isort>=5.0.5,<6.0.0", + "jinja2>=2.11.1,<3.0.0", + "pydantic>=1.6.1,<2.0.0", + "python-dateutil>=2.8.1,<3.0.0", + "pyyaml>=5.3.1,<6.0.0", + "shellingham>=1.3.2,<2.0.0", + "stringcase>=1.2.0,<2.0.0", + "typer>=0.3,<0.4", +] + +extras_require = { + ':python_version < "3.8"': ["importlib_metadata>=2.0.0,<3.0.0"], + ':sys_platform == "win32"': ["colorama>=0.4.3,<0.5.0"], +} + +entry_points = {"console_scripts": ["openapi-python-client = openapi_python_client.cli:app"]} + +setup_kwargs = { + "name": "openapi-python-client", + "version": "0.7.0", + "description": "Generate modern Python clients from OpenAPI", + "long_description": '[![triaxtec](https://circleci.com/gh/triaxtec/openapi-python-client.svg?style=svg)](https://circleci.com/gh/triaxtec/openapi-python-client)\n[![codecov](https://codecov.io/gh/triaxtec/openapi-python-client/branch/main/graph/badge.svg)](https://codecov.io/gh/triaxtec/openapi-python-client)\n[![MIT license](https://img.shields.io/badge/License-MIT-blue.svg)](https://lbesson.mit-license.org/)\n[![Generic badge](https://img.shields.io/badge/type_checked-mypy-informational.svg)](https://mypy.readthedocs.io/en/stable/introduction.html)\n[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)\n[![PyPI version shields.io](https://img.shields.io/pypi/v/openapi-python-client.svg)](https://pypi.python.org/pypi/openapi-python-client/)\n[![Downloads](https://static.pepy.tech/personalized-badge/openapi-python-client?period=total&units=international_system&left_color=blue&right_color=green&left_text=Downloads)](https://pepy.tech/project/openapi-python-client)\n\n# openapi-python-client\n\nGenerate modern Python clients from OpenAPI 3.x documents.\n\n_This generator does not support OpenAPI 2.x FKA Swagger. If you need to use an older document, try upgrading it to\nversion 3 first with one of many available converters._\n\n**This project is still in development and does not support all OpenAPI features**\n\n## Why This?\n\nThe Python clients generated by openapi-generator support Python 2 and therefore come with a lot of baggage. This tool\naims to generate clients which:\n\n1. Use all the latest and greatest Python features like type annotations and dataclasses\n1. Don\'t carry around a bunch of compatibility code for older version of Python (e.g. the `six` package)\n1. Have better documentation and more obvious usage instructions\n\nAdditionally, because this generator is written in Python, it should be more accessible to contribution by the people\nusing it (Python developers).\n\n## Installation\n\nI recommend you install with [pipx](https://pipxproject.github.io/pipx/) so you don\'t conflict with any other packages\nyou might have: `pipx install openapi-python-client`.\n\nBetter yet, use `pipx run openapi-python-client ` to always use the latest version of the generator.\n\nYou can install with normal pip if you want to though: `pip install openapi-python-client`\n\nThen, if you want tab completion: `openapi-python-client --install-completion`\n\n## Usage\n\n### Create a new client\n\n`openapi-python-client generate --url https://my.api.com/openapi.json`\n\nThis will generate a new client library named based on the title in your OpenAPI spec. For example, if the title\nof your API is "My API", the expected output will be "my-api-client". If a folder already exists by that name, you\'ll\nget an error.\n\n### Update an existing client\n\n`openapi-python-client update --url https://my.api.com/openapi.json`\n\n> For more usage details run `openapi-python-client --help` or read [usage](usage.md)\n\n### Using custom templates\n\nThis feature leverages Jinja2\'s [ChoiceLoader](https://jinja.palletsprojects.com/en/2.11.x/api/#jinja2.ChoiceLoader) and [FileSystemLoader](https://jinja.palletsprojects.com/en/2.11.x/api/#jinja2.FileSystemLoader). This means you do _not_ need to customize every template. Simply copy the template(s) you want to customize from [the default template directory](openapi_python_client/templates) to your own custom template directory (file names _must_ match exactly) and pass the template directory through the `custom-template-path` flag to the `generate` and `update` commands. For instance,\n\n```\nopenapi-python-client update \\\n --url https://my.api.com/openapi.json \\\n --custom-template-path=relative/path/to/mytemplates\n```\n\n_Be forewarned, this is a beta-level feature in the sense that the API exposed in the templates is undocumented and unstable._\n\n## What You Get\n\n1. A `pyproject.toml` file with some basic metadata intended to be used with [Poetry].\n1. A `README.md` you\'ll most definitely need to update with your project\'s details\n1. A Python module named just like the auto-generated project name (e.g. "my_api_client") which contains:\n 1. A `client` module which will have both a `Client` class and an `AuthenticatedClient` class. You\'ll need these\n for calling the functions in the `api` module.\n 1. An `api` module which will contain one module for each tag in your OpenAPI spec, as well as a `default` module\n for endpoints without a tag. Each of these modules in turn contains one function for calling each endpoint.\n 1. A `models` module which has all the classes defined by the various schemas in your OpenAPI spec\n\nFor a full example you can look at the `end_to_end_tests` directory which has an `openapi.json` file.\n"golden-record" in that same directory is the generated client from that OpenAPI document.\n\n## OpenAPI features supported\n\n1. All HTTP Methods\n1. JSON and form bodies, path and query parameters\n1. File uploads with multipart/form-data bodies\n1. float, string, int, date, datetime, string enums, and custom schemas or lists containing any of those\n1. html/text or application/json responses containing any of the previous types\n1. Bearer token security\n\n## Configuration\n\nYou can pass a YAML (or JSON) file to openapi-python-client with the `--config` option in order to change some behavior.\nThe following parameters are supported:\n\n### class_overrides\n\nUsed to change the name of generated model classes. This param should be a mapping of existing class name\n(usually a key in the "schemas" section of your OpenAPI document) to class_name and module_name. As an example, if the\nname of the a model in OpenAPI (and therefore the generated class name) was something like "\\_PrivateInternalLongName"\nand you want the generated client\'s model to be called "ShortName" in a module called "short_name" you could do this:\n\nExample:\n\n```yaml\nclass_overrides:\n _PrivateInternalLongName:\n class_name: ShortName\n module_name: short_name\n```\n\nThe easiest way to find what needs to be overridden is probably to generate your client and go look at everything in the\nmodels folder.\n\n### project_name_override and package_name_override\n\nUsed to change the name of generated client library project/package. If the project name is changed but an override for the package name\nisn\'t provided, the package name will be converted from the project name using the standard convention (replacing `-`\'s with `_`\'s).\n\nExample:\n\n```yaml\nproject_name_override: my-special-project-name\npackage_name_override: my_extra_special_package_name\n```\n\n### field_prefix\n\nWhen generating properties, the `name` attribute of the OpenAPI schema will be used. When the `name` is not a valid\nPython identifier (e.g. begins with a number) this string will be prepended. Defaults to "field\\_".\n\nExample:\n\n```yaml\nfield_prefix: attr_\n```\n\n### package_version_override\n\nSpecify the package version of the generated client. If unset, the client will use the version of the OpenAPI spec.\n\nExample:\n\n```yaml\npackage_version_override: 1.2.3\n```\n\n[changelog.md]: CHANGELOG.md\n[poetry]: https://python-poetry.org/\n', + "author": "Dylan Anthony", + "author_email": "danthony@triaxtec.com", + "maintainer": None, + "maintainer_email": None, + "url": "https://github.com/triaxtec/openapi-python-client", + "packages": packages, + "package_data": package_data, + "install_requires": install_requires, + "extras_require": extras_require, + "entry_points": entry_points, + "python_requires": ">=3.6,<4.0", +} + + +setup(**setup_kwargs) diff --git a/tests/test_parser/test_properties/test_init.py b/tests/test_parser/test_properties/test_init.py index a7ea05881..2f1c71cc5 100644 --- a/tests/test_parser/test_properties/test_init.py +++ b/tests/test_parser/test_properties/test_init.py @@ -16,45 +16,55 @@ class TestProperty: - def test_get_type_string(self, mocker): + @pytest.mark.parametrize( + "query_parameter,nullable,required,no_optional,expected", + [ + (False, False, False, False, "Union[Unset, TestType]"), + (False, False, False, True, "TestType"), + (False, False, True, False, "TestType"), + (False, False, True, True, "TestType"), + (False, True, False, False, "Union[Unset, None, TestType]"), + (False, True, False, True, "TestType"), + (False, True, True, False, "Optional[TestType]"), + (False, True, True, True, "TestType"), + (True, False, False, False, "Union[Unset, None, TestType]"), + (True, False, False, True, "TestType"), + (True, False, True, False, "TestType"), + (True, False, True, True, "TestType"), + (True, True, False, False, "Union[Unset, None, TestType]"), + (True, True, False, True, "TestType"), + (True, True, True, False, "Optional[TestType]"), + (True, True, True, True, "TestType"), + ], + ) + def test_get_type_string(self, mocker, query_parameter, nullable, required, no_optional, expected): from openapi_python_client.parser.properties import Property mocker.patch.object(Property, "_type_string", "TestType") - p = Property(name="test", required=True, default=None, nullable=False) - - base_type_string = f"TestType" - - assert p.get_type_string() == base_type_string + p = Property(name="test", required=required, default=None, nullable=nullable) + assert p.get_type_string(no_optional=no_optional, query_parameter=query_parameter) == expected - p = Property(name="test", required=True, default=None, nullable=True) - assert p.get_type_string() == f"Optional[{base_type_string}]" - assert p.get_type_string(no_optional=True) == base_type_string - - p = Property(name="test", required=False, default=None, nullable=True) - assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" - assert p.get_type_string(no_optional=True) == base_type_string - - p = Property(name="test", required=False, default=None, 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): + @pytest.mark.parametrize( + "query_parameter,default,required,expected", + [ + (False, None, False, "test: Union[Unset, TestType] = UNSET"), + (False, None, True, "test: TestType"), + (False, "Test", False, "test: Union[Unset, TestType] = Test"), + (False, "Test", True, "test: TestType = Test"), + (True, None, False, "test: Union[Unset, None, TestType] = UNSET"), + (True, None, True, "test: TestType"), + (True, "Test", False, "test: Union[Unset, None, TestType] = Test"), + (True, "Test", True, "test: TestType = Test"), + ], + ) + def test_to_string(self, mocker, query_parameter, default, required, expected): from openapi_python_client.parser.properties import Property name = "test" - get_type_string = mocker.patch.object(Property, "get_type_string") - p = Property(name=name, required=True, default=None, nullable=False) - - assert p.to_string() == f"{name}: {get_type_string()}" - - p = Property(name=name, required=False, default=None, nullable=False) - assert p.to_string() == f"{name}: {get_type_string()} = UNSET" - - p = Property(name=name, required=True, default=None, nullable=False) - assert p.to_string() == f"{name}: {get_type_string()}" + mocker.patch.object(Property, "_type_string", "TestType") + p = Property(name=name, required=required, default=default, nullable=False) - p = Property(name=name, required=True, default="TEST", nullable=False) - assert p.to_string() == f"{name}: {get_type_string()} = TEST" + assert p.to_string(query_parameter=query_parameter) == expected def test_get_imports(self): from openapi_python_client.parser.properties import Property @@ -87,7 +97,7 @@ def test_get_type_string(self): assert p.get_type_string() == f"Optional[{base_type_string}]" p = StringProperty(name="test", required=False, default=None, nullable=True) - assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" + assert p.get_type_string() == f"Union[Unset, None, {base_type_string}]" p = StringProperty(name="test", required=False, default=None, nullable=False) assert p.get_type_string() == f"Union[Unset, {base_type_string}]" @@ -202,7 +212,7 @@ def test_get_type_string(self, mocker): assert p.get_type_string(no_optional=True) == base_type_string p = ListProperty(name="test", required=False, default=None, inner_property=inner_property, nullable=True) - assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" + assert p.get_type_string() == f"Union[Unset, None, {base_type_string}]" assert p.get_type_string(no_optional=True) == base_type_string p = ListProperty(name="test", required=False, default=None, inner_property=inner_property, nullable=False) @@ -242,7 +252,28 @@ def test_get_type_imports(self, mocker): class TestUnionProperty: - def test_get_type_string(self, mocker): + @pytest.mark.parametrize( + "query_parameter,nullable,required,no_optional,expected", + [ + (False, False, False, False, "Union[Unset, inner_type_string_1, inner_type_string_2]"), + (False, False, False, True, "Union[inner_type_string_1, inner_type_string_2]"), + (False, False, True, False, "Union[inner_type_string_1, inner_type_string_2]"), + (False, False, True, True, "Union[inner_type_string_1, inner_type_string_2]"), + (False, True, False, False, "Union[Unset, None, inner_type_string_1, inner_type_string_2]"), + (False, True, False, True, "Union[inner_type_string_1, inner_type_string_2]"), + (False, True, True, False, "Union[None, inner_type_string_1, inner_type_string_2]"), + (False, True, True, True, "Union[inner_type_string_1, inner_type_string_2]"), + (True, False, False, False, "Union[Unset, None, inner_type_string_1, inner_type_string_2]"), + (True, False, False, True, "Union[inner_type_string_1, inner_type_string_2]"), + (True, False, True, False, "Union[inner_type_string_1, inner_type_string_2]"), + (True, False, True, True, "Union[inner_type_string_1, inner_type_string_2]"), + (True, True, False, False, "Union[Unset, None, inner_type_string_1, inner_type_string_2]"), + (True, True, False, True, "Union[inner_type_string_1, inner_type_string_2]"), + (True, True, True, False, "Union[None, inner_type_string_1, inner_type_string_2]"), + (True, True, True, True, "Union[inner_type_string_1, inner_type_string_2]"), + ], + ) + def test_get_type_string(self, mocker, query_parameter, nullable, required, no_optional, expected): from openapi_python_client.parser.properties import UnionProperty inner_property_1 = mocker.MagicMock() @@ -251,46 +282,13 @@ def test_get_type_string(self, mocker): inner_property_2.get_type_string.return_value = "inner_type_string_2" p = UnionProperty( name="test", - required=True, - default=None, - inner_properties=[inner_property_1, inner_property_2], - nullable=False, - ) - - base_type_string = f"Union[inner_type_string_1, inner_type_string_2]" - - assert p.get_type_string() == base_type_string - - p = UnionProperty( - name="test", - required=True, - default=None, - inner_properties=[inner_property_1, inner_property_2], - 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 = UnionProperty( - name="test", - required=False, + required=required, default=None, inner_properties=[inner_property_1, inner_property_2], - nullable=True, + nullable=nullable, ) - assert p.get_type_string() == f"Optional[{base_type_string_with_unset}]" - assert p.get_type_string(no_optional=True) == base_type_string - p = UnionProperty( - name="test", - required=False, - default=None, - inner_properties=[inner_property_1, inner_property_2], - nullable=False, - ) - assert p.get_type_string() == base_type_string_with_unset - assert p.get_type_string(no_optional=True) == base_type_string + assert p.get_type_string(query_parameter=query_parameter, no_optional=no_optional) == expected def test_get_imports(self, mocker): from openapi_python_client.parser.properties import UnionProperty @@ -389,7 +387,7 @@ def test_get_type_string(self, mocker): reference=fake_reference, value_type=str, ) - assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" + assert p.get_type_string() == f"Union[Unset, None, {base_type_string}]" assert p.get_type_string(no_optional=True) == base_type_string p = properties.EnumProperty( diff --git a/tests/test_parser/test_properties/test_model_property.py b/tests/test_parser/test_properties/test_model_property.py index 1024ef179..410112666 100644 --- a/tests/test_parser/test_properties/test_model_property.py +++ b/tests/test_parser/test_properties/test_model_property.py @@ -4,9 +4,9 @@ @pytest.mark.parametrize( "no_optional,nullable,required,expected", [ - (False, False, False, "Union[MyClass, Unset]"), + (False, False, False, "Union[Unset, MyClass]"), (False, False, True, "MyClass"), - (False, True, False, "Union[Optional[MyClass], Unset]"), + (False, True, False, "Union[Unset, None, MyClass]"), (False, True, True, "Optional[MyClass]"), (True, False, False, "MyClass"), (True, False, True, "MyClass"), diff --git a/tests/test_templates/test_property_templates/test_date_property/optional_nullable.py b/tests/test_templates/test_property_templates/test_date_property/optional_nullable.py index cf8780024..be32cfbd3 100644 --- a/tests/test_templates/test_property_templates/test_date_property/optional_nullable.py +++ b/tests/test_templates/test_property_templates/test_date_property/optional_nullable.py @@ -6,7 +6,7 @@ some_source = date(2020, 10, 12) -some_destination: Union[Unset, str] = UNSET +some_destination: Union[Unset, None, str] = UNSET if not isinstance(some_source, Unset): some_destination = some_source.isoformat() if some_source else None