Skip to content

feat: Allow allOf between enums, string, or int properties that are subtypes #423

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from enum import Enum


class AnotherAllOfSubModelType(str, Enum):
SUBMODEL = "submodel"

def __str__(self) -> str:
return str(self.value)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from enum import IntEnum


class AnotherAllOfSubModelTypeEnum(IntEnum):
VALUE_0 = 0

def __str__(self) -> str:
return str(self.value)
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -12,18 +14,32 @@ 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] = {}
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
if another_sub_property is not UNSET:
field_dict["another_sub_property"] = another_sub_property

Expand All @@ -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,
)

Expand Down
15 changes: 15 additions & 0 deletions end_to_end_tests/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1315,6 +1315,13 @@
"properties": {
"a_sub_property": {
"type": "string"
},
"type": {
"type": "string"
},
"type_enum": {
"type": "int",
"enum": [0, 1]
}
}
},
Expand All @@ -1324,6 +1331,14 @@
"properties": {
"another_sub_property": {
"type": "string"
},
"type": {
"type": "string",
"enum": ["submodel"]
},
"type_enum": {
"type": "int",
"enum": [0]
}
}
},
Expand Down
57 changes: 49 additions & 8 deletions openapi_python_client/parser/properties/model_property.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
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

from ... import Config
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

Expand Down Expand Up @@ -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)),
]
)
Comment on lines +68 to +79
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using any like this will cause every single case to be evaluated before any is, causing extra work. Here I think the best bet is to refactor into an if/return sequence.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Thanks @dbanty for taking care of this.



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):
Expand Down
Loading