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 fa6e9e47c..574d5018b 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 @@ -71,21 +71,20 @@ def httpx_request( 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_union_prop_with_ref: Union[Unset, float, AnEnum] if isinstance(union_prop_with_ref, Unset): json_union_prop_with_ref = UNSET - elif isinstance(union_prop_with_ref, float): - json_union_prop_with_ref = union_prop_with_ref - else: + elif isinstance(union_prop_with_ref, AnEnum): json_union_prop_with_ref = UNSET if not isinstance(union_prop_with_ref, Unset): json_union_prop_with_ref = union_prop_with_ref + else: + json_union_prop_with_ref = union_prop_with_ref + json_enum_prop: Union[Unset, AnEnum] = UNSET if not isinstance(enum_prop, Unset): json_enum_prop = enum_prop 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 475172136..d3ca924b3 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 @@ -12,6 +12,8 @@ ModelWithAdditionalPropertiesInlinedAdditionalProperty, ) from .model_with_additional_properties_refed import ModelWithAdditionalPropertiesRefed +from .model_with_any_json_properties import ModelWithAnyJsonProperties +from .model_with_any_json_properties_additional_property import ModelWithAnyJsonPropertiesAdditionalProperty from .model_with_primitive_additional_properties import ModelWithPrimitiveAdditionalProperties from .model_with_primitive_additional_properties_a_date_holder import ModelWithPrimitiveAdditionalPropertiesADateHolder from .model_with_union_property import ModelWithUnionProperty diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/models/model_with_any_json_properties.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/model_with_any_json_properties.py new file mode 100644 index 000000000..2e488ea69 --- /dev/null +++ b/end_to_end_tests/golden-record-custom/custom_e2e/models/model_with_any_json_properties.py @@ -0,0 +1,88 @@ +from typing import Any, Dict, List, Union, cast + +import attr + +from ..models.model_with_any_json_properties_additional_property import ModelWithAnyJsonPropertiesAdditionalProperty +from ..types import Unset + + +@attr.s(auto_attribs=True) +class ModelWithAnyJsonProperties: + """ """ + + additional_properties: Dict[ + str, Union[ModelWithAnyJsonPropertiesAdditionalProperty, List[str], str, float, int, bool] + ] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + + field_dict: Dict[str, Any] = {} + for prop_name, prop in self.additional_properties.items(): + if isinstance(prop, ModelWithAnyJsonPropertiesAdditionalProperty): + field_dict[prop_name] = prop.to_dict() + + elif isinstance(prop, list): + field_dict[prop_name] = prop + + else: + field_dict[prop_name] = prop + + field_dict.update({}) + + return field_dict + + @staticmethod + def from_dict(src_dict: Dict[str, Any]) -> "ModelWithAnyJsonProperties": + d = src_dict.copy() + model_with_any_json_properties = ModelWithAnyJsonProperties() + + additional_properties = {} + for prop_name, prop_dict in d.items(): + + def _parse_additional_property( + data: Any, + ) -> Union[ModelWithAnyJsonPropertiesAdditionalProperty, List[str], str, float, int, bool]: + data = None if isinstance(data, Unset) else data + additional_property: Union[ + ModelWithAnyJsonPropertiesAdditionalProperty, List[str], str, float, int, bool + ] + try: + additional_property = ModelWithAnyJsonPropertiesAdditionalProperty.from_dict(data) + + return additional_property + except: # noqa: E722 + pass + try: + additional_property = cast(List[str], data) + + return additional_property + except: # noqa: E722 + pass + return cast(str, data) + + additional_property = _parse_additional_property(prop_dict) + + additional_properties[prop_name] = additional_property + + model_with_any_json_properties.additional_properties = additional_properties + return model_with_any_json_properties + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__( + self, key: str + ) -> Union[ModelWithAnyJsonPropertiesAdditionalProperty, List[str], str, float, int, bool]: + return self.additional_properties[key] + + def __setitem__( + self, key: str, value: Union[ModelWithAnyJsonPropertiesAdditionalProperty, List[str], str, float, int, bool] + ) -> 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_any_json_properties_additional_property.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/model_with_any_json_properties_additional_property.py new file mode 100644 index 000000000..c823a73ed --- /dev/null +++ b/end_to_end_tests/golden-record-custom/custom_e2e/models/model_with_any_json_properties_additional_property.py @@ -0,0 +1,42 @@ +from typing import Any, Dict, List + +import attr + + +@attr.s(auto_attribs=True) +class ModelWithAnyJsonPropertiesAdditionalProperty: + """ """ + + additional_properties: Dict[str, str] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + + return field_dict + + @staticmethod + def from_dict(src_dict: Dict[str, Any]) -> "ModelWithAnyJsonPropertiesAdditionalProperty": + d = src_dict.copy() + model_with_any_json_properties_additional_property = ModelWithAnyJsonPropertiesAdditionalProperty() + + model_with_any_json_properties_additional_property.additional_properties = d + return model_with_any_json_properties_additional_property + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> str: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: str) -> 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/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 e4e7a7c73..9242cddaa 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 @@ -47,21 +47,20 @@ def _get_kwargs( 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_union_prop_with_ref: Union[Unset, float, AnEnum] if isinstance(union_prop_with_ref, Unset): json_union_prop_with_ref = UNSET - elif isinstance(union_prop_with_ref, float): - json_union_prop_with_ref = union_prop_with_ref - else: + elif isinstance(union_prop_with_ref, AnEnum): json_union_prop_with_ref = UNSET if not isinstance(union_prop_with_ref, Unset): json_union_prop_with_ref = union_prop_with_ref + else: + json_union_prop_with_ref = union_prop_with_ref + json_enum_prop: Union[Unset, AnEnum] = UNSET if not isinstance(enum_prop, Unset): json_enum_prop = enum_prop 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 475172136..d3ca924b3 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 @@ -12,6 +12,8 @@ ModelWithAdditionalPropertiesInlinedAdditionalProperty, ) from .model_with_additional_properties_refed import ModelWithAdditionalPropertiesRefed +from .model_with_any_json_properties import ModelWithAnyJsonProperties +from .model_with_any_json_properties_additional_property import ModelWithAnyJsonPropertiesAdditionalProperty from .model_with_primitive_additional_properties import ModelWithPrimitiveAdditionalProperties from .model_with_primitive_additional_properties_a_date_holder import ModelWithPrimitiveAdditionalPropertiesADateHolder from .model_with_union_property import ModelWithUnionProperty diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties.py new file mode 100644 index 000000000..2e488ea69 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties.py @@ -0,0 +1,88 @@ +from typing import Any, Dict, List, Union, cast + +import attr + +from ..models.model_with_any_json_properties_additional_property import ModelWithAnyJsonPropertiesAdditionalProperty +from ..types import Unset + + +@attr.s(auto_attribs=True) +class ModelWithAnyJsonProperties: + """ """ + + additional_properties: Dict[ + str, Union[ModelWithAnyJsonPropertiesAdditionalProperty, List[str], str, float, int, bool] + ] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + + field_dict: Dict[str, Any] = {} + for prop_name, prop in self.additional_properties.items(): + if isinstance(prop, ModelWithAnyJsonPropertiesAdditionalProperty): + field_dict[prop_name] = prop.to_dict() + + elif isinstance(prop, list): + field_dict[prop_name] = prop + + else: + field_dict[prop_name] = prop + + field_dict.update({}) + + return field_dict + + @staticmethod + def from_dict(src_dict: Dict[str, Any]) -> "ModelWithAnyJsonProperties": + d = src_dict.copy() + model_with_any_json_properties = ModelWithAnyJsonProperties() + + additional_properties = {} + for prop_name, prop_dict in d.items(): + + def _parse_additional_property( + data: Any, + ) -> Union[ModelWithAnyJsonPropertiesAdditionalProperty, List[str], str, float, int, bool]: + data = None if isinstance(data, Unset) else data + additional_property: Union[ + ModelWithAnyJsonPropertiesAdditionalProperty, List[str], str, float, int, bool + ] + try: + additional_property = ModelWithAnyJsonPropertiesAdditionalProperty.from_dict(data) + + return additional_property + except: # noqa: E722 + pass + try: + additional_property = cast(List[str], data) + + return additional_property + except: # noqa: E722 + pass + return cast(str, data) + + additional_property = _parse_additional_property(prop_dict) + + additional_properties[prop_name] = additional_property + + model_with_any_json_properties.additional_properties = additional_properties + return model_with_any_json_properties + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__( + self, key: str + ) -> Union[ModelWithAnyJsonPropertiesAdditionalProperty, List[str], str, float, int, bool]: + return self.additional_properties[key] + + def __setitem__( + self, key: str, value: Union[ModelWithAnyJsonPropertiesAdditionalProperty, List[str], str, float, int, bool] + ) -> 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_any_json_properties_additional_property.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties_additional_property.py new file mode 100644 index 000000000..c823a73ed --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties_additional_property.py @@ -0,0 +1,42 @@ +from typing import Any, Dict, List + +import attr + + +@attr.s(auto_attribs=True) +class ModelWithAnyJsonPropertiesAdditionalProperty: + """ """ + + additional_properties: Dict[str, str] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + + return field_dict + + @staticmethod + def from_dict(src_dict: Dict[str, Any]) -> "ModelWithAnyJsonPropertiesAdditionalProperty": + d = src_dict.copy() + model_with_any_json_properties_additional_property = ModelWithAnyJsonPropertiesAdditionalProperty() + + model_with_any_json_properties_additional_property.additional_properties = d + return model_with_any_json_properties_additional_property + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> str: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: str) -> 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/openapi.json b/end_to_end_tests/openapi.json index 1cbfd9597..2a0c1a644 100644 --- a/end_to_end_tests/openapi.json +++ b/end_to_end_tests/openapi.json @@ -806,6 +806,38 @@ "additionalProperties": { "$ref": "#/components/schemas/AnEnum" } + }, + "ModelWithAnyJsonProperties": { + "title": "ModelWithAnyJsonProperties", + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "integer" + }, + { + "type": "boolean" + } + ] + } } } } diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index 98fc4fc14..b5c47c0ea 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -1,3 +1,4 @@ +import builtins from itertools import chain from typing import Any, ClassVar, Dict, Generic, Iterable, List, Optional, Set, Tuple, TypeVar, Union @@ -135,6 +136,10 @@ def get_type_string(self, no_optional: bool = False) -> str: type_string = f"Union[Unset, {type_string}]" return type_string + def get_instance_type_string(self) -> str: + """Get a string representation of runtime type that should be used for `isinstance` checks""" + return "list" + def get_imports(self, *, prefix: str) -> Set[str]: """ Get a set of import strings that should be included when this property is used somewhere @@ -183,6 +188,14 @@ def get_imports(self, *, prefix: str) -> Set[str]: imports.add("from typing import Union") return imports + @builtins.property + def inner_properties_with_template(self) -> List[Property]: + return [prop for prop in self.inner_properties if prop.template] + + @builtins.property + def inner_properties_without_template(self) -> List[Property]: + return [prop for prop in self.inner_properties if not prop.template] + def _string_based_property( name: str, required: bool, data: oai.Schema diff --git a/openapi_python_client/parser/properties/property.py b/openapi_python_client/parser/properties/property.py index c7649200f..0b7047551 100644 --- a/openapi_python_client/parser/properties/property.py +++ b/openapi_python_client/parser/properties/property.py @@ -48,6 +48,10 @@ def get_type_string(self, no_optional: bool = False) -> str: type_string = f"Union[Unset, {type_string}]" return type_string + def get_instance_type_string(self) -> str: + """Get a string representation of runtime type that should be used for `isinstance` checks""" + return self.get_type_string(no_optional=True) + # noinspection PyUnusedLocal def get_imports(self, *, prefix: str) -> Set[str]: """ diff --git a/openapi_python_client/templates/model.pyi b/openapi_python_client/templates/model.pyi index c66ba46a3..0c81a32f9 100644 --- a/openapi_python_client/templates/model.pyi +++ b/openapi_python_client/templates/model.pyi @@ -51,7 +51,7 @@ class {{ model.reference.class_name }}: {% if model.additional_properties.template %} {% from "property_templates/" + model.additional_properties.template import transform %} for prop_name, prop in self.additional_properties.items(): - {{ transform(model.additional_properties, "prop", "field_dict[prop_name]") | indent(4) }} + {{ transform(model.additional_properties, "prop", "field_dict[prop_name]") | indent(12) }} {% else %} field_dict.update(self.additional_properties) {% endif %} diff --git a/openapi_python_client/templates/property_templates/union_property.pyi b/openapi_python_client/templates/property_templates/union_property.pyi index 114838b1c..c3cfee05e 100644 --- a/openapi_python_client/templates/property_templates/union_property.pyi +++ b/openapi_python_client/templates/property_templates/union_property.pyi @@ -2,22 +2,24 @@ def _parse_{{ property.python_name }}(data: Any) -> {{ property.get_type_string() }}: data = None if isinstance(data, Unset) else data {{ property.python_name }}: {{ property.get_type_string() }} - {% for inner_property in property.inner_properties %} - {% if inner_property.template and not loop.last %} + {% for inner_property in property.inner_properties_with_template %} + {% if not loop.last or property.inner_properties_without_template %} try: {% from "property_templates/" + inner_property.template import construct %} {{ construct(inner_property, "data", initial_value="UNSET") | indent(8) }} return {{ property.python_name }} except: # noqa: E722 pass - {% elif inner_property.template and loop.last %}{# Don't do try/except for the last one #} + {% else %}{# Don't do try/except for the last one #} {% from "property_templates/" + inner_property.template import construct %} {{ construct(inner_property, "data", initial_value="UNSET") | indent(4) }} return {{ property.python_name }} - {% else %} - return {{ source }} {% endif %} {% endfor %} + {% if property.inner_properties_without_template %} + {# Doesn't really matter what we cast it to as this type will be erased, so cast to one of the options #} + return cast({{ property.inner_properties_without_template[0].get_type_string() }}, data) + {% endif %} {{ property.python_name }} = _parse_{{ property.python_name }}({{ source }}) {% endmacro %} @@ -37,19 +39,22 @@ elif {{ source }} is None: {% endif %} {{ destination }}{% if declare_type %}: {{ property.get_type_string() }}{% endif %} = None {% endif %} -{% for inner_property in property.inner_properties %} +{% 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 #} -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) }}): +if isinstance({{ source }}, {{ inner_property.get_instance_type_string() }}): + {% elif not loop.last or property.inner_properties_without_template %} +elif isinstance({{ source }}, {{ inner_property.get_instance_type_string() }}): {% else %} else: {% endif %} -{% if inner_property.template %} {% from "property_templates/" + inner_property.template import transform %} {{ transform(inner_property, source, destination, declare_type=False) | indent(4) }} -{% else %} +{% endfor %} +{% if property.inner_properties_without_template and (property.inner_properties_with_template or not property.required)%} +else: {{ destination }} = {{ source }} +{% elif property.inner_properties_without_template %} +{{ destination }} = {{ source }} {% endif %} -{% endfor %} + {% endmacro %}