From 36a8fded5deacd78e5d6022b61ebe33c5cb11f79 Mon Sep 17 00:00:00 2001 From: Forest Tong Date: Mon, 10 May 2021 17:50:27 -0400 Subject: [PATCH 01/13] Allof enum and string or int --- .../my_test_api_client/models/__init__.py | 1 + .../models/all_of_sub_model.py | 7 ++++ .../models/another_all_of_sub_model.py | 13 ++++++++ .../models/another_all_of_sub_model_type.py | 8 +++++ .../models/model_from_all_of.py | 14 ++++++++ end_to_end_tests/openapi.json | 7 ++++ .../parser/properties/model_property.py | 32 +++++++++++++++---- 7 files changed, 75 insertions(+), 7 deletions(-) create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/another_all_of_sub_model_type.py 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..c28362723 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 @@ -8,6 +8,7 @@ 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 .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..d08c10e7a 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 @@ -12,16 +12,20 @@ class AllOfSubModel: """ """ a_sub_property: Union[Unset, str] = UNSET + type: 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 = self.type 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 return field_dict @@ -30,8 +34,11 @@ 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) + all_of_sub_model = cls( a_sub_property=a_sub_property, + type=type, ) 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.py b/end_to_end_tests/golden-record/my_test_api_client/models/another_all_of_sub_model.py index 2ecc464a8..8bea3bebc 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,7 @@ import attr +from ..models.another_all_of_sub_model_type import AnotherAllOfSubModelType from ..types import UNSET, Unset T = TypeVar("T", bound="AnotherAllOfSubModel") @@ -12,16 +13,22 @@ class AnotherAllOfSubModel: """ """ another_sub_property: Union[Unset, str] = UNSET + type: Union[Unset, AnotherAllOfSubModelType] = 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 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 return field_dict @@ -30,8 +37,14 @@ 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: Union[Unset, AnotherAllOfSubModelType] = UNSET + _type = d.pop("type", UNSET) + if not isinstance(_type, Unset): + type = AnotherAllOfSubModelType(_type) + another_all_of_sub_model = cls( another_sub_property=another_sub_property, + type=type, ) 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/model_from_all_of.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_from_all_of.py index 60406f46d..2ae83b25b 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,7 @@ import attr +from ..models.another_all_of_sub_model_type import AnotherAllOfSubModelType from ..types import UNSET, Unset T = TypeVar("T", bound="ModelFromAllOf") @@ -12,11 +13,16 @@ class ModelFromAllOf: """ """ a_sub_property: Union[Unset, str] = UNSET + type: Union[Unset, AnotherAllOfSubModelType] = 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 + another_sub_property = self.another_sub_property field_dict: Dict[str, Any] = {} @@ -24,6 +30,8 @@ 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 another_sub_property is not UNSET: field_dict["another_sub_property"] = another_sub_property @@ -34,10 +42,16 @@ 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: Union[Unset, AnotherAllOfSubModelType] = UNSET + _type = d.pop("type", UNSET) + if not isinstance(_type, Unset): + type = AnotherAllOfSubModelType(_type) + another_sub_property = d.pop("another_sub_property", UNSET) model_from_all_of = cls( a_sub_property=a_sub_property, + type=type, another_sub_property=another_sub_property, ) diff --git a/end_to_end_tests/openapi.json b/end_to_end_tests/openapi.json index 5e8c2cbe8..7ad879092 100644 --- a/end_to_end_tests/openapi.json +++ b/end_to_end_tests/openapi.json @@ -1382,6 +1382,9 @@ "properties": { "a_sub_property": { "type": "string" + }, + "type": { + "type": "string" } } }, @@ -1391,6 +1394,10 @@ "properties": { "another_sub_property": { "type": "string" + }, + "type": { + "type": "string", + "enum": ["submodel"] } } }, diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index 55c2b3cf1..df0f47bd1 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -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 @@ -50,15 +51,32 @@ def get_imports(self, *, prefix: str) -> Set[str]: 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 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 + elif first.__class__.__name__ == "StringProperty" and second.__class__ == EnumProperty and second.value_type == str: + second = attr.evolve(second, nullable=nullable, required=required) + return second + elif second.__class__.__name__ == "StringProperty" and first.__class__ == EnumProperty and first.value_type == str: + first = attr.evolve(first, nullable=nullable, required=required) + return first + elif first.__class__.__name__ == "IntProperty" and second.__class__ == EnumProperty and second.value_type == int: + second = attr.evolve(second, nullable=nullable, required=required) + return second + elif second.__class__.__name__ == "IntProperty" and first.__class__ == EnumProperty and first.value_type == int: + first = attr.evolve(first, nullable=nullable, required=required) + return first + else: + return PropertyError( + header="Cannot merge properties", + detail=f"{first.__class__}, {second.__class__}Properties have incompatible types", + ) class _PropertyData(NamedTuple): From c0805870f209bb6d562e49ca511b42f34e406f36 Mon Sep 17 00:00:00 2001 From: Forest Tong Date: Mon, 10 May 2021 17:57:28 -0400 Subject: [PATCH 02/13] Combine statements --- .../parser/properties/model_property.py | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index df0f47bd1..d2fc04ece 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -7,7 +7,6 @@ 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 @@ -60,16 +59,24 @@ def _merge_properties(first: Property, second: Property) -> Union[Property, Prop if first != second: return PropertyError(header="Cannot merge properties", detail="Properties has conflicting values") return first - elif first.__class__.__name__ == "StringProperty" and second.__class__ == EnumProperty and second.value_type == str: + elif ( + first.__class__.__name__ == "StringProperty" + and second.__class__.__name__ == "EnumProperty" + and second.value_type == str + or first.__class__.__name__ == "IntProperty" + and second.__class__.__name__ == "EnumProperty" + and second.value_type == int + ): second = attr.evolve(second, nullable=nullable, required=required) return second - elif second.__class__.__name__ == "StringProperty" and first.__class__ == EnumProperty and first.value_type == str: - first = attr.evolve(first, nullable=nullable, required=required) - return first - elif first.__class__.__name__ == "IntProperty" and second.__class__ == EnumProperty and second.value_type == int: - second = attr.evolve(second, nullable=nullable, required=required) - return second - elif second.__class__.__name__ == "IntProperty" and first.__class__ == EnumProperty and first.value_type == int: + elif ( + second.__class__.__name__ == "StringProperty" + and first.__class__.__name__ == "EnumProperty" + and first.value_type == str + or second.__class__.__name__ == "IntProperty" + and first.__class__.__name__ == "EnumProperty" + and first.value_type == int + ): first = attr.evolve(first, nullable=nullable, required=required) return first else: From 370cae75183615907cf86f27180aad71129017a3 Mon Sep 17 00:00:00 2001 From: Forest Tong Date: Mon, 10 May 2021 18:00:20 -0400 Subject: [PATCH 03/13] Another style --- .../parser/properties/model_property.py | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index d2fc04ece..21dcd98b9 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -49,6 +49,18 @@ def get_imports(self, *, prefix: str) -> Set[str]: return imports +def _is_subtype(first: Property, second: Property) -> bool: + return ( + first.__class__.__name__ == "EnumProperty" + and first.value_type == str + and second.__class__.__name__ == "StringProperty" + ) or ( + first.__class__.__name__ == "EnumProperty" + and first.value_type == int + and second.__class__.__name__ == "IntProperty" + ) + + def _merge_properties(first: Property, second: Property) -> Union[Property, PropertyError]: nullable = first.nullable and second.nullable required = first.required or second.required @@ -59,26 +71,12 @@ def _merge_properties(first: Property, second: Property) -> Union[Property, Prop if first != second: return PropertyError(header="Cannot merge properties", detail="Properties has conflicting values") return first - elif ( - first.__class__.__name__ == "StringProperty" - and second.__class__.__name__ == "EnumProperty" - and second.value_type == str - or first.__class__.__name__ == "IntProperty" - and second.__class__.__name__ == "EnumProperty" - and second.value_type == int - ): - second = attr.evolve(second, nullable=nullable, required=required) - return second - elif ( - second.__class__.__name__ == "StringProperty" - and first.__class__.__name__ == "EnumProperty" - and first.value_type == str - or second.__class__.__name__ == "IntProperty" - and first.__class__.__name__ == "EnumProperty" - and first.value_type == int - ): + elif _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 else: return PropertyError( header="Cannot merge properties", From a4da06c5d350b4d5d3b6d4edae8ddc28442aabee Mon Sep 17 00:00:00 2001 From: Forest Tong Date: Mon, 10 May 2021 18:08:17 -0400 Subject: [PATCH 04/13] Support enums --- .../my_test_api_client/models/__init__.py | 2 + .../models/all_of_sub_model.py | 13 +++++++ .../models/all_of_sub_model_type_enum.py | 9 +++++ .../models/another_all_of_sub_model.py | 14 +++++++ .../another_all_of_sub_model_type_enum.py | 8 ++++ .../models/model_from_all_of.py | 14 +++++++ end_to_end_tests/openapi.json | 8 ++++ .../parser/properties/model_property.py | 39 ++++++++++++------- 8 files changed, 92 insertions(+), 15 deletions(-) create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/all_of_sub_model_type_enum.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/another_all_of_sub_model_type_enum.py 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 c28362723..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,11 +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 d08c10e7a..2095f0d40 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") @@ -13,11 +14,15 @@ 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) @@ -26,6 +31,8 @@ def to_dict(self) -> Dict[str, Any]: 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 @@ -36,9 +43,15 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: type = d.pop("type", UNSET) + type_enum: Union[Unset, AllOfSubModelTypeEnum] = UNSET + _type_enum = d.pop("type_enum", UNSET) + if not isinstance(_type_enum, Unset): + 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 8bea3bebc..82476e2b7 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 @@ -3,6 +3,7 @@ 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") @@ -14,6 +15,7 @@ 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]: @@ -22,6 +24,10 @@ def to_dict(self) -> Dict[str, Any]: 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({}) @@ -29,6 +35,8 @@ def to_dict(self) -> Dict[str, Any]: 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 @@ -42,9 +50,15 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: if not isinstance(_type, Unset): type = AnotherAllOfSubModelType(_type) + type_enum: Union[Unset, AnotherAllOfSubModelTypeEnum] = UNSET + _type_enum = d.pop("type_enum", UNSET) + if not isinstance(_type_enum, Unset): + 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_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 2ae83b25b..0cca1941b 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 @@ -3,6 +3,7 @@ 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") @@ -14,6 +15,7 @@ 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) @@ -23,6 +25,10 @@ def to_dict(self) -> Dict[str, Any]: 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] = {} @@ -32,6 +38,8 @@ def to_dict(self) -> Dict[str, Any]: 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 @@ -47,11 +55,17 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: if not isinstance(_type, Unset): type = AnotherAllOfSubModelType(_type) + type_enum: Union[Unset, AnotherAllOfSubModelTypeEnum] = UNSET + _type_enum = d.pop("type_enum", UNSET) + if not isinstance(_type_enum, Unset): + 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 7ad879092..ab92c345a 100644 --- a/end_to_end_tests/openapi.json +++ b/end_to_end_tests/openapi.json @@ -1385,6 +1385,10 @@ }, "type": { "type": "string" + }, + "type_enum": { + "type": "int", + "enum": [0, 1] } } }, @@ -1398,6 +1402,10 @@ "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 21dcd98b9..1fd18098a 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -49,15 +49,24 @@ def get_imports(self, *, prefix: str) -> Set[str]: return imports +def _is_string_enum(prop: Property) -> bool: + return prop.__class__.__name__ == "EnumProperty" and prop.value_type == str + + +def _is_int_enum(prop: Property) -> bool: + return prop.__class__.__name__ == "EnumProperty" and prop.value_type == int + + def _is_subtype(first: Property, second: Property) -> bool: - return ( - first.__class__.__name__ == "EnumProperty" - and first.value_type == str - and second.__class__.__name__ == "StringProperty" - ) or ( - first.__class__.__name__ == "EnumProperty" - and first.value_type == int - and second.__class__.__name__ == "IntProperty" + return any( + [ + _is_string_enum(first) and second.__class__.__name__ == "StringProperty", + _is_int_enum(first) and second.__class__.__name__ == "IntProperty", + _is_string_enum(first) + and _is_string_enum(second) + and set(first.values.items()) <= set(second.values.items()), + _is_int_enum(first) and _is_int_enum(second) and set(first.values.items()) <= set(second.values.items()), + ] ) @@ -65,18 +74,18 @@ def _merge_properties(first: Property, second: Property) -> Union[Property, Prop nullable = first.nullable and second.nullable required = first.required or second.required - if 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 - elif _is_subtype(first, second): + 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", From e4e235386cff952195ad202ac5a1ad8311c21442 Mon Sep 17 00:00:00 2001 From: Forest Tong Date: Mon, 10 May 2021 18:13:36 -0400 Subject: [PATCH 05/13] Style --- .../parser/properties/model_property.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index 1fd18098a..592331ff9 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -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 @@ -50,18 +51,20 @@ def get_imports(self, *, prefix: str) -> Set[str]: def _is_string_enum(prop: Property) -> bool: - return prop.__class__.__name__ == "EnumProperty" and prop.value_type == str + return isinstance(prop, EnumProperty) and prop.value_type == str def _is_int_enum(prop: Property) -> bool: - return prop.__class__.__name__ == "EnumProperty" and prop.value_type == int + return isinstance(prop, EnumProperty) and prop.value_type == int def _is_subtype(first: Property, second: Property) -> bool: + from . import IntProperty, StringProperty + return any( [ - _is_string_enum(first) and second.__class__.__name__ == "StringProperty", - _is_int_enum(first) and second.__class__.__name__ == "IntProperty", + _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) and set(first.values.items()) <= set(second.values.items()), From 4e07c2f7c8ac2666737f1c6897cb101fe2fc100f Mon Sep 17 00:00:00 2001 From: Forest Tong Date: Mon, 28 Jun 2021 10:52:26 -0400 Subject: [PATCH 06/13] Fix bug --- openapi_python_client/parser/properties/model_property.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index 592331ff9..31cfde535 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -67,8 +67,10 @@ def _is_subtype(first: Property, second: Property) -> bool: _is_int_enum(first) and isinstance(second, IntProperty), _is_string_enum(first) and _is_string_enum(second) - and set(first.values.items()) <= set(second.values.items()), - _is_int_enum(first) and _is_int_enum(second) and set(first.values.items()) <= set(second.values.items()), + and set(e.value for e in first) <= set(e.value for e in second), + _is_int_enum(first) + and _is_int_enum(second) + and set(e.value for e in first) <= set(e.value for e in second), ] ) From 9da37c6e43bbde4ae7bf71bc54a9d6204da8ad5e Mon Sep 17 00:00:00 2001 From: Forest Tong Date: Mon, 28 Jun 2021 10:54:23 -0400 Subject: [PATCH 07/13] Revert "Fix bug" This reverts commit 7561c01ab10540ba22cfd036ec763cebf7b05ba6. --- openapi_python_client/parser/properties/model_property.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index 31cfde535..592331ff9 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -67,10 +67,8 @@ def _is_subtype(first: Property, second: Property) -> bool: _is_int_enum(first) and isinstance(second, IntProperty), _is_string_enum(first) and _is_string_enum(second) - and set(e.value for e in first) <= set(e.value for e in second), - _is_int_enum(first) - and _is_int_enum(second) - and set(e.value for e in first) <= set(e.value for e in second), + and set(first.values.items()) <= set(second.values.items()), + _is_int_enum(first) and _is_int_enum(second) and set(first.values.items()) <= set(second.values.items()), ] ) From 0b6f8e97e815b1aa71c086d9541d72bd8038ad52 Mon Sep 17 00:00:00 2001 From: Forest Tong Date: Mon, 28 Jun 2021 10:56:06 -0400 Subject: [PATCH 08/13] Fix bug --- openapi_python_client/parser/properties/model_property.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index 592331ff9..da545c9c4 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -58,6 +58,10 @@ 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 @@ -67,8 +71,8 @@ def _is_subtype(first: Property, second: Property) -> bool: _is_int_enum(first) and isinstance(second, IntProperty), _is_string_enum(first) and _is_string_enum(second) - and set(first.values.items()) <= set(second.values.items()), - _is_int_enum(first) and _is_int_enum(second) and set(first.values.items()) <= set(second.values.items()), + and values_are_subset(first, second), + _is_int_enum(first) and _is_int_enum(second) and values_are_subset(first, second), ] ) From 81e18ffc594cb637b41ba987390c5a371e789283 Mon Sep 17 00:00:00 2001 From: Forest Tong Date: Mon, 28 Jun 2021 10:58:01 -0400 Subject: [PATCH 09/13] Lint --- openapi_python_client/parser/properties/model_property.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index da545c9c4..db0866ec7 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -69,9 +69,7 @@ def _is_subtype(first: Property, second: Property) -> bool: [ _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) - and values_are_subset(first, second), + _is_string_enum(first) and _is_string_enum(second) and values_are_subset(first, second), _is_int_enum(first) and _is_int_enum(second) and values_are_subset(first, second), ] ) From d0ebdc5ece831fc8dd551afb3aeb5844135a97b1 Mon Sep 17 00:00:00 2001 From: Forest Tong Date: Mon, 28 Jun 2021 11:00:55 -0400 Subject: [PATCH 10/13] Fix bug --- .../parser/properties/model_property.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index db0866ec7..56879cbca 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 @@ -69,8 +69,12 @@ def _is_subtype(first: Property, second: Property) -> bool: [ _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) and values_are_subset(first, second), - _is_int_enum(first) and _is_int_enum(second) and values_are_subset(first, second), + _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)), ] ) From be5481ca6c03963123f5243a7b2f35703f87c49a Mon Sep 17 00:00:00 2001 From: Forest Tong Date: Mon, 28 Jun 2021 13:51:42 -0400 Subject: [PATCH 11/13] Regen --- .../my_test_api_client/models/all_of_sub_model.py | 6 ++++-- .../models/another_all_of_sub_model.py | 12 ++++++++---- .../my_test_api_client/models/model_from_all_of.py | 12 ++++++++---- 3 files changed, 20 insertions(+), 10 deletions(-) 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 2095f0d40..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 @@ -43,9 +43,11 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: type = d.pop("type", UNSET) - type_enum: Union[Unset, AllOfSubModelTypeEnum] = UNSET _type_enum = d.pop("type_enum", UNSET) - if not isinstance(_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( 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 82476e2b7..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 @@ -45,14 +45,18 @@ 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: Union[Unset, AnotherAllOfSubModelType] = UNSET _type = d.pop("type", UNSET) - if not isinstance(_type, Unset): + type: Union[Unset, AnotherAllOfSubModelType] + if isinstance(_type, Unset): + type = UNSET + else: type = AnotherAllOfSubModelType(_type) - type_enum: Union[Unset, AnotherAllOfSubModelTypeEnum] = UNSET _type_enum = d.pop("type_enum", UNSET) - if not isinstance(_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( 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 0cca1941b..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 @@ -50,14 +50,18 @@ 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: Union[Unset, AnotherAllOfSubModelType] = UNSET _type = d.pop("type", UNSET) - if not isinstance(_type, Unset): + type: Union[Unset, AnotherAllOfSubModelType] + if isinstance(_type, Unset): + type = UNSET + else: type = AnotherAllOfSubModelType(_type) - type_enum: Union[Unset, AnotherAllOfSubModelTypeEnum] = UNSET _type_enum = d.pop("type_enum", UNSET) - if not isinstance(_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) From a3623c2aa8b00569207fbb76016514ac406ccd89 Mon Sep 17 00:00:00 2001 From: Forest Tong Date: Tue, 29 Jun 2021 08:56:18 -0400 Subject: [PATCH 12/13] Add tests --- .../test_properties/test_model_property.py | 157 +++++++++++++++++- 1 file changed, 156 insertions(+), 1 deletion(-) diff --git a/tests/test_parser/test_properties/test_model_property.py b/tests/test_parser/test_properties/test_model_property.py index da57b3345..b251f6100 100644 --- a/tests/test_parser/test_properties/test_model_property.py +++ b/tests/test_parser/test_properties/test_model_property.py @@ -5,7 +5,12 @@ 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, + EnumProperty, + IntProperty, + StringProperty, +) @pytest.mark.parametrize( @@ -242,6 +247,156 @@ def test_conflicting_properties_same_types(self, model_property_factory, string_ 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, string_property_factory): from openapi_python_client.parser.properties import Schemas from openapi_python_client.parser.properties.model_property import _process_properties From 864e0f8776a89eeb81f9d3267af1c2bbcc1fe454 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sun, 1 Aug 2021 17:23:31 -0600 Subject: [PATCH 13/13] refactor: Improve performance of enum sub-type checking, add more test coverage. --- .../parser/properties/model_property.py | 106 ++++++------- pyproject.toml | 2 +- tests/conftest.py | 18 ++- .../test_properties/test_model_property.py | 142 ++++++++++-------- 4 files changed, 150 insertions(+), 118 deletions(-) diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index 56879cbca..aab642f0d 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, cast +from typing import ClassVar, Dict, List, NamedTuple, Optional, Set, Tuple, Union import attr @@ -50,56 +50,57 @@ 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: +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: +def _types_are_subset(first: EnumProperty, 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)), - ] - ) + if first.value_type == int and isinstance(second, IntProperty): + return True + if first.value_type == str and isinstance(second, StringProperty): + return True + return False + + +def _enum_subset(first: Property, second: Property) -> Optional[EnumProperty]: + """Return the EnumProperty that is the subset of the other, if possible.""" + + if isinstance(first, EnumProperty): + if isinstance(second, EnumProperty): + if _values_are_subset(first, second): + return first + if _values_are_subset(second, first): + return second + return None + return first if _types_are_subset(first, second) else None + if isinstance(second, EnumProperty) and _types_are_subset(second, first): + return second + return None def _merge_properties(first: Property, second: Property) -> Union[Property, PropertyError]: nullable = first.nullable and second.nullable required = first.required or second.required - 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__: + err = None + + if 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", - ) + if first == second: + return first + err = PropertyError(header="Cannot merge properties", detail="Properties has conflicting values") + + enum_subset = _enum_subset(first, second) + if enum_subset is not None: + return attr.evolve(enum_subset, nullable=nullable, required=required) + + return err or PropertyError( + header="Cannot merge properties", + detail=f"{first.__class__}, {second.__class__}Properties have incompatible types", + ) class _PropertyData(NamedTuple): @@ -118,16 +119,18 @@ def _process_properties( relative_imports: Set[str] = set() required_set = set(data.required or []) - def _check_existing(prop: Property) -> Union[Property, PropertyError]: + def _add_if_no_conflict(new_prop: Property) -> Optional[PropertyError]: nonlocal properties - existing = properties.get(prop.name) - 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 + existing = properties.get(new_prop.name) + merged_prop_or_error = _merge_properties(existing, new_prop) if existing else new_prop + if isinstance(merged_prop_or_error, PropertyError): + merged_prop_or_error.header = ( + f"Found conflicting properties named {new_prop.name} when creating {class_name}" + ) + return merged_prop_or_error + properties[merged_prop_or_error.name] = merged_prop_or_error + return None unprocessed_props = data.properties or {} for sub_prop in data.allOf or []: @@ -141,25 +144,24 @@ def _check_existing(prop: Property) -> Union[Property, PropertyError]: if not isinstance(sub_model, ModelProperty): return PropertyError("Cannot take allOf a non-object") 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 + err = _add_if_no_conflict(prop) + if err is not None: + return err else: unprocessed_props.update(sub_prop.properties or {}) required_set.update(sub_prop.required or []) for key, value in unprocessed_props.items(): prop_required = key in required_set + prop_or_error: Union[Property, PropertyError, None] prop_or_error, schemas = property_from_data( name=key, required=prop_required, data=value, schemas=schemas, parent_name=class_name, config=config ) if isinstance(prop_or_error, Property): - prop_or_error = _check_existing(prop_or_error) + prop_or_error = _add_if_no_conflict(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(): diff --git a/pyproject.toml b/pyproject.toml index 5d603920c..793a378ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ check = """ isort .\ && black .\ && flake8 openapi_python_client\ - && safety check --bare\ + && poetry export -f requirements.txt | poetry run safety check --bare --stdin\ && mypy openapi_python_client\ && pytest --cov openapi_python_client tests --cov-report=term-missing\ """ diff --git a/tests/conftest.py b/tests/conftest.py index bd1195f98..dfa885c23 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,6 +8,7 @@ DateTimeProperty, EnumProperty, FileProperty, + IntProperty, ListProperty, ModelProperty, Property, @@ -54,7 +55,7 @@ def enum_property_factory() -> Callable[..., EnumProperty]: def _factory(**kwargs): kwargs = _common_kwargs(kwargs) kwargs = { - "class_info": Class(name="", module_name=""), + "class_info": Class(name=kwargs["name"], module_name=kwargs["name"]), "values": {}, "value_type": str, **kwargs, @@ -109,6 +110,21 @@ def _factory(**kwargs): return _factory +@pytest.fixture +def int_property_factory() -> Callable[..., IntProperty]: + """ + This fixture surfaces in the test as a function which manufactures StringProperties with defaults. + + You can pass the same params into this as the StringProperty constructor to override defaults. + """ + + def _factory(**kwargs): + kwargs = _common_kwargs(kwargs) + return IntProperty(**kwargs) + + return _factory + + @pytest.fixture def date_time_property_factory() -> Callable[..., DateTimeProperty]: """ diff --git a/tests/test_parser/test_properties/test_model_property.py b/tests/test_parser/test_properties/test_model_property.py index b251f6100..0b5a729d1 100644 --- a/tests/test_parser/test_properties/test_model_property.py +++ b/tests/test_parser/test_properties/test_model_property.py @@ -5,12 +5,7 @@ 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 ( - Class, - EnumProperty, - IntProperty, - StringProperty, -) +from openapi_python_client.parser.properties import StringProperty @pytest.mark.parametrize( @@ -247,110 +242,110 @@ def test_conflicting_properties_same_types(self, model_property_factory, string_ assert isinstance(result, PropertyError) - def test_allof_string_and_string_enum(self, model_property_factory): + def test_allof_string_and_string_enum(self, model_property_factory, enum_property_factory, string_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, + enum_property = enum_property_factory( 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()]), + "/First": model_property_factory( + optional_properties=[string_property_factory(required=False, nullable=True)] + ), "/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 + assert result.required_props[0] == enum_property - def test_allof_string_enum_and_string(self, model_property_factory): + def test_allof_string_enum_and_string(self, model_property_factory, enum_property_factory, string_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, + enum_property = enum_property_factory( + required=False, 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()]), + "/Second": model_property_factory( + optional_properties=[string_property_factory(required=False, nullable=True)] + ), } ) 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): + def test_allof_int_and_int_enum(self, model_property_factory, enum_property_factory, int_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, + enum_property = enum_property_factory( 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)] - ), + "/First": model_property_factory(optional_properties=[int_property_factory()]), "/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 + assert result.required_props[0] == enum_property - def test_allof_string_enums(self, model_property_factory): + def test_allof_enum_incompatible_type(self, model_property_factory, enum_property_factory, int_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"), + enum_property = enum_property_factory( + values={"foo": 1}, value_type=str, - default=None, ) - enum_property2 = EnumProperty( - name="", - required=True, - nullable=True, + schemas = Schemas( + classes_by_reference={ + "/First": model_property_factory(optional_properties=[int_property_factory()]), + "/Second": model_property_factory(optional_properties=[enum_property]), + } + ) + + result = _process_properties(data=data, schemas=schemas, class_name="", config=Config()) + assert isinstance(result, PropertyError) + + def test_allof_string_enums(self, model_property_factory, enum_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 = enum_property_factory( + name="an_enum", + value_type=str, values={"foo": "foo"}, - class_info=Class(name="AnEnum2", module_name="an_enum2"), + ) + enum_property2 = enum_property_factory( + name="an_enum", + values={"foo": "foo", "bar": "bar"}, value_type=str, - default=None, ) schemas = Schemas( classes_by_reference={ @@ -360,32 +355,24 @@ def test_allof_string_enums(self, model_property_factory): ) result = _process_properties(data=data, schemas=schemas, class_name="", config=Config()) - assert result.optional_props[0] == enum_property2 + assert result.required_props[0] == enum_property1 - def test_allof_int_enums(self, model_property_factory): + def test_allof_int_enums(self, model_property_factory, enum_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, + enum_property1 = enum_property_factory( + name="an_enum", 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, + enum_property2 = enum_property_factory( + name="an_enum", values={"foo": 1}, - class_info=Class(name="AnEnum2", module_name="an_enum2"), value_type=int, - default=None, ) schemas = Schemas( classes_by_reference={ @@ -395,7 +382,34 @@ def test_allof_int_enums(self, model_property_factory): ) result = _process_properties(data=data, schemas=schemas, class_name="", config=Config()) - assert result.optional_props[0] == enum_property2 + assert result.required_props[0] == enum_property2 + + def test_allof_enums_are_not_subsets(self, model_property_factory, enum_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 = enum_property_factory( + name="an_enum", + values={"foo": 1, "bar": 2}, + value_type=int, + ) + enum_property2 = enum_property_factory( + name="an_enum", + values={"foo": 1, "baz": 3}, + value_type=int, + ) + 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 isinstance(result, PropertyError) def test_duplicate_properties(self, model_property_factory, string_property_factory): from openapi_python_client.parser.properties import Schemas