From a7b6d477ba5a2763dc61d5e68cc1c74c0c0a2131 Mon Sep 17 00:00:00 2001 From: Packy Gallagher Date: Tue, 8 Dec 2020 15:47:17 -0800 Subject: [PATCH 1/8] Add allOf support for model definitions (#98) Collapses the child elements into one, without class heirarchy, mixins, etc. --- CHANGELOG.md | 1 + .../custom_e2e/models/__init__.py | 3 + .../custom_e2e/models/all_of_sub_model.py | 54 +++++++ .../models/another_all_of_sub_model.py | 54 +++++++ .../custom_e2e/models/model_from_all_of.py | 61 ++++++++ .../my_test_api_client/models/__init__.py | 3 + .../models/all_of_sub_model.py | 54 +++++++ .../models/another_all_of_sub_model.py | 54 +++++++ .../models/model_from_all_of.py | 61 ++++++++ end_to_end_tests/openapi.json | 26 ++++ .../parser/properties/__init__.py | 32 +++- .../parser/properties/model_property.py | 51 ++++++- .../parser/properties/schemas.py | 6 +- .../test_parser/test_properties/test_init.py | 23 ++- .../test_properties/test_model_property.py | 142 ++++++++++++++++++ 15 files changed, 610 insertions(+), 15 deletions(-) create mode 100644 end_to_end_tests/golden-record-custom/custom_e2e/models/all_of_sub_model.py create mode 100644 end_to_end_tests/golden-record-custom/custom_e2e/models/another_all_of_sub_model.py create mode 100644 end_to_end_tests/golden-record-custom/custom_e2e/models/model_from_all_of.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/all_of_sub_model.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/another_all_of_sub_model.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/model_from_all_of.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b2372aca..585ccdadb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `none` will not create a project folder at all, only the inner package folder (which won't be inner anymore) - Attempt to detect and alert users if they are using an unsupported version of OpenAPI (#281). - Fixes `Enum` deserialization when the value is `UNSET`. +- Basic support for `allOf` in models (#98) ### Changes 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..382ddc401 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,12 +1,15 @@ """ Contains all the data models used in inputs/outputs """ from .a_model import AModel +from .all_of_sub_model import AllOfSubModel from .an_enum import AnEnum from .an_int_enum import AnIntEnum +from .another_all_of_sub_model import AnotherAllOfSubModel from .body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost from .different_enum import DifferentEnum from .free_form_model import FreeFormModel from .http_validation_error import HTTPValidationError +from .model_from_all_of import ModelFromAllOf from .model_with_additional_properties_inlined import ModelWithAdditionalPropertiesInlined from .model_with_additional_properties_inlined_additional_property import ( ModelWithAdditionalPropertiesInlinedAdditionalProperty, diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/models/all_of_sub_model.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/all_of_sub_model.py new file mode 100644 index 000000000..baa59d06a --- /dev/null +++ b/end_to_end_tests/golden-record-custom/custom_e2e/models/all_of_sub_model.py @@ -0,0 +1,54 @@ +from typing import Any, Dict, List, Type, TypeVar, Union + +import attr + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="AllOfSubModel") + + +@attr.s(auto_attribs=True) +class AllOfSubModel: + """ """ + + a_sub_property: Union[Unset, str] = UNSET + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + a_sub_property = self.a_sub_property + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if a_sub_property is not UNSET: + field_dict["a_sub_property"] = a_sub_property + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + d = src_dict.copy() + a_sub_property = d.pop("a_sub_property", UNSET) + + all_of_sub_model = cls( + a_sub_property=a_sub_property, + ) + + all_of_sub_model.additional_properties = d + return all_of_sub_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/another_all_of_sub_model.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/another_all_of_sub_model.py new file mode 100644 index 000000000..3949852c6 --- /dev/null +++ b/end_to_end_tests/golden-record-custom/custom_e2e/models/another_all_of_sub_model.py @@ -0,0 +1,54 @@ +from typing import Any, Dict, List, Type, TypeVar, Union + +import attr + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="AnotherAllOfSubModel") + + +@attr.s(auto_attribs=True) +class AnotherAllOfSubModel: + """ """ + + another_sub_property: Union[Unset, str] = UNSET + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + another_sub_property = self.another_sub_property + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if another_sub_property is not UNSET: + field_dict["another_sub_property"] = another_sub_property + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + d = src_dict.copy() + another_sub_property = d.pop("another_sub_property", UNSET) + + another_all_of_sub_model = cls( + another_sub_property=another_sub_property, + ) + + another_all_of_sub_model.additional_properties = d + return another_all_of_sub_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_from_all_of.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/model_from_all_of.py new file mode 100644 index 000000000..9e9e87737 --- /dev/null +++ b/end_to_end_tests/golden-record-custom/custom_e2e/models/model_from_all_of.py @@ -0,0 +1,61 @@ +from typing import Any, Dict, List, Type, TypeVar, Union + +import attr + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="ModelFromAllOf") + + +@attr.s(auto_attribs=True) +class ModelFromAllOf: + """ """ + + another_sub_property: Union[Unset, str] = UNSET + a_sub_property: Union[Unset, str] = UNSET + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + another_sub_property = self.another_sub_property + a_sub_property = self.a_sub_property + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if another_sub_property is not UNSET: + field_dict["another_sub_property"] = another_sub_property + if a_sub_property is not UNSET: + field_dict["a_sub_property"] = a_sub_property + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + d = src_dict.copy() + another_sub_property = d.pop("another_sub_property", UNSET) + + a_sub_property = d.pop("a_sub_property", UNSET) + + model_from_all_of = cls( + another_sub_property=another_sub_property, + a_sub_property=a_sub_property, + ) + + model_from_all_of.additional_properties = d + return model_from_all_of + + @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/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py index d3ca924b3..382ddc401 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,12 +1,15 @@ """ Contains all the data models used in inputs/outputs """ from .a_model import AModel +from .all_of_sub_model import AllOfSubModel from .an_enum import AnEnum from .an_int_enum import AnIntEnum +from .another_all_of_sub_model import AnotherAllOfSubModel from .body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost from .different_enum import DifferentEnum from .free_form_model import FreeFormModel from .http_validation_error import HTTPValidationError +from .model_from_all_of import ModelFromAllOf from .model_with_additional_properties_inlined import ModelWithAdditionalPropertiesInlined from .model_with_additional_properties_inlined_additional_property import ( ModelWithAdditionalPropertiesInlinedAdditionalProperty, diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/all_of_sub_model.py b/end_to_end_tests/golden-record/my_test_api_client/models/all_of_sub_model.py new file mode 100644 index 000000000..baa59d06a --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/all_of_sub_model.py @@ -0,0 +1,54 @@ +from typing import Any, Dict, List, Type, TypeVar, Union + +import attr + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="AllOfSubModel") + + +@attr.s(auto_attribs=True) +class AllOfSubModel: + """ """ + + a_sub_property: Union[Unset, str] = UNSET + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + a_sub_property = self.a_sub_property + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if a_sub_property is not UNSET: + field_dict["a_sub_property"] = a_sub_property + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + d = src_dict.copy() + a_sub_property = d.pop("a_sub_property", UNSET) + + all_of_sub_model = cls( + a_sub_property=a_sub_property, + ) + + all_of_sub_model.additional_properties = d + return all_of_sub_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/another_all_of_sub_model.py b/end_to_end_tests/golden-record/my_test_api_client/models/another_all_of_sub_model.py new file mode 100644 index 000000000..3949852c6 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/another_all_of_sub_model.py @@ -0,0 +1,54 @@ +from typing import Any, Dict, List, Type, TypeVar, Union + +import attr + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="AnotherAllOfSubModel") + + +@attr.s(auto_attribs=True) +class AnotherAllOfSubModel: + """ """ + + another_sub_property: Union[Unset, str] = UNSET + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + another_sub_property = self.another_sub_property + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if another_sub_property is not UNSET: + field_dict["another_sub_property"] = another_sub_property + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + d = src_dict.copy() + another_sub_property = d.pop("another_sub_property", UNSET) + + another_all_of_sub_model = cls( + another_sub_property=another_sub_property, + ) + + another_all_of_sub_model.additional_properties = d + return another_all_of_sub_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_from_all_of.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_from_all_of.py new file mode 100644 index 000000000..9e9e87737 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_from_all_of.py @@ -0,0 +1,61 @@ +from typing import Any, Dict, List, Type, TypeVar, Union + +import attr + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="ModelFromAllOf") + + +@attr.s(auto_attribs=True) +class ModelFromAllOf: + """ """ + + another_sub_property: Union[Unset, str] = UNSET + a_sub_property: Union[Unset, str] = UNSET + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + another_sub_property = self.another_sub_property + a_sub_property = self.a_sub_property + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if another_sub_property is not UNSET: + field_dict["another_sub_property"] = another_sub_property + if a_sub_property is not UNSET: + field_dict["a_sub_property"] = a_sub_property + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + d = src_dict.copy() + another_sub_property = d.pop("another_sub_property", UNSET) + + a_sub_property = d.pop("a_sub_property", UNSET) + + model_from_all_of = cls( + another_sub_property=another_sub_property, + a_sub_property=a_sub_property, + ) + + model_from_all_of.additional_properties = d + return model_from_all_of + + @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/openapi.json b/end_to_end_tests/openapi.json index 9e3d78908..eb127b69e 100644 --- a/end_to_end_tests/openapi.json +++ b/end_to_end_tests/openapi.json @@ -844,6 +844,32 @@ } ] } + }, + "ModelFromAllOf": { + "title": "ModelFromAllOf", + "type": "object", + "allOf": [ + {"$ref": "#/components/schemas/AllOfSubModel"}, + {"$ref": "#/components/schemas/AnotherAllOfSubModel"} + ] + }, + "AllOfSubModel": { + "title": "AllOfSubModel", + "type": "object", + "properties": { + "a_sub_property": { + "type": "string" + } + } + }, + "AnotherAllOfSubModel": { + "title": "AnotherAllOfSubModel", + "type": "object", + "properties": { + "another_sub_property": { + "type": "string" + } + } } } } diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index 427276692..f6289ce7c 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -250,13 +250,23 @@ def build_model_property( required_properties: List[Property] = [] optional_properties: List[Property] = [] relative_imports: Set[str] = set() + references: List[oai.Reference] = [] class_name = data.title or name if parent_name: class_name = f"{utils.pascal_case(parent_name)}{utils.pascal_case(class_name)}" ref = Reference.from_ref(class_name) - for key, value in (data.properties or {}).items(): + all_props = data.properties or {} + if not isinstance(data, oai.Reference) and data.allOf: + for sub_prop in data.allOf: + if isinstance(sub_prop, oai.Reference): + references += [sub_prop] + else: + all_props.update(sub_prop.properties or {}) + required_set.update(sub_prop.required or []) + + for key, value in all_props.items(): prop_required = key in required_set prop, schemas = property_from_data( name=key, required=prop_required, data=value, schemas=schemas, parent_name=class_name @@ -292,6 +302,7 @@ def build_model_property( prop = ModelProperty( reference=ref, + references=references, required_properties=required_properties, optional_properties=optional_properties, relative_imports=relative_imports, @@ -449,9 +460,6 @@ def _property_from_data( ) if data.anyOf or data.oneOf: return build_union_property(data=data, name=name, required=required, schemas=schemas, parent_name=parent_name) - if not data.type: - return NoneProperty(name=name, required=required, nullable=False, default=None), schemas - if data.type == "string": return _string_based_property(name=name, required=required, data=data), schemas elif data.type == "number": @@ -486,8 +494,10 @@ def _property_from_data( ) elif data.type == "array": return build_list_property(data=data, name=name, required=required, schemas=schemas, parent_name=parent_name) - elif data.type == "object": + elif data.type == "object" or data.allOf: return build_model_property(data=data, name=name, schemas=schemas, required=required, parent_name=parent_name) + elif not data.type: + return NoneProperty(name=name, required=required, nullable=False, default=None), schemas return PropertyError(data=data, detail=f"unknown type {data.type}"), schemas @@ -544,6 +554,16 @@ def build_schemas(*, components: Dict[str, Union[oai.Reference, oai.Schema]]) -> schemas = schemas_or_err processing = True # We made some progress this round, do another after it's done to_process = next_round - schemas.errors.extend(errors) + resolve_errors: List[PropertyError] = [] + models = list(schemas.models.values()) + for model in models: + schemas_or_err = model.resolve_references(components=components, schemas=schemas) + if isinstance(schemas_or_err, PropertyError): + resolve_errors.append(schemas_or_err) + else: + schemas = schemas_or_err + + schemas.errors.extend(errors) + schemas.errors.extend(resolve_errors) return schemas diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index 084017a41..d197c3ee7 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -1,9 +1,13 @@ -from typing import ClassVar, List, Set, Union +from collections.abc import Iterable +from typing import ClassVar, Dict, List, Set, Union import attr +from ... import schema as oai +from ..errors import PropertyError from ..reference import Reference from .property import Property +from .schemas import Schemas @attr.s(auto_attribs=True, frozen=True) @@ -11,7 +15,7 @@ class ModelProperty(Property): """ A property which refers to another Schema """ reference: Reference - + references: List[oai.Reference] required_properties: List[Property] optional_properties: List[Property] description: str @@ -20,6 +24,49 @@ class ModelProperty(Property): template: ClassVar[str] = "model_property.pyi" + def resolve_references( + self, components: Dict[str, Union[oai.Reference, oai.Schema]], schemas: Schemas + ) -> Union[Schemas, PropertyError]: + from ..properties import property_from_data + + required_set = set() + props = {} + while self.references: + reference = self.references.pop() + source_name = Reference.from_ref(reference.ref).class_name + referenced_prop = components[source_name] + assert isinstance(referenced_prop, oai.Schema) + for p, val in (referenced_prop.properties or {}).items(): + props[p] = (val, source_name) + for sub_prop in referenced_prop.allOf or []: + if isinstance(sub_prop, oai.Reference): + self.references.append(sub_prop) + else: + for p, val in (sub_prop.properties or {}).items(): + props[p] = (val, source_name) + if isinstance(referenced_prop.required, Iterable): + for sub_prop_name in referenced_prop.required: + required_set.add(sub_prop_name) + + for key, (value, source_name) in (props or {}).items(): + required = key in required_set + prop, schemas = property_from_data( + name=key, required=required, data=value, schemas=schemas, parent_name=source_name + ) + if isinstance(prop, PropertyError): + return prop + if required: + self.required_properties.append(prop) + # Remove the optional version + new_optional_props = [op for op in self.optional_properties if op.name != prop.name] + self.optional_properties.clear() + self.optional_properties.extend(new_optional_props) + elif not any(ep for ep in (self.optional_properties + self.required_properties) if ep.name == prop.name): + self.optional_properties.append(prop) + self.relative_imports.update(prop.get_imports(prefix="..")) + + return schemas + 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 diff --git a/openapi_python_client/parser/properties/schemas.py b/openapi_python_client/parser/properties/schemas.py index 338938673..a299a8181 100644 --- a/openapi_python_client/parser/properties/schemas.py +++ b/openapi_python_client/parser/properties/schemas.py @@ -5,8 +5,10 @@ import attr from ..errors import ParseError + +# Avoid circular import with forward reference +from . import model_property from .enum_property import EnumProperty -from .model_property import ModelProperty @attr.s(auto_attribs=True, frozen=True) @@ -14,5 +16,5 @@ class Schemas: """ Structure for containing all defined, shareable, and resuabled schemas (attr classes and Enums) """ enums: Dict[str, EnumProperty] = attr.ib(factory=dict) - models: Dict[str, ModelProperty] = attr.ib(factory=dict) + models: Dict[str, "model_property.ModelProperty"] = attr.ib(factory=dict) errors: List[ParseError] = attr.ib(factory=list) diff --git a/tests/test_parser/test_properties/test_init.py b/tests/test_parser/test_properties/test_init.py index a7ea05881..2f3c13676 100644 --- a/tests/test_parser/test_properties/test_init.py +++ b/tests/test_parser/test_properties/test_init.py @@ -586,6 +586,7 @@ def test_property_from_data_ref_model(self): nullable=False, default=None, reference=Reference(class_name=class_name, module_name="my_model"), + references=[], required_properties=[], optional_properties=[], description="", @@ -602,6 +603,7 @@ def test_property_from_data_ref_model(self): nullable=False, default=None, reference=Reference(class_name=class_name, module_name="my_model"), + references=[], required_properties=[], optional_properties=[], description="", @@ -988,19 +990,25 @@ def test__string_based_property_unsupported_format(self, mocker): def test_build_schemas(mocker): build_model_property = mocker.patch(f"{MODULE_NAME}.build_model_property") in_data = {"1": mocker.MagicMock(enum=None), "2": mocker.MagicMock(enum=None), "3": mocker.MagicMock(enum=None)} + model_1 = mocker.MagicMock() schemas_1 = mocker.MagicMock() model_2 = mocker.MagicMock() schemas_2 = mocker.MagicMock(errors=[]) - error = PropertyError() + schemas_2.models = {"1": model_1, "2": model_2} + error_1 = PropertyError() schemas_3 = mocker.MagicMock() + schemas_4 = mocker.MagicMock(errors=[]) + model_1.resolve_references.return_value = schemas_4 + error_2 = PropertyError() + model_2.resolve_references.return_value = error_2 # This loops through one for each, then again to retry the error build_model_property.side_effect = [ (model_1, schemas_1), (model_2, schemas_2), - (error, schemas_3), - (error, schemas_3), + (error_1, schemas_3), + (error_1, schemas_3), ] from openapi_python_client.parser.properties import Schemas, build_schemas @@ -1016,8 +1024,12 @@ def test_build_schemas(mocker): ] ) # schemas_3 was the last to come back from build_model_property, but it should be ignored because it's an error - assert result == schemas_2 - assert result.errors == [error] + model_1.resolve_references.assert_called_once_with(components=in_data, schemas=schemas_2) + # schemas_4 came from resolving model_1 + model_2.resolve_references.assert_called_once_with(components=in_data, schemas=schemas_4) + # resolving model_2 resulted in err, so no schemas_5 + assert result == schemas_4 + assert result.errors == [error_1, error_2] def test_build_parse_error_on_reference(): @@ -1091,6 +1103,7 @@ def test_build_model_property(additional_properties_schema, expected_additional_ nullable=False, default=None, reference=Reference(class_name="ParentMyModel", module_name="parent_my_model"), + references=[], required_properties=[StringProperty(name="req", required=True, nullable=False, default=None)], optional_properties=[DateTimeProperty(name="opt", required=False, nullable=False, default=None)], description=data.description, diff --git a/tests/test_parser/test_properties/test_model_property.py b/tests/test_parser/test_properties/test_model_property.py index 1024ef179..421f40d48 100644 --- a/tests/test_parser/test_properties/test_model_property.py +++ b/tests/test_parser/test_properties/test_model_property.py @@ -23,6 +23,7 @@ def test_get_type_string(no_optional, nullable, required, expected): nullable=nullable, default=None, reference=Reference(class_name="MyClass", module_name="my_module"), + references=[], description="", optional_properties=[], required_properties=[], @@ -42,6 +43,7 @@ def test_get_imports(): nullable=True, default=None, reference=Reference(class_name="MyClass", module_name="my_module"), + references=[], description="", optional_properties=[], required_properties=[], @@ -57,3 +59,143 @@ def test_get_imports(): "from typing import Dict", "from typing import cast", } + + +def test_resolve_references(mocker): + import openapi_python_client.schema as oai + from openapi_python_client.parser.properties import build_model_property + + schemas = { + "RefA": oai.Schema.construct( + title=mocker.MagicMock(), + description=mocker.MagicMock(), + required=["String"], + properties={ + "String": oai.Schema.construct(type="string"), + "Enum": oai.Schema.construct(type="string", enum=["aValue"]), + "DateTime": oai.Schema.construct(type="string", format="date-time"), + }, + ), + "RefB": oai.Schema.construct( + title=mocker.MagicMock(), + description=mocker.MagicMock(), + required=["DateTime"], + properties={ + "Int": oai.Schema.construct(type="integer"), + "DateTime": oai.Schema.construct(type="string", format="date-time"), + "Float": oai.Schema.construct(type="number", format="float"), + }, + ), + # Intentionally no properties defined + "RefC": oai.Schema.construct( + title=mocker.MagicMock(), + description=mocker.MagicMock(), + ), + } + + model_schema = oai.Schema.construct( + allOf=[ + oai.Reference.construct(ref="#/components/schemas/RefA"), + oai.Reference.construct(ref="#/components/schemas/RefB"), + oai.Reference.construct(ref="#/components/schemas/RefC"), + oai.Schema.construct( + title=mocker.MagicMock(), + description=mocker.MagicMock(), + required=["Float"], + properties={ + "String": oai.Schema.construct(type="string"), + "Float": oai.Schema.construct(type="number", format="float"), + }, + ), + ] + ) + + components = {**schemas, "Model": model_schema} + + from openapi_python_client.parser.properties import Schemas + + schemas_holder = Schemas() + model, schemas_holder = build_model_property( + data=model_schema, name="Model", required=True, schemas=schemas_holder, parent_name=None + ) + model.resolve_references(components, schemas_holder) + assert sorted(p.name for p in model.required_properties) == ["DateTime", "Float", "String"] + assert all(p.required for p in model.required_properties) + assert sorted(p.name for p in model.optional_properties) == ["Enum", "Int"] + assert all(not p.required for p in model.optional_properties) + + +def test_resolve_references_nested_allof(mocker): + import openapi_python_client.schema as oai + from openapi_python_client.parser.properties import build_model_property + + schemas = { + "RefA": oai.Schema.construct( + title=mocker.MagicMock(), + description=mocker.MagicMock(), + required=["String"], + properties={ + "String": oai.Schema.construct(type="string"), + "Enum": oai.Schema.construct(type="string", enum=["aValue"]), + "DateTime": oai.Schema.construct(type="string", format="date-time"), + }, + ), + "RefB": oai.Schema.construct( + title=mocker.MagicMock(), + description=mocker.MagicMock(), + required=["DateTime"], + properties={ + "Int": oai.Schema.construct(type="integer"), + "DateTime": oai.Schema.construct(type="string", format="date-time"), + "Float": oai.Schema.construct(type="number", format="float"), + }, + ), + # Intentionally no properties defined + "RefC": oai.Schema.construct( + title=mocker.MagicMock(), + description=mocker.MagicMock(), + ), + } + + model_schema = oai.Schema.construct( + type="object", + properties={ + "Key": oai.Schema.construct( + allOf=[ + oai.Reference.construct(ref="#/components/schemas/RefA"), + oai.Reference.construct(ref="#/components/schemas/RefB"), + oai.Reference.construct(ref="#/components/schemas/RefC"), + oai.Schema.construct( + title=mocker.MagicMock(), + description=mocker.MagicMock(), + required=["Float"], + properties={ + "String": oai.Schema.construct(type="string"), + "Float": oai.Schema.construct(type="number", format="float"), + }, + ), + ] + ), + }, + ) + + components = {**schemas, "Model": model_schema} + + from openapi_python_client.parser.properties import ModelProperty, Schemas + + schemas_holder = Schemas() + model, schemas_holder = build_model_property( + data=model_schema, name="Model", required=True, schemas=schemas_holder, parent_name=None + ) + model.resolve_references(components, schemas_holder) + assert sorted(p.name for p in model.required_properties) == [] + assert sorted(p.name for p in model.optional_properties) == ["Key"] + assert all(not p.required for p in model.optional_properties) + + key_property = model.optional_properties[0] + assert isinstance(key_property, ModelProperty) + key_property.resolve_references(components, schemas_holder) + assert sorted(p.name for p in key_property.required_properties) == ["DateTime", "Float", "String"] + assert all(p.required for p in key_property.required_properties) + assert sorted(p.name for p in key_property.optional_properties) == ["Enum", "Int"] + assert all(not p.required for p in key_property.optional_properties) From 74da896b7f208a41db744488110121bbeb459115 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sun, 31 Jan 2021 18:07:20 -0700 Subject: [PATCH 2/8] refactor: allOf support to happen in build_model_property --- .../custom_e2e/models/model_from_all_of.py | 14 +- .../models/model_from_all_of.py | 14 +- .../parser/properties/__init__.py | 31 ++-- .../parser/properties/model_property.py | 50 +----- .../parser/properties/schemas.py | 6 +- .../test_parser/test_properties/test_init.py | 23 +-- .../test_properties/test_model_property.py | 142 ------------------ 7 files changed, 34 insertions(+), 246 deletions(-) diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/models/model_from_all_of.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/model_from_all_of.py index 9e9e87737..ce26a3bbb 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/models/model_from_all_of.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/models/model_from_all_of.py @@ -11,34 +11,34 @@ class ModelFromAllOf: """ """ - another_sub_property: Union[Unset, str] = UNSET a_sub_property: Union[Unset, str] = UNSET + another_sub_property: Union[Unset, str] = UNSET additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) def to_dict(self) -> Dict[str, Any]: - another_sub_property = self.another_sub_property a_sub_property = self.a_sub_property + another_sub_property = self.another_sub_property field_dict: Dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update({}) - if another_sub_property is not UNSET: - field_dict["another_sub_property"] = another_sub_property if a_sub_property is not UNSET: field_dict["a_sub_property"] = a_sub_property + if another_sub_property is not UNSET: + field_dict["another_sub_property"] = another_sub_property return field_dict @classmethod def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: d = src_dict.copy() - another_sub_property = d.pop("another_sub_property", UNSET) - a_sub_property = d.pop("a_sub_property", UNSET) + another_sub_property = d.pop("another_sub_property", UNSET) + model_from_all_of = cls( - another_sub_property=another_sub_property, a_sub_property=a_sub_property, + another_sub_property=another_sub_property, ) model_from_all_of.additional_properties = d diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_from_all_of.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_from_all_of.py index 9e9e87737..ce26a3bbb 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_from_all_of.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_from_all_of.py @@ -11,34 +11,34 @@ class ModelFromAllOf: """ """ - another_sub_property: Union[Unset, str] = UNSET a_sub_property: Union[Unset, str] = UNSET + another_sub_property: Union[Unset, str] = UNSET additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) def to_dict(self) -> Dict[str, Any]: - another_sub_property = self.another_sub_property a_sub_property = self.a_sub_property + another_sub_property = self.another_sub_property field_dict: Dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update({}) - if another_sub_property is not UNSET: - field_dict["another_sub_property"] = another_sub_property if a_sub_property is not UNSET: field_dict["a_sub_property"] = a_sub_property + if another_sub_property is not UNSET: + field_dict["another_sub_property"] = another_sub_property return field_dict @classmethod def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: d = src_dict.copy() - another_sub_property = d.pop("another_sub_property", UNSET) - a_sub_property = d.pop("a_sub_property", UNSET) + another_sub_property = d.pop("another_sub_property", UNSET) + model_from_all_of = cls( - another_sub_property=another_sub_property, a_sub_property=a_sub_property, + another_sub_property=another_sub_property, ) model_from_all_of.additional_properties = d diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index f6289ce7c..0a24b59db 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -250,7 +250,6 @@ def build_model_property( required_properties: List[Property] = [] optional_properties: List[Property] = [] relative_imports: Set[str] = set() - references: List[oai.Reference] = [] class_name = data.title or name if parent_name: @@ -258,13 +257,18 @@ def build_model_property( ref = Reference.from_ref(class_name) all_props = data.properties or {} - if not isinstance(data, oai.Reference) and data.allOf: - for sub_prop in data.allOf: - if isinstance(sub_prop, oai.Reference): - references += [sub_prop] - else: - all_props.update(sub_prop.properties or {}) - required_set.update(sub_prop.required or []) + for sub_prop in data.allOf or []: + if isinstance(sub_prop, oai.Reference): + source_name = Reference.from_ref(sub_prop.ref).class_name + sub_model = schemas.models.get(source_name) + if sub_model is None: + return PropertyError(f"Reference {sub_prop.ref} not found"), schemas + required_properties.extend(sub_model.required_properties) + optional_properties.extend(sub_model.optional_properties) + relative_imports.update(sub_model.relative_imports) + else: + all_props.update(sub_prop.properties or {}) + required_set.update(sub_prop.required or []) for key, value in all_props.items(): prop_required = key in required_set @@ -302,7 +306,6 @@ def build_model_property( prop = ModelProperty( reference=ref, - references=references, required_properties=required_properties, optional_properties=optional_properties, relative_imports=relative_imports, @@ -555,15 +558,5 @@ def build_schemas(*, components: Dict[str, Union[oai.Reference, oai.Schema]]) -> processing = True # We made some progress this round, do another after it's done to_process = next_round - resolve_errors: List[PropertyError] = [] - models = list(schemas.models.values()) - for model in models: - schemas_or_err = model.resolve_references(components=components, schemas=schemas) - if isinstance(schemas_or_err, PropertyError): - resolve_errors.append(schemas_or_err) - else: - schemas = schemas_or_err - schemas.errors.extend(errors) - schemas.errors.extend(resolve_errors) return schemas diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index d197c3ee7..3b8ca8c06 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -1,13 +1,9 @@ -from collections.abc import Iterable -from typing import ClassVar, Dict, List, Set, Union +from typing import ClassVar, List, Set, Union import attr -from ... import schema as oai -from ..errors import PropertyError from ..reference import Reference from .property import Property -from .schemas import Schemas @attr.s(auto_attribs=True, frozen=True) @@ -15,7 +11,6 @@ class ModelProperty(Property): """ A property which refers to another Schema """ reference: Reference - references: List[oai.Reference] required_properties: List[Property] optional_properties: List[Property] description: str @@ -24,49 +19,6 @@ class ModelProperty(Property): template: ClassVar[str] = "model_property.pyi" - def resolve_references( - self, components: Dict[str, Union[oai.Reference, oai.Schema]], schemas: Schemas - ) -> Union[Schemas, PropertyError]: - from ..properties import property_from_data - - required_set = set() - props = {} - while self.references: - reference = self.references.pop() - source_name = Reference.from_ref(reference.ref).class_name - referenced_prop = components[source_name] - assert isinstance(referenced_prop, oai.Schema) - for p, val in (referenced_prop.properties or {}).items(): - props[p] = (val, source_name) - for sub_prop in referenced_prop.allOf or []: - if isinstance(sub_prop, oai.Reference): - self.references.append(sub_prop) - else: - for p, val in (sub_prop.properties or {}).items(): - props[p] = (val, source_name) - if isinstance(referenced_prop.required, Iterable): - for sub_prop_name in referenced_prop.required: - required_set.add(sub_prop_name) - - for key, (value, source_name) in (props or {}).items(): - required = key in required_set - prop, schemas = property_from_data( - name=key, required=required, data=value, schemas=schemas, parent_name=source_name - ) - if isinstance(prop, PropertyError): - return prop - if required: - self.required_properties.append(prop) - # Remove the optional version - new_optional_props = [op for op in self.optional_properties if op.name != prop.name] - self.optional_properties.clear() - self.optional_properties.extend(new_optional_props) - elif not any(ep for ep in (self.optional_properties + self.required_properties) if ep.name == prop.name): - self.optional_properties.append(prop) - self.relative_imports.update(prop.get_imports(prefix="..")) - - return schemas - 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 diff --git a/openapi_python_client/parser/properties/schemas.py b/openapi_python_client/parser/properties/schemas.py index a299a8181..338938673 100644 --- a/openapi_python_client/parser/properties/schemas.py +++ b/openapi_python_client/parser/properties/schemas.py @@ -5,10 +5,8 @@ import attr from ..errors import ParseError - -# Avoid circular import with forward reference -from . import model_property from .enum_property import EnumProperty +from .model_property import ModelProperty @attr.s(auto_attribs=True, frozen=True) @@ -16,5 +14,5 @@ class Schemas: """ Structure for containing all defined, shareable, and resuabled schemas (attr classes and Enums) """ enums: Dict[str, EnumProperty] = attr.ib(factory=dict) - models: Dict[str, "model_property.ModelProperty"] = attr.ib(factory=dict) + models: Dict[str, ModelProperty] = attr.ib(factory=dict) errors: List[ParseError] = attr.ib(factory=list) diff --git a/tests/test_parser/test_properties/test_init.py b/tests/test_parser/test_properties/test_init.py index 2f3c13676..a7ea05881 100644 --- a/tests/test_parser/test_properties/test_init.py +++ b/tests/test_parser/test_properties/test_init.py @@ -586,7 +586,6 @@ def test_property_from_data_ref_model(self): nullable=False, default=None, reference=Reference(class_name=class_name, module_name="my_model"), - references=[], required_properties=[], optional_properties=[], description="", @@ -603,7 +602,6 @@ def test_property_from_data_ref_model(self): nullable=False, default=None, reference=Reference(class_name=class_name, module_name="my_model"), - references=[], required_properties=[], optional_properties=[], description="", @@ -990,25 +988,19 @@ def test__string_based_property_unsupported_format(self, mocker): def test_build_schemas(mocker): build_model_property = mocker.patch(f"{MODULE_NAME}.build_model_property") in_data = {"1": mocker.MagicMock(enum=None), "2": mocker.MagicMock(enum=None), "3": mocker.MagicMock(enum=None)} - model_1 = mocker.MagicMock() schemas_1 = mocker.MagicMock() model_2 = mocker.MagicMock() schemas_2 = mocker.MagicMock(errors=[]) - schemas_2.models = {"1": model_1, "2": model_2} - error_1 = PropertyError() + error = PropertyError() schemas_3 = mocker.MagicMock() - schemas_4 = mocker.MagicMock(errors=[]) - model_1.resolve_references.return_value = schemas_4 - error_2 = PropertyError() - model_2.resolve_references.return_value = error_2 # This loops through one for each, then again to retry the error build_model_property.side_effect = [ (model_1, schemas_1), (model_2, schemas_2), - (error_1, schemas_3), - (error_1, schemas_3), + (error, schemas_3), + (error, schemas_3), ] from openapi_python_client.parser.properties import Schemas, build_schemas @@ -1024,12 +1016,8 @@ def test_build_schemas(mocker): ] ) # schemas_3 was the last to come back from build_model_property, but it should be ignored because it's an error - model_1.resolve_references.assert_called_once_with(components=in_data, schemas=schemas_2) - # schemas_4 came from resolving model_1 - model_2.resolve_references.assert_called_once_with(components=in_data, schemas=schemas_4) - # resolving model_2 resulted in err, so no schemas_5 - assert result == schemas_4 - assert result.errors == [error_1, error_2] + assert result == schemas_2 + assert result.errors == [error] def test_build_parse_error_on_reference(): @@ -1103,7 +1091,6 @@ def test_build_model_property(additional_properties_schema, expected_additional_ nullable=False, default=None, reference=Reference(class_name="ParentMyModel", module_name="parent_my_model"), - references=[], required_properties=[StringProperty(name="req", required=True, nullable=False, default=None)], optional_properties=[DateTimeProperty(name="opt", required=False, nullable=False, default=None)], description=data.description, diff --git a/tests/test_parser/test_properties/test_model_property.py b/tests/test_parser/test_properties/test_model_property.py index 421f40d48..1024ef179 100644 --- a/tests/test_parser/test_properties/test_model_property.py +++ b/tests/test_parser/test_properties/test_model_property.py @@ -23,7 +23,6 @@ def test_get_type_string(no_optional, nullable, required, expected): nullable=nullable, default=None, reference=Reference(class_name="MyClass", module_name="my_module"), - references=[], description="", optional_properties=[], required_properties=[], @@ -43,7 +42,6 @@ def test_get_imports(): nullable=True, default=None, reference=Reference(class_name="MyClass", module_name="my_module"), - references=[], description="", optional_properties=[], required_properties=[], @@ -59,143 +57,3 @@ def test_get_imports(): "from typing import Dict", "from typing import cast", } - - -def test_resolve_references(mocker): - import openapi_python_client.schema as oai - from openapi_python_client.parser.properties import build_model_property - - schemas = { - "RefA": oai.Schema.construct( - title=mocker.MagicMock(), - description=mocker.MagicMock(), - required=["String"], - properties={ - "String": oai.Schema.construct(type="string"), - "Enum": oai.Schema.construct(type="string", enum=["aValue"]), - "DateTime": oai.Schema.construct(type="string", format="date-time"), - }, - ), - "RefB": oai.Schema.construct( - title=mocker.MagicMock(), - description=mocker.MagicMock(), - required=["DateTime"], - properties={ - "Int": oai.Schema.construct(type="integer"), - "DateTime": oai.Schema.construct(type="string", format="date-time"), - "Float": oai.Schema.construct(type="number", format="float"), - }, - ), - # Intentionally no properties defined - "RefC": oai.Schema.construct( - title=mocker.MagicMock(), - description=mocker.MagicMock(), - ), - } - - model_schema = oai.Schema.construct( - allOf=[ - oai.Reference.construct(ref="#/components/schemas/RefA"), - oai.Reference.construct(ref="#/components/schemas/RefB"), - oai.Reference.construct(ref="#/components/schemas/RefC"), - oai.Schema.construct( - title=mocker.MagicMock(), - description=mocker.MagicMock(), - required=["Float"], - properties={ - "String": oai.Schema.construct(type="string"), - "Float": oai.Schema.construct(type="number", format="float"), - }, - ), - ] - ) - - components = {**schemas, "Model": model_schema} - - from openapi_python_client.parser.properties import Schemas - - schemas_holder = Schemas() - model, schemas_holder = build_model_property( - data=model_schema, name="Model", required=True, schemas=schemas_holder, parent_name=None - ) - model.resolve_references(components, schemas_holder) - assert sorted(p.name for p in model.required_properties) == ["DateTime", "Float", "String"] - assert all(p.required for p in model.required_properties) - assert sorted(p.name for p in model.optional_properties) == ["Enum", "Int"] - assert all(not p.required for p in model.optional_properties) - - -def test_resolve_references_nested_allof(mocker): - import openapi_python_client.schema as oai - from openapi_python_client.parser.properties import build_model_property - - schemas = { - "RefA": oai.Schema.construct( - title=mocker.MagicMock(), - description=mocker.MagicMock(), - required=["String"], - properties={ - "String": oai.Schema.construct(type="string"), - "Enum": oai.Schema.construct(type="string", enum=["aValue"]), - "DateTime": oai.Schema.construct(type="string", format="date-time"), - }, - ), - "RefB": oai.Schema.construct( - title=mocker.MagicMock(), - description=mocker.MagicMock(), - required=["DateTime"], - properties={ - "Int": oai.Schema.construct(type="integer"), - "DateTime": oai.Schema.construct(type="string", format="date-time"), - "Float": oai.Schema.construct(type="number", format="float"), - }, - ), - # Intentionally no properties defined - "RefC": oai.Schema.construct( - title=mocker.MagicMock(), - description=mocker.MagicMock(), - ), - } - - model_schema = oai.Schema.construct( - type="object", - properties={ - "Key": oai.Schema.construct( - allOf=[ - oai.Reference.construct(ref="#/components/schemas/RefA"), - oai.Reference.construct(ref="#/components/schemas/RefB"), - oai.Reference.construct(ref="#/components/schemas/RefC"), - oai.Schema.construct( - title=mocker.MagicMock(), - description=mocker.MagicMock(), - required=["Float"], - properties={ - "String": oai.Schema.construct(type="string"), - "Float": oai.Schema.construct(type="number", format="float"), - }, - ), - ] - ), - }, - ) - - components = {**schemas, "Model": model_schema} - - from openapi_python_client.parser.properties import ModelProperty, Schemas - - schemas_holder = Schemas() - model, schemas_holder = build_model_property( - data=model_schema, name="Model", required=True, schemas=schemas_holder, parent_name=None - ) - model.resolve_references(components, schemas_holder) - assert sorted(p.name for p in model.required_properties) == [] - assert sorted(p.name for p in model.optional_properties) == ["Key"] - assert all(not p.required for p in model.optional_properties) - - key_property = model.optional_properties[0] - assert isinstance(key_property, ModelProperty) - key_property.resolve_references(components, schemas_holder) - assert sorted(p.name for p in key_property.required_properties) == ["DateTime", "Float", "String"] - assert all(p.required for p in key_property.required_properties) - assert sorted(p.name for p in key_property.optional_properties) == ["Enum", "Int"] - assert all(not p.required for p in key_property.optional_properties) From 9600afc3eb1fa17fcb24f18830c4572f4bd8440b Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Thu, 11 Mar 2021 18:53:38 -0700 Subject: [PATCH 3/8] refactor: Clean up ModelProperty code --- end_to_end_tests/openapi.json | 2 +- .../parser/properties/__init__.py | 94 +----------- .../parser/properties/model_property.py | 125 ++++++++++++++- .../parser/properties/schemas.py | 11 +- .../test_parser/test_properties/test_init.py | 143 +----------------- .../test_properties/test_model_property.py | 138 +++++++++++++++++ 6 files changed, 273 insertions(+), 240 deletions(-) diff --git a/end_to_end_tests/openapi.json b/end_to_end_tests/openapi.json index 16971878a..10d466adb 100644 --- a/end_to_end_tests/openapi.json +++ b/end_to_end_tests/openapi.json @@ -716,7 +716,7 @@ "a_not_required_date": { "title": "A Nullable Date", "type": "string", - "format": "date", + "format": "date" }, "1_leading_digit": { "title": "Leading Digit", diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index 5533a6038..1578180ae 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -9,7 +9,7 @@ from ..reference import Reference from .converter import convert, convert_chain from .enum_property import EnumProperty -from .model_property import ModelProperty +from .model_property import ModelProperty, build_model_property from .property import Property from .schemas import Schemas @@ -234,98 +234,6 @@ def _string_based_property( ) -def build_model_property( - *, data: oai.Schema, name: str, schemas: Schemas, required: bool, parent_name: Optional[str] -) -> Tuple[Union[ModelProperty, PropertyError], Schemas]: - """ - A single ModelProperty from its OAI data - - Args: - data: Data of a single Schema - name: Name by which the schema is referenced, such as a model name. - Used to infer the type name if a `title` property is not available. - schemas: Existing Schemas which have already been processed (to check name conflicts) - """ - required_set = set(data.required or []) - required_properties: List[Property] = [] - optional_properties: List[Property] = [] - relative_imports: Set[str] = set() - - class_name = data.title or name - if parent_name: - class_name = f"{utils.pascal_case(parent_name)}{utils.pascal_case(class_name)}" - ref = Reference.from_ref(class_name) - - all_props = data.properties or {} - for sub_prop in data.allOf or []: - if isinstance(sub_prop, oai.Reference): - source_name = Reference.from_ref(sub_prop.ref).class_name - sub_model = schemas.models.get(source_name) - if sub_model is None: - return PropertyError(f"Reference {sub_prop.ref} not found"), schemas - required_properties.extend(sub_model.required_properties) - optional_properties.extend(sub_model.optional_properties) - relative_imports.update(sub_model.relative_imports) - else: - all_props.update(sub_prop.properties or {}) - required_set.update(sub_prop.required or []) - - for key, value in all_props.items(): - prop_required = key in required_set - prop, schemas = property_from_data( - name=key, required=prop_required, data=value, schemas=schemas, parent_name=class_name - ) - if isinstance(prop, PropertyError): - return prop, schemas - if prop_required and not prop.nullable: - required_properties.append(prop) - else: - optional_properties.append(prop) - relative_imports.update(prop.get_imports(prefix="..")) - - additional_properties: Union[bool, Property, PropertyError] - if data.additionalProperties is None: - additional_properties = True - elif isinstance(data.additionalProperties, bool): - additional_properties = data.additionalProperties - elif isinstance(data.additionalProperties, oai.Schema) and not any(data.additionalProperties.dict().values()): - # An empty schema - additional_properties = True - else: - assert isinstance(data.additionalProperties, (oai.Schema, oai.Reference)) - additional_properties, schemas = property_from_data( - name="AdditionalProperty", - required=True, # in the sense that if present in the dict will not be None - data=data.additionalProperties, - schemas=schemas, - parent_name=class_name, - ) - if isinstance(additional_properties, PropertyError): - return additional_properties, schemas - relative_imports.update(additional_properties.get_imports(prefix="..")) - - prop = ModelProperty( - reference=ref, - required_properties=required_properties, - optional_properties=optional_properties, - relative_imports=relative_imports, - description=data.description or "", - default=None, - nullable=data.nullable, - required=required, - name=name, - additional_properties=additional_properties, - ) - if prop.reference.class_name in schemas.models: - error = PropertyError( - data=data, detail=f'Attempted to generate duplicate models with name "{prop.reference.class_name}"' - ) - return error, schemas - - schemas = attr.evolve(schemas, models={**schemas.models, prop.reference.class_name: prop}) - return prop, schemas - - def build_enum_property( *, data: oai.Schema, diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index 0dda364c5..4266b03a5 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -1,9 +1,13 @@ -from typing import ClassVar, List, Set, Union +from typing import ClassVar, List, NamedTuple, Optional, Set, Tuple, Union import attr +from ... import schema as oai +from ... import utils +from ..errors import PropertyError from ..reference import Reference from .property import Property +from .schemas import Schemas @attr.s(auto_attribs=True, frozen=True) @@ -48,3 +52,122 @@ def get_imports(self, *, prefix: str) -> Set[str]: } ) return imports + + +class _PropertyData(NamedTuple): + optional_props: List[Property] + required_props: List[Property] + relative_imports: Set[str] + schemas: Schemas + + +def _process_properties(*, data: oai.Schema, schemas: Schemas, class_name: str) -> Union[_PropertyData, PropertyError]: + from . import property_from_data + + required_properties: List[Property] = [] + optional_properties: List[Property] = [] + relative_imports: Set[str] = set() + required_set = set(data.required or []) + + all_props = data.properties or {} + for sub_prop in data.allOf or []: + if isinstance(sub_prop, oai.Reference): + source_name = Reference.from_ref(sub_prop.ref).class_name + sub_model = schemas.models.get(source_name) + if sub_model is None: + return PropertyError(f"Reference {sub_prop.ref} not found") + required_properties.extend(sub_model.required_properties) + optional_properties.extend(sub_model.optional_properties) + relative_imports.update(sub_model.relative_imports) + else: + all_props.update(sub_prop.properties or {}) + required_set.update(sub_prop.required or []) + + for key, value in all_props.items(): + prop_required = key in required_set + prop, schemas = property_from_data( + name=key, required=prop_required, data=value, schemas=schemas, parent_name=class_name + ) + if isinstance(prop, PropertyError): + return prop + if prop_required and not prop.nullable: + required_properties.append(prop) + else: + optional_properties.append(prop) + relative_imports.update(prop.get_imports(prefix="..")) + + return _PropertyData( + optional_props=optional_properties, + required_props=required_properties, + relative_imports=relative_imports, + schemas=schemas, + ) + + +def build_model_property( + *, data: oai.Schema, name: str, schemas: Schemas, required: bool, parent_name: Optional[str] +) -> Tuple[Union[ModelProperty, PropertyError], Schemas]: + """ + A single ModelProperty from its OAI data + + Args: + data: Data of a single Schema + name: Name by which the schema is referenced, such as a model name. + Used to infer the type name if a `title` property is not available. + schemas: Existing Schemas which have already been processed (to check name conflicts) + required: Whether or not this property is required by the parent (affects typing) + parent_name: The name of the property that this property is inside of (affects class naming) + """ + from . import property_from_data + + class_name = data.title or name + if parent_name: + class_name = f"{utils.pascal_case(parent_name)}{utils.pascal_case(class_name)}" + ref = Reference.from_ref(class_name) + + property_data = _process_properties(data=data, schemas=schemas, class_name=class_name) + if isinstance(property_data, PropertyError): + return property_data, schemas + schemas = property_data.schemas + + additional_properties: Union[bool, Property, PropertyError] + if data.additionalProperties is None: + additional_properties = True + elif isinstance(data.additionalProperties, bool): + additional_properties = data.additionalProperties + elif isinstance(data.additionalProperties, oai.Schema) and not any(data.additionalProperties.dict().values()): + # An empty schema + additional_properties = True + else: + assert isinstance(data.additionalProperties, (oai.Schema, oai.Reference)) + additional_properties, schemas = property_from_data( + name="AdditionalProperty", + required=True, # in the sense that if present in the dict will not be None + data=data.additionalProperties, + schemas=schemas, + parent_name=class_name, + ) + if isinstance(additional_properties, PropertyError): + return additional_properties, schemas + property_data.relative_imports.update(additional_properties.get_imports(prefix="..")) + + prop = ModelProperty( + reference=ref, + required_properties=property_data.required_props, + optional_properties=property_data.optional_props, + relative_imports=property_data.relative_imports, + description=data.description or "", + default=None, + nullable=data.nullable, + required=required, + name=name, + additional_properties=additional_properties, + ) + if prop.reference.class_name in schemas.models: + error = PropertyError( + data=data, detail=f'Attempted to generate duplicate models with name "{prop.reference.class_name}"' + ) + return error, schemas + + schemas = attr.evolve(schemas, models={**schemas.models, prop.reference.class_name: prop}) + return prop, schemas diff --git a/openapi_python_client/parser/properties/schemas.py b/openapi_python_client/parser/properties/schemas.py index 338938673..cf65746a1 100644 --- a/openapi_python_client/parser/properties/schemas.py +++ b/openapi_python_client/parser/properties/schemas.py @@ -1,12 +1,17 @@ __all__ = ["Schemas"] -from typing import Dict, List +from typing import TYPE_CHECKING, Dict, List import attr from ..errors import ParseError -from .enum_property import EnumProperty -from .model_property import ModelProperty + +if TYPE_CHECKING: + from .enum_property import EnumProperty + from .model_property import ModelProperty +else: + EnumProperty = "EnumProperty" + ModelProperty = "ModelProperty" @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 ee07d3973..696dfb58f 100644 --- a/tests/test_parser/test_properties/test_init.py +++ b/tests/test_parser/test_properties/test_init.py @@ -2,15 +2,7 @@ import openapi_python_client.schema as oai from openapi_python_client.parser.errors import PropertyError, ValidationError -from openapi_python_client.parser.properties import ( - BooleanProperty, - DateTimeProperty, - FloatProperty, - IntProperty, - ModelProperty, - StringProperty, -) -from openapi_python_client.parser.reference import Reference +from openapi_python_client.parser.properties import BooleanProperty, FloatProperty, IntProperty MODULE_NAME = "openapi_python_client.parser.properties" @@ -1043,139 +1035,6 @@ def test_build_enums(mocker): build_model_property.assert_not_called() -@pytest.mark.parametrize( - "additional_properties_schema, expected_additional_properties", - [ - (True, True), - (oai.Schema.construct(), True), - (None, True), - (False, False), - ( - oai.Schema.construct(type="string"), - StringProperty(name="AdditionalProperty", required=True, nullable=False, default=None), - ), - ], -) -def test_build_model_property(additional_properties_schema, expected_additional_properties): - from openapi_python_client.parser.properties import Schemas, build_model_property - - data = oai.Schema.construct( - required=["req"], - title="MyModel", - properties={ - "req": oai.Schema.construct(type="string"), - "opt": oai.Schema(type="string", format="date-time"), - }, - description="A class called MyModel", - nullable=False, - additionalProperties=additional_properties_schema, - ) - schemas = Schemas(models={"OtherModel": None}) - - model, new_schemas = build_model_property( - data=data, - name="prop", - schemas=schemas, - required=True, - parent_name="parent", - ) - - assert new_schemas != schemas - assert new_schemas.models == { - "OtherModel": None, - "ParentMyModel": model, - } - assert model == ModelProperty( - name="prop", - required=True, - nullable=False, - default=None, - reference=Reference(class_name="ParentMyModel", module_name="parent_my_model"), - required_properties=[StringProperty(name="req", required=True, nullable=False, default=None)], - optional_properties=[DateTimeProperty(name="opt", required=False, nullable=False, default=None)], - description=data.description, - relative_imports={ - "from dateutil.parser import isoparse", - "from typing import cast", - "import datetime", - "from ..types import UNSET, Unset", - "from typing import Union", - }, - additional_properties=expected_additional_properties, - ) - - -def test_build_model_property_conflict(): - from openapi_python_client.parser.properties import Schemas, build_model_property - - data = oai.Schema.construct( - required=["req"], - properties={ - "req": oai.Schema.construct(type="string"), - "opt": oai.Schema(type="string", format="date-time"), - }, - nullable=False, - ) - schemas = Schemas(models={"OtherModel": None}) - - err, new_schemas = build_model_property( - data=data, - name="OtherModel", - schemas=schemas, - required=True, - parent_name=None, - ) - - assert new_schemas == schemas - assert err == PropertyError(detail='Attempted to generate duplicate models with name "OtherModel"', data=data) - - -def test_build_model_property_bad_prop(): - from openapi_python_client.parser.properties import Schemas, build_model_property - - data = oai.Schema( - properties={ - "bad": oai.Schema(type="not_real"), - }, - ) - schemas = Schemas(models={"OtherModel": None}) - - err, new_schemas = build_model_property( - data=data, - name="prop", - schemas=schemas, - required=True, - parent_name=None, - ) - - assert new_schemas == schemas - assert err == PropertyError(detail="unknown type not_real", data=oai.Schema(type="not_real")) - - -def test_build_model_property_bad_additional_props(): - from openapi_python_client.parser.properties import Schemas, build_model_property - - additional_properties = oai.Schema( - type="object", - properties={ - "bad": oai.Schema(type="not_real"), - }, - ) - data = oai.Schema(additionalProperties=additional_properties) - schemas = Schemas(models={"OtherModel": None}) - - err, new_schemas = build_model_property( - data=data, - name="prop", - schemas=schemas, - required=True, - parent_name=None, - ) - - assert new_schemas == schemas - assert err == PropertyError(detail="unknown type not_real", data=oai.Schema(type="not_real")) - - def test_build_enum_property_conflict(mocker): from openapi_python_client.parser.properties import Schemas, build_enum_property diff --git a/tests/test_parser/test_properties/test_model_property.py b/tests/test_parser/test_properties/test_model_property.py index 1024ef179..ece72a2fd 100644 --- a/tests/test_parser/test_properties/test_model_property.py +++ b/tests/test_parser/test_properties/test_model_property.py @@ -1,5 +1,10 @@ import pytest +import openapi_python_client.schema as oai +from openapi_python_client.parser.errors import PropertyError +from openapi_python_client.parser.properties import DateTimeProperty, ModelProperty, StringProperty +from openapi_python_client.parser.reference import Reference + @pytest.mark.parametrize( "no_optional,nullable,required,expected", @@ -57,3 +62,136 @@ def test_get_imports(): "from typing import Dict", "from typing import cast", } + + +@pytest.mark.parametrize( + "additional_properties_schema, expected_additional_properties", + [ + (True, True), + (oai.Schema.construct(), True), + (None, True), + (False, False), + ( + oai.Schema.construct(type="string"), + StringProperty(name="AdditionalProperty", required=True, nullable=False, default=None), + ), + ], +) +def test_build_model_property(additional_properties_schema, expected_additional_properties): + from openapi_python_client.parser.properties import Schemas, build_model_property + + data = oai.Schema.construct( + required=["req"], + title="MyModel", + properties={ + "req": oai.Schema.construct(type="string"), + "opt": oai.Schema(type="string", format="date-time"), + }, + description="A class called MyModel", + nullable=False, + additionalProperties=additional_properties_schema, + ) + schemas = Schemas(models={"OtherModel": None}) + + model, new_schemas = build_model_property( + data=data, + name="prop", + schemas=schemas, + required=True, + parent_name="parent", + ) + + assert new_schemas != schemas + assert new_schemas.models == { + "OtherModel": None, + "ParentMyModel": model, + } + assert model == ModelProperty( + name="prop", + required=True, + nullable=False, + default=None, + reference=Reference(class_name="ParentMyModel", module_name="parent_my_model"), + required_properties=[StringProperty(name="req", required=True, nullable=False, default=None)], + optional_properties=[DateTimeProperty(name="opt", required=False, nullable=False, default=None)], + description=data.description, + relative_imports={ + "from dateutil.parser import isoparse", + "from typing import cast", + "import datetime", + "from ..types import UNSET, Unset", + "from typing import Union", + }, + additional_properties=expected_additional_properties, + ) + + +def test_build_model_property_conflict(): + from openapi_python_client.parser.properties import Schemas, build_model_property + + data = oai.Schema.construct( + required=["req"], + properties={ + "req": oai.Schema.construct(type="string"), + "opt": oai.Schema(type="string", format="date-time"), + }, + nullable=False, + ) + schemas = Schemas(models={"OtherModel": None}) + + err, new_schemas = build_model_property( + data=data, + name="OtherModel", + schemas=schemas, + required=True, + parent_name=None, + ) + + assert new_schemas == schemas + assert err == PropertyError(detail='Attempted to generate duplicate models with name "OtherModel"', data=data) + + +def test_build_model_property_bad_prop(): + from openapi_python_client.parser.properties import Schemas, build_model_property + + data = oai.Schema( + properties={ + "bad": oai.Schema(type="not_real"), + }, + ) + schemas = Schemas(models={"OtherModel": None}) + + err, new_schemas = build_model_property( + data=data, + name="prop", + schemas=schemas, + required=True, + parent_name=None, + ) + + assert new_schemas == schemas + assert err == PropertyError(detail="unknown type not_real", data=oai.Schema(type="not_real")) + + +def test_build_model_property_bad_additional_props(): + from openapi_python_client.parser.properties import Schemas, build_model_property + + additional_properties = oai.Schema( + type="object", + properties={ + "bad": oai.Schema(type="not_real"), + }, + ) + data = oai.Schema(additionalProperties=additional_properties) + schemas = Schemas(models={"OtherModel": None}) + + err, new_schemas = build_model_property( + data=data, + name="prop", + schemas=schemas, + required=True, + parent_name=None, + ) + + assert new_schemas == schemas + assert err == PropertyError(detail="unknown type not_real", data=oai.Schema(type="not_real")) From a3f8fa86fc6769052fd23c56022a90bc631e9a7b Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sat, 13 Mar 2021 12:08:05 -0700 Subject: [PATCH 4/8] refactor: More test cleanup for ModelProperty --- mypy.ini | 1 + .../parser/properties/model_property.py | 52 ++-- .../test_properties/test_model_property.py | 267 +++++++++--------- 3 files changed, 169 insertions(+), 151 deletions(-) diff --git a/mypy.ini b/mypy.ini index a85aa5f4d..b37bb1eb0 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,4 +1,5 @@ [mypy] +plugins = pydantic.mypy disallow_any_generics = True disallow_untyped_defs = True warn_redundant_casts = True diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index 4266b03a5..b90570194 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -104,6 +104,31 @@ def _process_properties(*, data: oai.Schema, schemas: Schemas, class_name: str) ) +def _get_additional_properties( + *, schema_additional: Union[None, bool, oai.Reference, oai.Schema], schemas: Schemas, class_name: str +) -> Tuple[Union[bool, Property, PropertyError], Schemas]: + from . import property_from_data + + if schema_additional is None: + return True, schemas + + if isinstance(schema_additional, bool): + return schema_additional, schemas + + if isinstance(schema_additional, oai.Schema) and not any(schema_additional.dict().values()): + # An empty schema + return True, schemas + + additional_properties, schemas = property_from_data( + name="AdditionalProperty", + required=True, # in the sense that if present in the dict will not be None + data=schema_additional, + schemas=schemas, + parent_name=class_name, + ) + return additional_properties, schemas + + def build_model_property( *, data: oai.Schema, name: str, schemas: Schemas, required: bool, parent_name: Optional[str] ) -> Tuple[Union[ModelProperty, PropertyError], Schemas]: @@ -118,8 +143,6 @@ def build_model_property( required: Whether or not this property is required by the parent (affects typing) parent_name: The name of the property that this property is inside of (affects class naming) """ - from . import property_from_data - class_name = data.title or name if parent_name: class_name = f"{utils.pascal_case(parent_name)}{utils.pascal_case(class_name)}" @@ -130,26 +153,13 @@ def build_model_property( return property_data, schemas schemas = property_data.schemas - additional_properties: Union[bool, Property, PropertyError] - if data.additionalProperties is None: - additional_properties = True - elif isinstance(data.additionalProperties, bool): - additional_properties = data.additionalProperties - elif isinstance(data.additionalProperties, oai.Schema) and not any(data.additionalProperties.dict().values()): - # An empty schema - additional_properties = True - else: - assert isinstance(data.additionalProperties, (oai.Schema, oai.Reference)) - additional_properties, schemas = property_from_data( - name="AdditionalProperty", - required=True, # in the sense that if present in the dict will not be None - data=data.additionalProperties, - schemas=schemas, - parent_name=class_name, - ) - if isinstance(additional_properties, PropertyError): - return additional_properties, schemas + additional_properties, schemas = _get_additional_properties( + schema_additional=data.additionalProperties, schemas=schemas, class_name=class_name + ) + if isinstance(additional_properties, Property): property_data.relative_imports.update(additional_properties.get_imports(prefix="..")) + elif isinstance(additional_properties, PropertyError): + return additional_properties, schemas prop = ModelProperty( reference=ref, diff --git a/tests/test_parser/test_properties/test_model_property.py b/tests/test_parser/test_properties/test_model_property.py index ece72a2fd..21847d0fb 100644 --- a/tests/test_parser/test_properties/test_model_property.py +++ b/tests/test_parser/test_properties/test_model_property.py @@ -64,134 +64,141 @@ def test_get_imports(): } -@pytest.mark.parametrize( - "additional_properties_schema, expected_additional_properties", - [ - (True, True), - (oai.Schema.construct(), True), - (None, True), - (False, False), - ( - oai.Schema.construct(type="string"), - StringProperty(name="AdditionalProperty", required=True, nullable=False, default=None), - ), - ], -) -def test_build_model_property(additional_properties_schema, expected_additional_properties): - from openapi_python_client.parser.properties import Schemas, build_model_property - - data = oai.Schema.construct( - required=["req"], - title="MyModel", - properties={ - "req": oai.Schema.construct(type="string"), - "opt": oai.Schema(type="string", format="date-time"), - }, - description="A class called MyModel", - nullable=False, - additionalProperties=additional_properties_schema, - ) - schemas = Schemas(models={"OtherModel": None}) - - model, new_schemas = build_model_property( - data=data, - name="prop", - schemas=schemas, - required=True, - parent_name="parent", - ) - - assert new_schemas != schemas - assert new_schemas.models == { - "OtherModel": None, - "ParentMyModel": model, - } - assert model == ModelProperty( - name="prop", - required=True, - nullable=False, - default=None, - reference=Reference(class_name="ParentMyModel", module_name="parent_my_model"), - required_properties=[StringProperty(name="req", required=True, nullable=False, default=None)], - optional_properties=[DateTimeProperty(name="opt", required=False, nullable=False, default=None)], - description=data.description, - relative_imports={ - "from dateutil.parser import isoparse", - "from typing import cast", - "import datetime", - "from ..types import UNSET, Unset", - "from typing import Union", - }, - additional_properties=expected_additional_properties, - ) - - -def test_build_model_property_conflict(): - from openapi_python_client.parser.properties import Schemas, build_model_property - - data = oai.Schema.construct( - required=["req"], - properties={ - "req": oai.Schema.construct(type="string"), - "opt": oai.Schema(type="string", format="date-time"), - }, - nullable=False, - ) - schemas = Schemas(models={"OtherModel": None}) - - err, new_schemas = build_model_property( - data=data, - name="OtherModel", - schemas=schemas, - required=True, - parent_name=None, +class TestBuildModelProperty: + @pytest.mark.parametrize( + "additional_properties_schema, expected_additional_properties", + [ + (True, True), + (oai.Schema.construct(), True), + (None, True), + (False, False), + ( + oai.Schema.construct(type="string"), + StringProperty(name="AdditionalProperty", required=True, nullable=False, default=None), + ), + ], ) - - assert new_schemas == schemas - assert err == PropertyError(detail='Attempted to generate duplicate models with name "OtherModel"', data=data) - - -def test_build_model_property_bad_prop(): - from openapi_python_client.parser.properties import Schemas, build_model_property - - data = oai.Schema( - properties={ - "bad": oai.Schema(type="not_real"), - }, - ) - schemas = Schemas(models={"OtherModel": None}) - - err, new_schemas = build_model_property( - data=data, - name="prop", - schemas=schemas, - required=True, - parent_name=None, - ) - - assert new_schemas == schemas - assert err == PropertyError(detail="unknown type not_real", data=oai.Schema(type="not_real")) - - -def test_build_model_property_bad_additional_props(): - from openapi_python_client.parser.properties import Schemas, build_model_property - - additional_properties = oai.Schema( - type="object", - properties={ - "bad": oai.Schema(type="not_real"), - }, - ) - data = oai.Schema(additionalProperties=additional_properties) - schemas = Schemas(models={"OtherModel": None}) - - err, new_schemas = build_model_property( - data=data, - name="prop", - schemas=schemas, - required=True, - parent_name=None, - ) - - assert new_schemas == schemas - assert err == PropertyError(detail="unknown type not_real", data=oai.Schema(type="not_real")) + def test_additional_schemas(self, additional_properties_schema, expected_additional_properties): + from openapi_python_client.parser.properties import Schemas, build_model_property + + data = oai.Schema.construct( + additionalProperties=additional_properties_schema, + ) + + model, _ = build_model_property( + data=data, + name="prop", + schemas=Schemas(), + required=True, + parent_name="parent", + ) + + assert model.additional_properties == expected_additional_properties + + def test_happy_path(self): + from openapi_python_client.parser.properties import Schemas, build_model_property + + data = oai.Schema.construct( + required=["req"], + title="MyModel", + properties={ + "req": oai.Schema.construct(type="string"), + "opt": oai.Schema(type="string", format="date-time"), + }, + description="A class called MyModel", + nullable=False, + ) + schemas = Schemas(models={"OtherModel": None}) + + model, new_schemas = build_model_property( + data=data, + name="prop", + schemas=schemas, + required=True, + parent_name="parent", + ) + + assert new_schemas != schemas + assert new_schemas.models == { + "OtherModel": None, + "ParentMyModel": model, + } + assert model == ModelProperty( + name="prop", + required=True, + nullable=False, + default=None, + reference=Reference(class_name="ParentMyModel", module_name="parent_my_model"), + required_properties=[StringProperty(name="req", required=True, nullable=False, default=None)], + optional_properties=[DateTimeProperty(name="opt", required=False, nullable=False, default=None)], + description=data.description, + relative_imports={ + "from dateutil.parser import isoparse", + "from typing import cast", + "import datetime", + "from ..types import UNSET, Unset", + "from typing import Union", + }, + additional_properties=True, + ) + + def test_model_name_conflict(self): + from openapi_python_client.parser.properties import Schemas, build_model_property + + data = oai.Schema.construct() + schemas = Schemas(models={"OtherModel": None}) + + err, new_schemas = build_model_property( + data=data, + name="OtherModel", + schemas=schemas, + required=True, + parent_name=None, + ) + + assert new_schemas == schemas + assert err == PropertyError(detail='Attempted to generate duplicate models with name "OtherModel"', data=data) + + def test_bad_props_return_error(self): + from openapi_python_client.parser.properties import Schemas, build_model_property + + data = oai.Schema( + properties={ + "bad": oai.Schema(type="not_real"), + }, + ) + schemas = Schemas() + + err, new_schemas = build_model_property( + data=data, + name="prop", + schemas=schemas, + required=True, + parent_name=None, + ) + + assert new_schemas == schemas + assert err == PropertyError(detail="unknown type not_real", data=oai.Schema(type="not_real")) + + def test_bad_additional_props_return_error(self): + from openapi_python_client.parser.properties import Schemas, build_model_property + + additional_properties = oai.Schema( + type="object", + properties={ + "bad": oai.Schema(type="not_real"), + }, + ) + data = oai.Schema(additionalProperties=additional_properties) + schemas = Schemas() + + err, new_schemas = build_model_property( + data=data, + name="prop", + schemas=schemas, + required=True, + parent_name=None, + ) + + assert new_schemas == schemas + assert err == PropertyError(detail="unknown type not_real", data=oai.Schema(type="not_real")) From 70060bf37d3ac65ce6780515e27169e66d6af7a8 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sat, 13 Mar 2021 13:59:26 -0700 Subject: [PATCH 5/8] fix: Handle duplicate and conflicting properties in allOf --- .../parser/properties/model_property.py | 50 ++++++-- .../test_properties/test_model_property.py | 109 ++++++++++++++++++ 2 files changed, 149 insertions(+), 10 deletions(-) diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index b90570194..67fe45d49 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -1,4 +1,5 @@ -from typing import ClassVar, List, NamedTuple, Optional, Set, Tuple, Union +from itertools import chain +from typing import ClassVar, Dict, List, NamedTuple, Optional, Set, Tuple, Union import attr @@ -54,6 +55,18 @@ def get_imports(self, *, prefix: str) -> Set[str]: return imports +def _merge_properties(first: Property, second: Property) -> Union[Property, PropertyError]: + if first.__class__ != second.__class__: + return PropertyError(header="Cannot merge properties", detail="Properties are two different types") + nullable = first.nullable and second.nullable + required = first.required or second.required + first = attr.evolve(first, nullable=nullable, required=required) + second = attr.evolve(second, nullable=nullable, required=required) + if first != second: + return PropertyError(header="Cannot merge properties", detail="Properties has conflicting values") + return first + + class _PropertyData(NamedTuple): optional_props: List[Property] required_props: List[Property] @@ -64,11 +77,19 @@ class _PropertyData(NamedTuple): def _process_properties(*, data: oai.Schema, schemas: Schemas, class_name: str) -> Union[_PropertyData, PropertyError]: from . import property_from_data - required_properties: List[Property] = [] - optional_properties: List[Property] = [] + properties: Dict[str, Property] = {} relative_imports: Set[str] = set() required_set = set(data.required or []) + def _check_existing(prop: Property) -> Union[Property, PropertyError]: + existing = properties.get(prop.name) + prop_or_error = (existing and _merge_properties(existing, prop)) or prop + if isinstance(prop_or_error, PropertyError): + prop_or_error.header = f"Found conflicting properties named {prop.name} when creating {class_name}" + return prop_or_error + properties[prop_or_error.name] = prop_or_error + return prop_or_error + all_props = data.properties or {} for sub_prop in data.allOf or []: if isinstance(sub_prop, oai.Reference): @@ -76,21 +97,30 @@ def _process_properties(*, data: oai.Schema, schemas: Schemas, class_name: str) sub_model = schemas.models.get(source_name) if sub_model is None: return PropertyError(f"Reference {sub_prop.ref} not found") - required_properties.extend(sub_model.required_properties) - optional_properties.extend(sub_model.optional_properties) - relative_imports.update(sub_model.relative_imports) + for prop in chain(sub_model.required_properties, sub_model.optional_properties): + prop_or_error = _check_existing(prop) + if isinstance(prop_or_error, PropertyError): + return prop_or_error else: all_props.update(sub_prop.properties or {}) required_set.update(sub_prop.required or []) for key, value in all_props.items(): prop_required = key in required_set - prop, schemas = property_from_data( + prop_or_error, schemas = property_from_data( name=key, required=prop_required, data=value, schemas=schemas, parent_name=class_name ) - if isinstance(prop, PropertyError): - return prop - if prop_required and not prop.nullable: + if isinstance(prop_or_error, Property): + prop_or_error = _check_existing(prop_or_error) + if isinstance(prop_or_error, PropertyError): + return prop_or_error + + properties[prop_or_error.name] = prop_or_error + + required_properties = [] + optional_properties = [] + for prop in properties.values(): + if prop.required and not prop.nullable: required_properties.append(prop) else: optional_properties.append(prop) diff --git a/tests/test_parser/test_properties/test_model_property.py b/tests/test_parser/test_properties/test_model_property.py index 21847d0fb..33984cb6c 100644 --- a/tests/test_parser/test_properties/test_model_property.py +++ b/tests/test_parser/test_properties/test_model_property.py @@ -1,3 +1,5 @@ +from typing import Callable + import pytest import openapi_python_client.schema as oai @@ -202,3 +204,110 @@ def test_bad_additional_props_return_error(self): assert new_schemas == schemas assert err == PropertyError(detail="unknown type not_real", data=oai.Schema(type="not_real")) + + +@pytest.fixture +def model_property() -> Callable[..., ModelProperty]: + from openapi_python_client.parser.reference import Reference + + def _factory(**kwargs): + kwargs = { + "name": "", + "description": "", + "required": True, + "nullable": True, + "default": None, + "reference": Reference(class_name="", module_name=""), + "required_properties": [], + "optional_properties": [], + "relative_imports": set(), + "additional_properties": False, + **kwargs, + } + return ModelProperty(**kwargs) + + return _factory + + +def string_property(**kwargs) -> StringProperty: + kwargs = { + "name": "", + "required": True, + "nullable": True, + "default": None, + **kwargs, + } + return StringProperty(**kwargs) + + +class TestProcessProperties: + def test_conflicting_properties(self, model_property): + from openapi_python_client.parser.properties import Schemas + from openapi_python_client.parser.properties.model_property import _process_properties + + data = oai.Schema.construct(allOf=[oai.Reference.construct(ref="First"), oai.Reference.construct(ref="Second")]) + schemas = Schemas( + models={ + "First": model_property( + optional_properties=[StringProperty(name="prop", required=True, nullable=True, default=None)] + ), + "Second": model_property( + optional_properties=[DateTimeProperty(name="prop", required=True, nullable=True, default=None)] + ), + } + ) + + result = _process_properties(data=data, schemas=schemas, class_name="") + + assert isinstance(result, PropertyError) + + def test_duplicate_properties(self, model_property): + from openapi_python_client.parser.properties import Schemas + from openapi_python_client.parser.properties.model_property import _process_properties + + data = oai.Schema.construct(allOf=[oai.Reference.construct(ref="First"), oai.Reference.construct(ref="Second")]) + prop = string_property() + schemas = Schemas( + models={ + "First": model_property(optional_properties=[prop]), + "Second": model_property(optional_properties=[prop]), + } + ) + + result = _process_properties(data=data, schemas=schemas, class_name="") + + assert result.optional_props == [prop], "There should only be one copy of duplicate properties" + + @pytest.mark.parametrize("first_nullable", [True, False]) + @pytest.mark.parametrize("second_nullable", [True, False]) + @pytest.mark.parametrize("first_required", [True, False]) + @pytest.mark.parametrize("second_required", [True, False]) + def test_mixed_requirements(self, model_property, first_nullable, second_nullable, first_required, second_required): + from openapi_python_client.parser.properties import Schemas + from openapi_python_client.parser.properties.model_property import _process_properties + + data = oai.Schema.construct(allOf=[oai.Reference.construct(ref="First"), oai.Reference.construct(ref="Second")]) + schemas = Schemas( + models={ + "First": model_property( + optional_properties=[string_property(required=first_required, nullable=first_nullable)] + ), + "Second": model_property( + optional_properties=[string_property(required=second_required, nullable=second_nullable)] + ), + } + ) + + result = _process_properties(data=data, schemas=schemas, class_name="") + + nullable = first_nullable and second_nullable + required = first_required or second_required + expected_prop = string_property( + nullable=nullable, + required=required, + ) + + if nullable or not required: + assert result.optional_props == [expected_prop] + else: + assert result.required_props == [expected_prop] From a19763fd5bb21269b6894df4cbee10b5c5d1abcd Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sat, 13 Mar 2021 14:14:11 -0700 Subject: [PATCH 6/8] ci: Improve test coverage --- .../parser/properties/schemas.py | 2 +- .../test_properties/test_model_property.py | 40 ++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/openapi_python_client/parser/properties/schemas.py b/openapi_python_client/parser/properties/schemas.py index cf65746a1..c30f6a059 100644 --- a/openapi_python_client/parser/properties/schemas.py +++ b/openapi_python_client/parser/properties/schemas.py @@ -6,7 +6,7 @@ from ..errors import ParseError -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from .enum_property import EnumProperty from .model_property import ModelProperty else: diff --git a/tests/test_parser/test_properties/test_model_property.py b/tests/test_parser/test_properties/test_model_property.py index 33984cb6c..b90454255 100644 --- a/tests/test_parser/test_properties/test_model_property.py +++ b/tests/test_parser/test_properties/test_model_property.py @@ -241,7 +241,7 @@ def string_property(**kwargs) -> StringProperty: class TestProcessProperties: - def test_conflicting_properties(self, model_property): + def test_conflicting_properties_different_types(self, model_property): from openapi_python_client.parser.properties import Schemas from openapi_python_client.parser.properties.model_property import _process_properties @@ -261,6 +261,22 @@ def test_conflicting_properties(self, model_property): assert isinstance(result, PropertyError) + def test_conflicting_properties_same_types(self, model_property): + from openapi_python_client.parser.properties import Schemas + from openapi_python_client.parser.properties.model_property import _process_properties + + data = oai.Schema.construct(allOf=[oai.Reference.construct(ref="First"), oai.Reference.construct(ref="Second")]) + schemas = Schemas( + models={ + "First": model_property(optional_properties=[string_property(default="abc")]), + "Second": model_property(optional_properties=[string_property()]), + } + ) + + result = _process_properties(data=data, schemas=schemas, class_name="") + + assert isinstance(result, PropertyError) + def test_duplicate_properties(self, model_property): from openapi_python_client.parser.properties import Schemas from openapi_python_client.parser.properties.model_property import _process_properties @@ -311,3 +327,25 @@ def test_mixed_requirements(self, model_property, first_nullable, second_nullabl assert result.optional_props == [expected_prop] else: assert result.required_props == [expected_prop] + + def test_direct_properties_non_ref(self): + from openapi_python_client.parser.properties import Schemas + from openapi_python_client.parser.properties.model_property import _process_properties + + data = oai.Schema.construct( + allOf=[ + oai.Schema.construct( + required=["first"], + properties={ + "first": oai.Schema.construct(type="string"), + "second": oai.Schema.construct(type="string"), + }, + ) + ] + ) + schemas = Schemas() + + result = _process_properties(data=data, schemas=schemas, class_name="") + + assert result.optional_props == [string_property(name="second", required=False, nullable=False)] + assert result.required_props == [string_property(name="first", required=True, nullable=False)] From 08a450a4ecd36379f678c158da523208271e1eb5 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sat, 13 Mar 2021 14:33:37 -0700 Subject: [PATCH 7/8] refactor: A bit of cleanup for clarity --- .../parser/properties/model_property.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index 67fe45d49..47ab5dfd8 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -82,15 +82,17 @@ def _process_properties(*, data: oai.Schema, schemas: Schemas, class_name: str) required_set = set(data.required or []) def _check_existing(prop: Property) -> Union[Property, PropertyError]: + nonlocal properties + existing = properties.get(prop.name) - prop_or_error = (existing and _merge_properties(existing, prop)) or prop + prop_or_error = _merge_properties(existing, prop) if existing else prop if isinstance(prop_or_error, PropertyError): prop_or_error.header = f"Found conflicting properties named {prop.name} when creating {class_name}" return prop_or_error properties[prop_or_error.name] = prop_or_error return prop_or_error - all_props = data.properties or {} + unprocessed_props = data.properties or {} for sub_prop in data.allOf or []: if isinstance(sub_prop, oai.Reference): source_name = Reference.from_ref(sub_prop.ref).class_name @@ -102,10 +104,10 @@ def _check_existing(prop: Property) -> Union[Property, PropertyError]: if isinstance(prop_or_error, PropertyError): return prop_or_error else: - all_props.update(sub_prop.properties or {}) + unprocessed_props.update(sub_prop.properties or {}) required_set.update(sub_prop.required or []) - for key, value in all_props.items(): + for key, value in unprocessed_props.items(): prop_required = key in required_set prop_or_error, schemas = property_from_data( name=key, required=prop_required, data=value, schemas=schemas, parent_name=class_name From ee2b8af47b46423e9541c6b3a1069b83d291e9e3 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Tue, 16 Mar 2021 08:19:58 -0600 Subject: [PATCH 8/8] Merge main into benching-allof-support --- .../custom_e2e/models/a_model_model.py | 44 +++++++++++-------- .../models/a_model_not_required_model.py | 44 +++++++++++-------- .../a_model_not_required_nullable_model.py | 44 +++++++++++-------- .../models/a_model_nullable_model.py | 44 +++++++++++-------- .../models/a_model_model.py | 44 +++++++++++-------- .../models/a_model_not_required_model.py | 44 +++++++++++-------- .../a_model_not_required_nullable_model.py | 44 +++++++++++-------- .../models/a_model_nullable_model.py | 44 +++++++++++-------- 8 files changed, 200 insertions(+), 152 deletions(-) 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 index 6cf422c40..0a7a54bd6 100644 --- 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 @@ -13,22 +13,22 @@ class AModelModel: """ """ - a_property: Union[Unset, AnEnum, AnIntEnum] = UNSET + a_property: Union[AnEnum, AnIntEnum, Unset] = 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] + a_property: Union[Unset, int, str] 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 + a_property = self.a_property.value else: a_property = UNSET if not isinstance(self.a_property, Unset): - a_property = self.a_property + a_property = self.a_property.value field_dict: Dict[str, Any] = {} field_dict.update(self.additional_properties) @@ -42,24 +42,30 @@ def to_dict(self) -> Dict[str, Any]: def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: 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] + def _parse_a_property(data: object) -> Union[AnEnum, AnIntEnum, Unset]: + if isinstance(data, Unset): + return data try: - a_property = UNSET - _a_property = data - if not isinstance(_a_property, Unset): - a_property = AnEnum(_a_property) - - return a_property + a_property_type0: Union[Unset, AnEnum] + if not isinstance(data, str): + raise TypeError() + a_property_type0 = UNSET + _a_property_type0 = data + if not isinstance(_a_property_type0, Unset): + a_property_type0 = AnEnum(_a_property_type0) + + return a_property_type0 except: # noqa: E722 pass - a_property = UNSET - _a_property = data - if not isinstance(_a_property, Unset): - a_property = AnIntEnum(_a_property) - - return a_property + if not isinstance(data, int): + raise TypeError() + a_property_type1: Union[Unset, AnIntEnum] + a_property_type1 = UNSET + _a_property_type1 = data + if not isinstance(_a_property_type1, Unset): + a_property_type1 = AnIntEnum(_a_property_type1) + + return a_property_type1 a_property = _parse_a_property(d.pop("a_property", UNSET)) 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 index 2dccd3bd3..fd568db52 100644 --- 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 @@ -13,22 +13,22 @@ class AModelNotRequiredModel: """ """ - a_property: Union[Unset, AnEnum, AnIntEnum] = UNSET + a_property: Union[AnEnum, AnIntEnum, Unset] = 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] + a_property: Union[Unset, int, str] 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 + a_property = self.a_property.value else: a_property = UNSET if not isinstance(self.a_property, Unset): - a_property = self.a_property + a_property = self.a_property.value field_dict: Dict[str, Any] = {} field_dict.update(self.additional_properties) @@ -42,24 +42,30 @@ def to_dict(self) -> Dict[str, Any]: def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: 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] + def _parse_a_property(data: object) -> Union[AnEnum, AnIntEnum, Unset]: + if isinstance(data, Unset): + return data try: - a_property = UNSET - _a_property = data - if not isinstance(_a_property, Unset): - a_property = AnEnum(_a_property) - - return a_property + a_property_type0: Union[Unset, AnEnum] + if not isinstance(data, str): + raise TypeError() + a_property_type0 = UNSET + _a_property_type0 = data + if not isinstance(_a_property_type0, Unset): + a_property_type0 = AnEnum(_a_property_type0) + + return a_property_type0 except: # noqa: E722 pass - a_property = UNSET - _a_property = data - if not isinstance(_a_property, Unset): - a_property = AnIntEnum(_a_property) - - return a_property + if not isinstance(data, int): + raise TypeError() + a_property_type1: Union[Unset, AnIntEnum] + a_property_type1 = UNSET + _a_property_type1 = data + if not isinstance(_a_property_type1, Unset): + a_property_type1 = AnIntEnum(_a_property_type1) + + return a_property_type1 a_property = _parse_a_property(d.pop("a_property", UNSET)) 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 index f048e8ebb..6413e7f9a 100644 --- 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 @@ -13,22 +13,22 @@ class AModelNotRequiredNullableModel: """ """ - a_property: Union[Unset, AnEnum, AnIntEnum] = UNSET + a_property: Union[AnEnum, AnIntEnum, Unset] = 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] + a_property: Union[Unset, int, str] 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 + a_property = self.a_property.value else: a_property = UNSET if not isinstance(self.a_property, Unset): - a_property = self.a_property + a_property = self.a_property.value field_dict: Dict[str, Any] = {} field_dict.update(self.additional_properties) @@ -42,24 +42,30 @@ def to_dict(self) -> Dict[str, Any]: def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: 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] + def _parse_a_property(data: object) -> Union[AnEnum, AnIntEnum, Unset]: + if isinstance(data, Unset): + return data try: - a_property = UNSET - _a_property = data - if not isinstance(_a_property, Unset): - a_property = AnEnum(_a_property) - - return a_property + a_property_type0: Union[Unset, AnEnum] + if not isinstance(data, str): + raise TypeError() + a_property_type0 = UNSET + _a_property_type0 = data + if not isinstance(_a_property_type0, Unset): + a_property_type0 = AnEnum(_a_property_type0) + + return a_property_type0 except: # noqa: E722 pass - a_property = UNSET - _a_property = data - if not isinstance(_a_property, Unset): - a_property = AnIntEnum(_a_property) - - return a_property + if not isinstance(data, int): + raise TypeError() + a_property_type1: Union[Unset, AnIntEnum] + a_property_type1 = UNSET + _a_property_type1 = data + if not isinstance(_a_property_type1, Unset): + a_property_type1 = AnIntEnum(_a_property_type1) + + return a_property_type1 a_property = _parse_a_property(d.pop("a_property", UNSET)) 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 index 024d1da73..cc6484d5f 100644 --- 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 @@ -13,22 +13,22 @@ class AModelNullableModel: """ """ - a_property: Union[Unset, AnEnum, AnIntEnum] = UNSET + a_property: Union[AnEnum, AnIntEnum, Unset] = 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] + a_property: Union[Unset, int, str] 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 + a_property = self.a_property.value else: a_property = UNSET if not isinstance(self.a_property, Unset): - a_property = self.a_property + a_property = self.a_property.value field_dict: Dict[str, Any] = {} field_dict.update(self.additional_properties) @@ -42,24 +42,30 @@ def to_dict(self) -> Dict[str, Any]: def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: 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] + def _parse_a_property(data: object) -> Union[AnEnum, AnIntEnum, Unset]: + if isinstance(data, Unset): + return data try: - a_property = UNSET - _a_property = data - if not isinstance(_a_property, Unset): - a_property = AnEnum(_a_property) - - return a_property + a_property_type0: Union[Unset, AnEnum] + if not isinstance(data, str): + raise TypeError() + a_property_type0 = UNSET + _a_property_type0 = data + if not isinstance(_a_property_type0, Unset): + a_property_type0 = AnEnum(_a_property_type0) + + return a_property_type0 except: # noqa: E722 pass - a_property = UNSET - _a_property = data - if not isinstance(_a_property, Unset): - a_property = AnIntEnum(_a_property) - - return a_property + if not isinstance(data, int): + raise TypeError() + a_property_type1: Union[Unset, AnIntEnum] + a_property_type1 = UNSET + _a_property_type1 = data + if not isinstance(_a_property_type1, Unset): + a_property_type1 = AnIntEnum(_a_property_type1) + + return a_property_type1 a_property = _parse_a_property(d.pop("a_property", UNSET)) 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 index 6cf422c40..0a7a54bd6 100644 --- 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 @@ -13,22 +13,22 @@ class AModelModel: """ """ - a_property: Union[Unset, AnEnum, AnIntEnum] = UNSET + a_property: Union[AnEnum, AnIntEnum, Unset] = 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] + a_property: Union[Unset, int, str] 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 + a_property = self.a_property.value else: a_property = UNSET if not isinstance(self.a_property, Unset): - a_property = self.a_property + a_property = self.a_property.value field_dict: Dict[str, Any] = {} field_dict.update(self.additional_properties) @@ -42,24 +42,30 @@ def to_dict(self) -> Dict[str, Any]: def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: 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] + def _parse_a_property(data: object) -> Union[AnEnum, AnIntEnum, Unset]: + if isinstance(data, Unset): + return data try: - a_property = UNSET - _a_property = data - if not isinstance(_a_property, Unset): - a_property = AnEnum(_a_property) - - return a_property + a_property_type0: Union[Unset, AnEnum] + if not isinstance(data, str): + raise TypeError() + a_property_type0 = UNSET + _a_property_type0 = data + if not isinstance(_a_property_type0, Unset): + a_property_type0 = AnEnum(_a_property_type0) + + return a_property_type0 except: # noqa: E722 pass - a_property = UNSET - _a_property = data - if not isinstance(_a_property, Unset): - a_property = AnIntEnum(_a_property) - - return a_property + if not isinstance(data, int): + raise TypeError() + a_property_type1: Union[Unset, AnIntEnum] + a_property_type1 = UNSET + _a_property_type1 = data + if not isinstance(_a_property_type1, Unset): + a_property_type1 = AnIntEnum(_a_property_type1) + + return a_property_type1 a_property = _parse_a_property(d.pop("a_property", UNSET)) 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 index 2dccd3bd3..fd568db52 100644 --- 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 @@ -13,22 +13,22 @@ class AModelNotRequiredModel: """ """ - a_property: Union[Unset, AnEnum, AnIntEnum] = UNSET + a_property: Union[AnEnum, AnIntEnum, Unset] = 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] + a_property: Union[Unset, int, str] 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 + a_property = self.a_property.value else: a_property = UNSET if not isinstance(self.a_property, Unset): - a_property = self.a_property + a_property = self.a_property.value field_dict: Dict[str, Any] = {} field_dict.update(self.additional_properties) @@ -42,24 +42,30 @@ def to_dict(self) -> Dict[str, Any]: def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: 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] + def _parse_a_property(data: object) -> Union[AnEnum, AnIntEnum, Unset]: + if isinstance(data, Unset): + return data try: - a_property = UNSET - _a_property = data - if not isinstance(_a_property, Unset): - a_property = AnEnum(_a_property) - - return a_property + a_property_type0: Union[Unset, AnEnum] + if not isinstance(data, str): + raise TypeError() + a_property_type0 = UNSET + _a_property_type0 = data + if not isinstance(_a_property_type0, Unset): + a_property_type0 = AnEnum(_a_property_type0) + + return a_property_type0 except: # noqa: E722 pass - a_property = UNSET - _a_property = data - if not isinstance(_a_property, Unset): - a_property = AnIntEnum(_a_property) - - return a_property + if not isinstance(data, int): + raise TypeError() + a_property_type1: Union[Unset, AnIntEnum] + a_property_type1 = UNSET + _a_property_type1 = data + if not isinstance(_a_property_type1, Unset): + a_property_type1 = AnIntEnum(_a_property_type1) + + return a_property_type1 a_property = _parse_a_property(d.pop("a_property", UNSET)) 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 index f048e8ebb..6413e7f9a 100644 --- 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 @@ -13,22 +13,22 @@ class AModelNotRequiredNullableModel: """ """ - a_property: Union[Unset, AnEnum, AnIntEnum] = UNSET + a_property: Union[AnEnum, AnIntEnum, Unset] = 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] + a_property: Union[Unset, int, str] 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 + a_property = self.a_property.value else: a_property = UNSET if not isinstance(self.a_property, Unset): - a_property = self.a_property + a_property = self.a_property.value field_dict: Dict[str, Any] = {} field_dict.update(self.additional_properties) @@ -42,24 +42,30 @@ def to_dict(self) -> Dict[str, Any]: def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: 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] + def _parse_a_property(data: object) -> Union[AnEnum, AnIntEnum, Unset]: + if isinstance(data, Unset): + return data try: - a_property = UNSET - _a_property = data - if not isinstance(_a_property, Unset): - a_property = AnEnum(_a_property) - - return a_property + a_property_type0: Union[Unset, AnEnum] + if not isinstance(data, str): + raise TypeError() + a_property_type0 = UNSET + _a_property_type0 = data + if not isinstance(_a_property_type0, Unset): + a_property_type0 = AnEnum(_a_property_type0) + + return a_property_type0 except: # noqa: E722 pass - a_property = UNSET - _a_property = data - if not isinstance(_a_property, Unset): - a_property = AnIntEnum(_a_property) - - return a_property + if not isinstance(data, int): + raise TypeError() + a_property_type1: Union[Unset, AnIntEnum] + a_property_type1 = UNSET + _a_property_type1 = data + if not isinstance(_a_property_type1, Unset): + a_property_type1 = AnIntEnum(_a_property_type1) + + return a_property_type1 a_property = _parse_a_property(d.pop("a_property", UNSET)) 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 index 024d1da73..cc6484d5f 100644 --- 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 @@ -13,22 +13,22 @@ class AModelNullableModel: """ """ - a_property: Union[Unset, AnEnum, AnIntEnum] = UNSET + a_property: Union[AnEnum, AnIntEnum, Unset] = 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] + a_property: Union[Unset, int, str] 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 + a_property = self.a_property.value else: a_property = UNSET if not isinstance(self.a_property, Unset): - a_property = self.a_property + a_property = self.a_property.value field_dict: Dict[str, Any] = {} field_dict.update(self.additional_properties) @@ -42,24 +42,30 @@ def to_dict(self) -> Dict[str, Any]: def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: 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] + def _parse_a_property(data: object) -> Union[AnEnum, AnIntEnum, Unset]: + if isinstance(data, Unset): + return data try: - a_property = UNSET - _a_property = data - if not isinstance(_a_property, Unset): - a_property = AnEnum(_a_property) - - return a_property + a_property_type0: Union[Unset, AnEnum] + if not isinstance(data, str): + raise TypeError() + a_property_type0 = UNSET + _a_property_type0 = data + if not isinstance(_a_property_type0, Unset): + a_property_type0 = AnEnum(_a_property_type0) + + return a_property_type0 except: # noqa: E722 pass - a_property = UNSET - _a_property = data - if not isinstance(_a_property, Unset): - a_property = AnIntEnum(_a_property) - - return a_property + if not isinstance(data, int): + raise TypeError() + a_property_type1: Union[Unset, AnIntEnum] + a_property_type1 = UNSET + _a_property_type1 = data + if not isinstance(_a_property_type1, Unset): + a_property_type1 = AnIntEnum(_a_property_type1) + + return a_property_type1 a_property = _parse_a_property(d.pop("a_property", UNSET))