From ca2a18be6822831d16e5c33873536ee77248f373 Mon Sep 17 00:00:00 2001 From: Packy Gallagher Date: Tue, 15 Dec 2020 10:23:03 -0800 Subject: [PATCH 01/11] Fix indentation of union-ed addtionalProperties --- openapi_python_client/templates/model.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 %} From ab6622d48c7e9c469cbfbe8d74aec82c17cae2cf Mon Sep 17 00:00:00 2001 From: Packy Gallagher Date: Tue, 15 Dec 2020 13:33:16 -0800 Subject: [PATCH 02/11] Typing fixes check for list not List in isinstance cast primitives returned by union property's _parse_* --- openapi_python_client/parser/properties/__init__.py | 4 ++++ openapi_python_client/parser/properties/property.py | 4 ++++ .../templates/property_templates/union_property.pyi | 6 +++--- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index 98fc4fc14..746de4bdd 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -135,6 +135,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): + """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 diff --git a/openapi_python_client/parser/properties/property.py b/openapi_python_client/parser/properties/property.py index c7649200f..e820c72ce 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): + """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/property_templates/union_property.pyi b/openapi_python_client/templates/property_templates/union_property.pyi index 114838b1c..ee5aa1f3b 100644 --- a/openapi_python_client/templates/property_templates/union_property.pyi +++ b/openapi_python_client/templates/property_templates/union_property.pyi @@ -15,7 +15,7 @@ def _parse_{{ property.python_name }}(data: Any) -> {{ property.get_type_string( {{ construct(inner_property, "data", initial_value="UNSET") | indent(4) }} return {{ property.python_name }} {% else %} - return {{ source }} + return cast({{ inner_property.get_type_string() }}, {{ source }}) {% endif %} {% endfor %} @@ -39,9 +39,9 @@ elif {{ source }} is None: {% endif %} {% for inner_property in property.inner_properties %} {% if loop.first and property.required and not property.nullable %}{# No if UNSET or if None statement before this #} -if isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True) }}): +if isinstance({{ source }}, {{ inner_property.get_instance_type_string() }}): {% elif not loop.last %} -elif isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True) }}): +elif isinstance({{ source }}, {{ inner_property.get_instance_type_string() }}): {% else %} else: {% endif %} From 45d18013045bc3496524f657e518821b88cc2e88 Mon Sep 17 00:00:00 2001 From: Packy Gallagher Date: Tue, 15 Dec 2020 13:33:41 -0800 Subject: [PATCH 03/11] Add ModelWithAnyJsonProperties to e2e tests --- end_to_end_tests/openapi.json | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) 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" + } + ] + } } } } From 85fc68591683606047dd9443b1b542fdce0d8fe1 Mon Sep 17 00:00:00 2001 From: Packy Gallagher Date: Tue, 15 Dec 2020 13:33:51 -0800 Subject: [PATCH 04/11] Regen golden record --- .../custom_e2e/models/__init__.py | 2 + .../models/model_with_any_json_properties.py | 97 +++++++++++++++++++ ...any_json_properties_additional_property.py | 42 ++++++++ .../my_test_api_client/models/__init__.py | 2 + .../models/model_with_any_json_properties.py | 97 +++++++++++++++++++ ...any_json_properties_additional_property.py | 42 ++++++++ 6 files changed, 282 insertions(+) create mode 100644 end_to_end_tests/golden-record-custom/custom_e2e/models/model_with_any_json_properties.py create mode 100644 end_to_end_tests/golden-record-custom/custom_e2e/models/model_with_any_json_properties_additional_property.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties_additional_property.py 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..aac19fe73 --- /dev/null +++ b/end_to_end_tests/golden-record-custom/custom_e2e/models/model_with_any_json_properties.py @@ -0,0 +1,97 @@ +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 + + elif isinstance(prop, str): + field_dict[prop_name] = prop + elif isinstance(prop, float): + field_dict[prop_name] = prop + elif isinstance(prop, int): + 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, prop_dict) + return cast(float, prop_dict) + return cast(int, prop_dict) + return cast(bool, prop_dict) + + 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/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..aac19fe73 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties.py @@ -0,0 +1,97 @@ +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 + + elif isinstance(prop, str): + field_dict[prop_name] = prop + elif isinstance(prop, float): + field_dict[prop_name] = prop + elif isinstance(prop, int): + 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, prop_dict) + return cast(float, prop_dict) + return cast(int, prop_dict) + return cast(bool, prop_dict) + + 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 From bd59aa65094472fe777600a94a9f9e3600301a14 Mon Sep 17 00:00:00 2001 From: Packy Gallagher Date: Tue, 15 Dec 2020 14:36:45 -0800 Subject: [PATCH 05/11] Add return type for get_instance_type_string --- openapi_python_client/parser/properties/__init__.py | 2 +- openapi_python_client/parser/properties/property.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index 746de4bdd..4708bdc14 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -135,7 +135,7 @@ 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): + def get_instance_type_string(self) -> str: """Get a string representation of runtime type that should be used for `isinstance` checks""" return "list" diff --git a/openapi_python_client/parser/properties/property.py b/openapi_python_client/parser/properties/property.py index e820c72ce..0b7047551 100644 --- a/openapi_python_client/parser/properties/property.py +++ b/openapi_python_client/parser/properties/property.py @@ -48,7 +48,7 @@ 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): + 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) From c5f21b58f72cb06774ce602c5d4235f650a2467c Mon Sep 17 00:00:00 2001 From: Packy Gallagher Date: Wed, 16 Dec 2020 11:09:03 -0800 Subject: [PATCH 06/11] Union Property - use `data` from inner function when returning primitive values --- .../templates/property_templates/union_property.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi_python_client/templates/property_templates/union_property.pyi b/openapi_python_client/templates/property_templates/union_property.pyi index ee5aa1f3b..f2a0359fa 100644 --- a/openapi_python_client/templates/property_templates/union_property.pyi +++ b/openapi_python_client/templates/property_templates/union_property.pyi @@ -15,7 +15,7 @@ def _parse_{{ property.python_name }}(data: Any) -> {{ property.get_type_string( {{ construct(inner_property, "data", initial_value="UNSET") | indent(4) }} return {{ property.python_name }} {% else %} - return cast({{ inner_property.get_type_string() }}, {{ source }}) + return cast({{ inner_property.get_type_string() }}, data) {% endif %} {% endfor %} From 0521e366e5aefd757fa4becf978240a6077284a1 Mon Sep 17 00:00:00 2001 From: Packy Gallagher Date: Wed, 16 Dec 2020 11:09:13 -0800 Subject: [PATCH 07/11] Update golden record --- .../custom_e2e/models/model_with_any_json_properties.py | 8 ++++---- .../models/model_with_any_json_properties.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) 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 index aac19fe73..4aafeb91a 100644 --- 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 @@ -64,10 +64,10 @@ def _parse_additional_property( return additional_property except: # noqa: E722 pass - return cast(str, prop_dict) - return cast(float, prop_dict) - return cast(int, prop_dict) - return cast(bool, prop_dict) + return cast(str, data) + return cast(float, data) + return cast(int, data) + return cast(bool, data) additional_property = _parse_additional_property(prop_dict) 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 index aac19fe73..4aafeb91a 100644 --- 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 @@ -64,10 +64,10 @@ def _parse_additional_property( return additional_property except: # noqa: E722 pass - return cast(str, prop_dict) - return cast(float, prop_dict) - return cast(int, prop_dict) - return cast(bool, prop_dict) + return cast(str, data) + return cast(float, data) + return cast(int, data) + return cast(bool, data) additional_property = _parse_additional_property(prop_dict) From 9a679afcb4e21aded441e265239f38e31d75aca9 Mon Sep 17 00:00:00 2001 From: Packy Gallagher Date: Fri, 18 Dec 2020 17:43:14 -0800 Subject: [PATCH 08/11] Update union properties handling of props with and without template --- .../parser/properties/__init__.py | 9 +++++++ .../property_templates/union_property.pyi | 25 +++++++++++-------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index 4708bdc14..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 @@ -187,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/templates/property_templates/union_property.pyi b/openapi_python_client/templates/property_templates/union_property.pyi index f2a0359fa..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 cast({{ inner_property.get_type_string() }}, data) {% 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_instance_type_string() }}): - {% elif not loop.last %} + {% 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 %} From bfab9d4312a4717324eb9fc5f6214c62ec2b904b Mon Sep 17 00:00:00 2001 From: Packy Gallagher Date: Fri, 18 Dec 2020 17:43:24 -0800 Subject: [PATCH 09/11] Update golden record --- .../custom_e2e/api/tests/defaults_tests_defaults_post.py | 9 ++++----- .../custom_e2e/models/model_with_any_json_properties.py | 9 --------- .../api/tests/defaults_tests_defaults_post.py | 9 ++++----- .../models/model_with_any_json_properties.py | 9 --------- 4 files changed, 8 insertions(+), 28 deletions(-) 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/model_with_any_json_properties.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/model_with_any_json_properties.py index 4aafeb91a..2e488ea69 100644 --- 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 @@ -24,12 +24,6 @@ def to_dict(self) -> Dict[str, Any]: elif isinstance(prop, list): field_dict[prop_name] = prop - elif isinstance(prop, str): - field_dict[prop_name] = prop - elif isinstance(prop, float): - field_dict[prop_name] = prop - elif isinstance(prop, int): - field_dict[prop_name] = prop else: field_dict[prop_name] = prop @@ -65,9 +59,6 @@ def _parse_additional_property( except: # noqa: E722 pass return cast(str, data) - return cast(float, data) - return cast(int, data) - return cast(bool, data) additional_property = _parse_additional_property(prop_dict) 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/model_with_any_json_properties.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties.py index 4aafeb91a..2e488ea69 100644 --- 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 @@ -24,12 +24,6 @@ def to_dict(self) -> Dict[str, Any]: elif isinstance(prop, list): field_dict[prop_name] = prop - elif isinstance(prop, str): - field_dict[prop_name] = prop - elif isinstance(prop, float): - field_dict[prop_name] = prop - elif isinstance(prop, int): - field_dict[prop_name] = prop else: field_dict[prop_name] = prop @@ -65,9 +59,6 @@ def _parse_additional_property( except: # noqa: E722 pass return cast(str, data) - return cast(float, data) - return cast(int, data) - return cast(bool, data) additional_property = _parse_additional_property(prop_dict) From a3653842a0ec0efe89669f0dc2f7572d8522e017 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sun, 20 Dec 2020 11:00:17 -0700 Subject: [PATCH 10/11] Perf improvements to #268 (additionalProperties Union Fix): - Added CHANGELOG entry - Change inner_properties_without_template to bool generated once - Change inner_properties_with_template to generator --- CHANGELOG.md | 4 +++ .../models/model_with_any_json_properties.py | 2 +- .../models/model_with_any_json_properties.py | 2 +- openapi_python_client/__init__.py | 11 +++++-- .../parser/properties/__init__.py | 29 ++++++++++--------- .../property_templates/union_property.pyi | 24 +++++++-------- 6 files changed, 42 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be5608af5..46e8ab3ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 0.7.3 - Unreleased +### Fixes +- Spacing and extra returns for Union types of `additionalProperties` (#266 & #268). Thanks @joshzana & @packyg! + ## 0.7.2 - 2020-12-08 ### Fixes - A bug in handling optional properties that are themselves models (introduced in 0.7.1) (#262). Thanks @packyg! 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 index 2e488ea69..62481f913 100644 --- 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 @@ -58,7 +58,7 @@ def _parse_additional_property( return additional_property except: # noqa: E722 pass - return cast(str, data) + return cast(Union[ModelWithAnyJsonPropertiesAdditionalProperty, List[str], str, float, int, bool], data) additional_property = _parse_additional_property(prop_dict) 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 index 2e488ea69..62481f913 100644 --- 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 @@ -58,7 +58,7 @@ def _parse_additional_property( return additional_property except: # noqa: E722 pass - return cast(str, data) + return cast(Union[ModelWithAnyJsonPropertiesAdditionalProperty, List[str], str, float, int, bool], data) additional_property = _parse_additional_property(prop_dict) diff --git a/openapi_python_client/__init__.py b/openapi_python_client/__init__.py index b1adb07b6..152bce66e 100644 --- a/openapi_python_client/__init__.py +++ b/openapi_python_client/__init__.py @@ -25,8 +25,15 @@ __version__ = version(__package__) +TEMPLATE_FILTERS = { + "snakecase": utils.snake_case, + "kebabcase": utils.kebab_case, + "pascalcase": utils.pascal_case, + "any": any, +} + + class Project: - TEMPLATE_FILTERS = {"snakecase": utils.snake_case, "kebabcase": utils.kebab_case, "pascalcase": utils.pascal_case} project_name_override: Optional[str] = None package_name_override: Optional[str] = None package_version_override: Optional[str] = None @@ -57,7 +64,7 @@ def __init__(self, *, openapi: GeneratorData, custom_template_path: Optional[Pat ) self.version: str = self.package_version_override or openapi.version - self.env.filters.update(self.TEMPLATE_FILTERS) + self.env.filters.update(TEMPLATE_FILTERS) def build(self) -> Sequence[GeneratorError]: """ Create the project from templates """ diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index b5c47c0ea..358148f8a 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -1,18 +1,17 @@ -import builtins from itertools import chain -from typing import Any, ClassVar, Dict, Generic, Iterable, List, Optional, Set, Tuple, TypeVar, Union +from typing import Any, ClassVar, Dict, Generic, Iterable, List, Optional, Set, Tuple, TypeVar, Union, Iterator import attr -from ... import schema as oai -from ... import utils -from ..errors import PropertyError, ValidationError -from ..reference import Reference from .converter import convert, convert_chain from .enum_property import EnumProperty from .model_property import ModelProperty from .property import Property from .schemas import Schemas +from ..errors import PropertyError, ValidationError +from ..reference import Reference +from ... import schema as oai +from ... import utils @attr.s(auto_attribs=True, frozen=True) @@ -160,6 +159,13 @@ class UnionProperty(Property): inner_properties: List[Property] template: ClassVar[str] = "union_property.pyi" + has_properties_without_templates: bool = attr.ib(init=False) + + def __attrs_post_init__(self) -> None: + super().__attrs_post_init__() + object.__setattr__( + 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 """ @@ -185,16 +191,11 @@ def get_imports(self, *, prefix: str) -> Set[str]: imports = super().get_imports(prefix=prefix) for inner_prop in self.inner_properties: imports.update(inner_prop.get_imports(prefix=prefix)) - imports.add("from typing import Union") + imports.add("from typing import cast, 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 inner_properties_with_template(self) -> Iterator[Property]: + return (prop for prop in self.inner_properties if prop.template) def _string_based_property( diff --git a/openapi_python_client/templates/property_templates/union_property.pyi b/openapi_python_client/templates/property_templates/union_property.pyi index c3cfee05e..4c632c60a 100644 --- a/openapi_python_client/templates/property_templates/union_property.pyi +++ b/openapi_python_client/templates/property_templates/union_property.pyi @@ -2,8 +2,8 @@ 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_with_template %} - {% if not loop.last or property.inner_properties_without_template %} + {% for inner_property in property.inner_properties_with_template() %} + {% if not loop.last or property.has_properties_without_templates %} try: {% from "property_templates/" + inner_property.template import construct %} {{ construct(inner_property, "data", initial_value="UNSET") | indent(8) }} @@ -16,9 +16,9 @@ def _parse_{{ property.python_name }}(data: Any) -> {{ property.get_type_string( return {{ property.python_name }} {% endif %} {% endfor %} - {% if property.inner_properties_without_template %} + {% if property.has_properties_without_templates %} {# 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) + return cast({{ property.get_type_string() }}, data) {% endif %} {{ property.python_name }} = _parse_{{ property.python_name }}({{ source }}) @@ -32,28 +32,28 @@ if isinstance({{ source }}, Unset): {{ destination }} = UNSET {% endif %} {% if property.nullable %} -{% if property.required %} + {% if property.required %} if {{ source }} is None: -{% else %}{# There's an if UNSET statement before this #} + {% else %}{# There's an if UNSET statement before this #} elif {{ source }} is None: -{% endif %} + {% endif %} {{ destination }}{% if declare_type %}: {{ property.get_type_string() }}{% endif %} = None {% endif %} -{% for inner_property in property.inner_properties_with_template %} +{% 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_instance_type_string() }}): - {% elif not loop.last or property.inner_properties_without_template %} + {% elif not loop.last or property.has_properties_without_templates %} elif isinstance({{ source }}, {{ inner_property.get_instance_type_string() }}): {% else %} else: {% endif %} -{% from "property_templates/" + inner_property.template import transform %} + {% from "property_templates/" + inner_property.template import transform %} {{ transform(inner_property, source, destination, declare_type=False) | indent(4) }} {% endfor %} -{% if property.inner_properties_without_template and (property.inner_properties_with_template or not property.required)%} +{% if property.has_properties_without_templates and (property.inner_properties_with_template() | any or not property.required)%} else: {{ destination }} = {{ source }} -{% elif property.inner_properties_without_template %} +{% elif property.has_properties_without_templates %} {{ destination }} = {{ source }} {% endif %} From 8a1bef36ae4995f7fb56fb2fcc90fb61c895b630 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sun, 20 Dec 2020 11:11:15 -0700 Subject: [PATCH 11/11] Run isort, fix unit test --- openapi_python_client/parser/properties/__init__.py | 10 +++++----- tests/test_parser/test_properties/test_init.py | 6 ++++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index 358148f8a..427276692 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -1,17 +1,17 @@ from itertools import chain -from typing import Any, ClassVar, Dict, Generic, Iterable, List, Optional, Set, Tuple, TypeVar, Union, Iterator +from typing import Any, ClassVar, Dict, Generic, Iterable, Iterator, List, Optional, Set, Tuple, TypeVar, Union import attr +from ... import schema as oai +from ... import utils +from ..errors import PropertyError, ValidationError +from ..reference import Reference from .converter import convert, convert_chain from .enum_property import EnumProperty from .model_property import ModelProperty from .property import Property from .schemas import Schemas -from ..errors import PropertyError, ValidationError -from ..reference import Reference -from ... import schema as oai -from ... import utils @attr.s(auto_attribs=True, frozen=True) diff --git a/tests/test_parser/test_properties/test_init.py b/tests/test_parser/test_properties/test_init.py index b2dd1bb2f..a7ea05881 100644 --- a/tests/test_parser/test_properties/test_init.py +++ b/tests/test_parser/test_properties/test_init.py @@ -292,7 +292,7 @@ def test_get_type_string(self, mocker): assert p.get_type_string() == base_type_string_with_unset assert p.get_type_string(no_optional=True) == base_type_string - def test_get_type_imports(self, mocker): + def test_get_imports(self, mocker): from openapi_python_client.parser.properties import UnionProperty inner_property_1 = mocker.MagicMock() @@ -313,7 +313,7 @@ def test_get_type_imports(self, mocker): assert p.get_imports(prefix=prefix) == { inner_import_1, inner_import_2, - "from typing import Union", + "from typing import cast, Union", } p = UnionProperty( @@ -327,6 +327,7 @@ def test_get_type_imports(self, mocker): inner_import_1, inner_import_2, "from typing import Union", + "from typing import cast, Union", "from ...types import UNSET, Unset", } @@ -341,6 +342,7 @@ def test_get_type_imports(self, mocker): inner_import_1, inner_import_2, "from typing import Union", + "from typing import cast, Union", "from typing import Optional", "from ...types import UNSET, Unset", }