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 c71152ef6..51d0dd02c 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 @@ -4,10 +4,13 @@ from .a_model import AModel from .a_model_with_properties_reference_that_are_not_object import AModelWithPropertiesReferenceThatAreNotObject from .all_of_sub_model import AllOfSubModel +from .all_of_sub_model_type_enum import AllOfSubModelTypeEnum from .an_all_of_enum import AnAllOfEnum from .an_enum import AnEnum from .an_int_enum import AnIntEnum from .another_all_of_sub_model import AnotherAllOfSubModel +from .another_all_of_sub_model_type import AnotherAllOfSubModelType +from .another_all_of_sub_model_type_enum import AnotherAllOfSubModelTypeEnum from .body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost from .body_upload_file_tests_upload_post_additional_property import BodyUploadFileTestsUploadPostAdditionalProperty from .body_upload_file_tests_upload_post_some_nullable_object import BodyUploadFileTestsUploadPostSomeNullableObject 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 index 8945c70ab..515374d19 100644 --- 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 @@ -2,6 +2,7 @@ import attr +from ..models.all_of_sub_model_type_enum import AllOfSubModelTypeEnum from ..types import UNSET, Unset T = TypeVar("T", bound="AllOfSubModel") @@ -12,16 +13,26 @@ class AllOfSubModel: """ """ a_sub_property: Union[Unset, str] = UNSET + type: Union[Unset, str] = UNSET + type_enum: Union[Unset, AllOfSubModelTypeEnum] = 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 + type = self.type + type_enum: Union[Unset, int] = UNSET + if not isinstance(self.type_enum, Unset): + type_enum = self.type_enum.value 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 + if type is not UNSET: + field_dict["type"] = type + if type_enum is not UNSET: + field_dict["type_enum"] = type_enum return field_dict @@ -30,8 +41,19 @@ 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) + type = d.pop("type", UNSET) + + _type_enum = d.pop("type_enum", UNSET) + type_enum: Union[Unset, AllOfSubModelTypeEnum] + if isinstance(_type_enum, Unset): + type_enum = UNSET + else: + type_enum = AllOfSubModelTypeEnum(_type_enum) + all_of_sub_model = cls( a_sub_property=a_sub_property, + type=type, + type_enum=type_enum, ) all_of_sub_model.additional_properties = d diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/all_of_sub_model_type_enum.py b/end_to_end_tests/golden-record/my_test_api_client/models/all_of_sub_model_type_enum.py new file mode 100644 index 000000000..817e0eb7c --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/all_of_sub_model_type_enum.py @@ -0,0 +1,9 @@ +from enum import IntEnum + + +class AllOfSubModelTypeEnum(IntEnum): + VALUE_0 = 0 + VALUE_1 = 1 + + def __str__(self) -> str: + return str(self.value) 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 index 2ecc464a8..5fabb03e4 100644 --- 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 @@ -2,6 +2,8 @@ import attr +from ..models.another_all_of_sub_model_type import AnotherAllOfSubModelType +from ..models.another_all_of_sub_model_type_enum import AnotherAllOfSubModelTypeEnum from ..types import UNSET, Unset T = TypeVar("T", bound="AnotherAllOfSubModel") @@ -12,16 +14,29 @@ class AnotherAllOfSubModel: """ """ another_sub_property: Union[Unset, str] = UNSET + type: Union[Unset, AnotherAllOfSubModelType] = UNSET + type_enum: Union[Unset, AnotherAllOfSubModelTypeEnum] = 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 + type: Union[Unset, str] = UNSET + if not isinstance(self.type, Unset): + type = self.type.value + + type_enum: Union[Unset, int] = UNSET + if not isinstance(self.type_enum, Unset): + type_enum = self.type_enum.value 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 type is not UNSET: + field_dict["type"] = type + if type_enum is not UNSET: + field_dict["type_enum"] = type_enum return field_dict @@ -30,8 +45,24 @@ 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) + _type = d.pop("type", UNSET) + type: Union[Unset, AnotherAllOfSubModelType] + if isinstance(_type, Unset): + type = UNSET + else: + type = AnotherAllOfSubModelType(_type) + + _type_enum = d.pop("type_enum", UNSET) + type_enum: Union[Unset, AnotherAllOfSubModelTypeEnum] + if isinstance(_type_enum, Unset): + type_enum = UNSET + else: + type_enum = AnotherAllOfSubModelTypeEnum(_type_enum) + another_all_of_sub_model = cls( another_sub_property=another_sub_property, + type=type, + type_enum=type_enum, ) another_all_of_sub_model.additional_properties = d diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/another_all_of_sub_model_type.py b/end_to_end_tests/golden-record/my_test_api_client/models/another_all_of_sub_model_type.py new file mode 100644 index 000000000..b2e82aa7c --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/another_all_of_sub_model_type.py @@ -0,0 +1,8 @@ +from enum import Enum + + +class AnotherAllOfSubModelType(str, Enum): + SUBMODEL = "submodel" + + def __str__(self) -> str: + return str(self.value) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/another_all_of_sub_model_type_enum.py b/end_to_end_tests/golden-record/my_test_api_client/models/another_all_of_sub_model_type_enum.py new file mode 100644 index 000000000..d54ed9dde --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/another_all_of_sub_model_type_enum.py @@ -0,0 +1,8 @@ +from enum import IntEnum + + +class AnotherAllOfSubModelTypeEnum(IntEnum): + VALUE_0 = 0 + + def __str__(self) -> str: + return str(self.value) 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 60406f46d..415f27486 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 @@ -2,6 +2,8 @@ import attr +from ..models.another_all_of_sub_model_type import AnotherAllOfSubModelType +from ..models.another_all_of_sub_model_type_enum import AnotherAllOfSubModelTypeEnum from ..types import UNSET, Unset T = TypeVar("T", bound="ModelFromAllOf") @@ -12,11 +14,21 @@ class ModelFromAllOf: """ """ a_sub_property: Union[Unset, str] = UNSET + type: Union[Unset, AnotherAllOfSubModelType] = UNSET + type_enum: Union[Unset, AnotherAllOfSubModelTypeEnum] = 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]: a_sub_property = self.a_sub_property + type: Union[Unset, str] = UNSET + if not isinstance(self.type, Unset): + type = self.type.value + + type_enum: Union[Unset, int] = UNSET + if not isinstance(self.type_enum, Unset): + type_enum = self.type_enum.value + another_sub_property = self.another_sub_property field_dict: Dict[str, Any] = {} @@ -24,6 +36,10 @@ def to_dict(self) -> Dict[str, Any]: field_dict.update({}) if a_sub_property is not UNSET: field_dict["a_sub_property"] = a_sub_property + if type is not UNSET: + field_dict["type"] = type + if type_enum is not UNSET: + field_dict["type_enum"] = type_enum if another_sub_property is not UNSET: field_dict["another_sub_property"] = another_sub_property @@ -34,10 +50,26 @@ 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) + _type = d.pop("type", UNSET) + type: Union[Unset, AnotherAllOfSubModelType] + if isinstance(_type, Unset): + type = UNSET + else: + type = AnotherAllOfSubModelType(_type) + + _type_enum = d.pop("type_enum", UNSET) + type_enum: Union[Unset, AnotherAllOfSubModelTypeEnum] + if isinstance(_type_enum, Unset): + type_enum = UNSET + else: + type_enum = AnotherAllOfSubModelTypeEnum(_type_enum) + another_sub_property = d.pop("another_sub_property", UNSET) model_from_all_of = cls( a_sub_property=a_sub_property, + type=type, + type_enum=type_enum, another_sub_property=another_sub_property, ) diff --git a/end_to_end_tests/openapi.json b/end_to_end_tests/openapi.json index 4d40f108e..2bdd49c12 100644 --- a/end_to_end_tests/openapi.json +++ b/end_to_end_tests/openapi.json @@ -1315,6 +1315,13 @@ "properties": { "a_sub_property": { "type": "string" + }, + "type": { + "type": "string" + }, + "type_enum": { + "type": "int", + "enum": [0, 1] } } }, @@ -1324,6 +1331,14 @@ "properties": { "another_sub_property": { "type": "string" + }, + "type": { + "type": "string", + "enum": ["submodel"] + }, + "type_enum": { + "type": "int", + "enum": [0] } } }, diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index 79ac48764..239a90427 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -1,5 +1,5 @@ from itertools import chain -from typing import ClassVar, Dict, List, NamedTuple, Optional, Set, Tuple, Union +from typing import ClassVar, Dict, List, NamedTuple, Optional, Set, Tuple, Union, cast import attr @@ -7,6 +7,7 @@ from ... import schema as oai from ... import utils from ..errors import ParseError, PropertyError +from .enum_property import EnumProperty from .property import Property from .schemas import Class, Schemas, parse_reference_path @@ -49,16 +50,56 @@ def get_imports(self, *, prefix: str) -> Set[str]: return imports +def _is_string_enum(prop: Property) -> bool: + return isinstance(prop, EnumProperty) and prop.value_type == str + + +def _is_int_enum(prop: Property) -> bool: + return isinstance(prop, EnumProperty) and prop.value_type == int + + +def values_are_subset(first: EnumProperty, second: EnumProperty) -> bool: + return set(first.values.items()) <= set(second.values.items()) + + +def _is_subtype(first: Property, second: Property) -> bool: + from . import IntProperty, StringProperty + + return any( + [ + _is_string_enum(first) and isinstance(second, StringProperty), + _is_int_enum(first) and isinstance(second, IntProperty), + _is_string_enum(first) and _is_string_enum(second) + # cast because MyPy fails to deduce type + and values_are_subset(cast(EnumProperty, first), cast(EnumProperty, second)), + _is_int_enum(first) and _is_int_enum(second) + # cast because MyPy fails to deduce type + and values_are_subset(cast(EnumProperty, first), cast(EnumProperty, second)), + ] + ) + + 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 + + if _is_subtype(first, second): + first = attr.evolve(first, nullable=nullable, required=required) + return first + elif _is_subtype(second, first): + second = attr.evolve(second, nullable=nullable, required=required) + return second + elif first.__class__ == second.__class__: + 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 + else: + return PropertyError( + header="Cannot merge properties", + detail=f"{first.__class__}, {second.__class__}Properties have incompatible types", + ) class _PropertyData(NamedTuple): diff --git a/tests/test_parser/test_properties/test_model_property.py b/tests/test_parser/test_properties/test_model_property.py index abe0e5323..09baef9ce 100644 --- a/tests/test_parser/test_properties/test_model_property.py +++ b/tests/test_parser/test_properties/test_model_property.py @@ -6,7 +6,15 @@ import openapi_python_client.schema as oai from openapi_python_client import Config from openapi_python_client.parser.errors import PropertyError -from openapi_python_client.parser.properties import DateTimeProperty, ModelProperty, StringProperty +from openapi_python_client.parser.properties import ( + Class, + DateTimeProperty, + EnumProperty, + IntProperty, + ModelProperty, + StringProperty, + enum_property, +) def get_class(): @@ -275,6 +283,156 @@ def test_conflicting_properties_same_types(self, model_property_factory): assert isinstance(result, PropertyError) + def test_allof_string_and_string_enum(self, model_property_factory): + 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")] + ) + enum_property = EnumProperty( + name="", + required=True, + nullable=True, + values={"foo": "foo"}, + class_info=Class(name="AnEnum", module_name="an_enum"), + value_type=str, + default=None, + ) + schemas = Schemas( + classes_by_reference={ + "/First": model_property_factory(optional_properties=[string_property()]), + "/Second": model_property_factory(optional_properties=[enum_property]), + } + ) + + result = _process_properties(data=data, schemas=schemas, class_name="", config=Config()) + assert result.optional_props[0] == enum_property + + def test_allof_string_enum_and_string(self, model_property_factory): + 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")] + ) + enum_property = EnumProperty( + name="", + required=True, + nullable=True, + values={"foo": "foo"}, + class_info=Class(name="AnEnum", module_name="an_enum"), + value_type=str, + default=None, + ) + schemas = Schemas( + classes_by_reference={ + "/First": model_property_factory(optional_properties=[enum_property]), + "/Second": model_property_factory(optional_properties=[string_property()]), + } + ) + + result = _process_properties(data=data, schemas=schemas, class_name="", config=Config()) + assert result.optional_props[0] == enum_property + + def test_allof_int_and_int_enum(self, model_property_factory): + 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")] + ) + enum_property = EnumProperty( + name="", + required=True, + nullable=True, + values={"foo": 1}, + class_info=Class(name="AnEnum", module_name="an_enum"), + value_type=int, + default=None, + ) + schemas = Schemas( + classes_by_reference={ + "/First": model_property_factory( + optional_properties=[IntProperty(name="", required=True, nullable=True, default=None)] + ), + "/Second": model_property_factory(optional_properties=[enum_property]), + } + ) + + result = _process_properties(data=data, schemas=schemas, class_name="", config=Config()) + assert result.optional_props[0] == enum_property + + def test_allof_string_enums(self, model_property_factory): + 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")] + ) + enum_property1 = EnumProperty( + name="", + required=True, + nullable=True, + values={"foo": "foo", "bar": "bar"}, + class_info=Class(name="AnEnum1", module_name="an_enum1"), + value_type=str, + default=None, + ) + enum_property2 = EnumProperty( + name="", + required=True, + nullable=True, + values={"foo": "foo"}, + class_info=Class(name="AnEnum2", module_name="an_enum2"), + value_type=str, + default=None, + ) + schemas = Schemas( + classes_by_reference={ + "/First": model_property_factory(optional_properties=[enum_property1]), + "/Second": model_property_factory(optional_properties=[enum_property2]), + } + ) + + result = _process_properties(data=data, schemas=schemas, class_name="", config=Config()) + assert result.optional_props[0] == enum_property2 + + def test_allof_int_enums(self, model_property_factory): + 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")] + ) + enum_property1 = EnumProperty( + name="", + required=True, + nullable=True, + values={"foo": 1, "bar": 2}, + class_info=Class(name="AnEnum1", module_name="an_enum1"), + value_type=int, + default=None, + ) + enum_property2 = EnumProperty( + name="", + required=True, + nullable=True, + values={"foo": 1}, + class_info=Class(name="AnEnum2", module_name="an_enum2"), + value_type=int, + default=None, + ) + schemas = Schemas( + classes_by_reference={ + "/First": model_property_factory(optional_properties=[enum_property1]), + "/Second": model_property_factory(optional_properties=[enum_property2]), + } + ) + + result = _process_properties(data=data, schemas=schemas, class_name="", config=Config()) + assert result.optional_props[0] == enum_property2 + def test_duplicate_properties(self, model_property_factory): from openapi_python_client.parser.properties import Schemas from openapi_python_client.parser.properties.model_property import _process_properties