diff --git a/.changeset/correctly_resolve_references_to_a_type_that_is_itself_just_a_single_allof_reference.md b/.changeset/correctly_resolve_references_to_a_type_that_is_itself_just_a_single_allof_reference.md new file mode 100644 index 000000000..a55f8b7d1 --- /dev/null +++ b/.changeset/correctly_resolve_references_to_a_type_that_is_itself_just_a_single_allof_reference.md @@ -0,0 +1,7 @@ +--- +default: patch +--- + +# Correctly resolve references to a type that is itself just a single allOf reference + +PR #1103 fixed issue #1091. Thanks @eli-bl! diff --git a/end_to_end_tests/baseline_openapi_3.0.json b/end_to_end_tests/baseline_openapi_3.0.json index f34f27366..6f9d711f9 100644 --- a/end_to_end_tests/baseline_openapi_3.0.json +++ b/end_to_end_tests/baseline_openapi_3.0.json @@ -1629,6 +1629,33 @@ } } } + }, + "/models/allof": { + "get": { + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "aliased": { + "$ref": "#/components/schemas/Aliased" + }, + "extended": { + "$ref": "#/components/schemas/Extended" + }, + "model": { + "$ref": "#/components/schemas/AModel" + } + } + } + } + } + } + } + } } }, "components": { @@ -1647,6 +1674,23 @@ "an_required_field" ] }, + "Aliased":{ + "allOf": [ + {"$ref": "#/components/schemas/AModel"} + ] + }, + "Extended": { + "allOf": [ + {"$ref": "#/components/schemas/Aliased"}, + {"type": "object", + "properties": { + "fromExtended": { + "type": "string" + } + } + } + ] + }, "AModel": { "title": "AModel", "required": [ diff --git a/end_to_end_tests/baseline_openapi_3.1.yaml b/end_to_end_tests/baseline_openapi_3.1.yaml index 13d267b77..6bea1ec32 100644 --- a/end_to_end_tests/baseline_openapi_3.1.yaml +++ b/end_to_end_tests/baseline_openapi_3.1.yaml @@ -1619,7 +1619,34 @@ info: } } } - } + }, + "/models/allof": { + "get": { + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "aliased": { + "$ref": "#/components/schemas/Aliased" + }, + "extended": { + "$ref": "#/components/schemas/Extended" + }, + "model": { + "$ref": "#/components/schemas/AModel" + } + } + } + } + } + } + } + } + }, } "components": "schemas": { @@ -1637,6 +1664,23 @@ info: "an_required_field" ] }, + "Aliased": { + "allOf": [ + { "$ref": "#/components/schemas/AModel" } + ] + }, + "Extended": { + "allOf": [ + { "$ref": "#/components/schemas/Aliased" }, + { "type": "object", + "properties": { + "fromExtended": { + "type": "string" + } + } + } + ] + }, "AModel": { "title": "AModel", "required": [ @@ -1667,11 +1711,7 @@ info: "default": "overridden_default" }, "an_optional_allof_enum": { - "allOf": [ - { - "$ref": "#/components/schemas/AnAllOfEnum" - } - ] + "$ref": "#/components/schemas/AnAllOfEnum", }, "nested_list_of_enums": { "title": "Nested List Of Enums", @@ -1808,11 +1848,7 @@ info: ] }, "model": { - "allOf": [ - { - "$ref": "#/components/schemas/ModelWithUnionProperty" - } - ] + "$ref": "#/components/schemas/ModelWithUnionProperty" }, "nullable_model": { "oneOf": [ @@ -1825,11 +1861,7 @@ info: ] }, "not_required_model": { - "allOf": [ - { - "$ref": "#/components/schemas/ModelWithUnionProperty" - } - ] + "$ref": "#/components/schemas/ModelWithUnionProperty" }, "not_required_nullable_model": { "oneOf": [ diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/default/__init__.py b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/default/__init__.py index ab2d97db8..04d1162e8 100644 --- a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/default/__init__.py +++ b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/default/__init__.py @@ -2,7 +2,7 @@ import types -from . import get_common_parameters, post_common_parameters, reserved_parameters +from . import get_common_parameters, get_models_allof, post_common_parameters, reserved_parameters class DefaultEndpoints: @@ -17,3 +17,7 @@ def post_common_parameters(cls) -> types.ModuleType: @classmethod def reserved_parameters(cls) -> types.ModuleType: return reserved_parameters + + @classmethod + def get_models_allof(cls) -> types.ModuleType: + return get_models_allof diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/default/get_models_allof.py b/end_to_end_tests/golden-record/my_test_api_client/api/default/get_models_allof.py new file mode 100644 index 000000000..875aeeea1 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/api/default/get_models_allof.py @@ -0,0 +1,122 @@ +from http import HTTPStatus +from typing import Any, Dict, Optional, Union + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.get_models_allof_response_200 import GetModelsAllofResponse200 +from ...types import Response + + +def _get_kwargs() -> Dict[str, Any]: + _kwargs: Dict[str, Any] = { + "method": "get", + "url": "/models/allof", + } + + return _kwargs + + +def _parse_response( + *, client: Union[AuthenticatedClient, Client], response: httpx.Response +) -> Optional[GetModelsAllofResponse200]: + if response.status_code == HTTPStatus.OK: + response_200 = GetModelsAllofResponse200.from_dict(response.json()) + + return response_200 + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: Union[AuthenticatedClient, Client], response: httpx.Response +) -> Response[GetModelsAllofResponse200]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: Union[AuthenticatedClient, Client], +) -> Response[GetModelsAllofResponse200]: + """ + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[GetModelsAllofResponse200] + """ + + kwargs = _get_kwargs() + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: Union[AuthenticatedClient, Client], +) -> Optional[GetModelsAllofResponse200]: + """ + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + GetModelsAllofResponse200 + """ + + return sync_detailed( + client=client, + ).parsed + + +async def asyncio_detailed( + *, + client: Union[AuthenticatedClient, Client], +) -> Response[GetModelsAllofResponse200]: + """ + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[GetModelsAllofResponse200] + """ + + kwargs = _get_kwargs() + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: Union[AuthenticatedClient, Client], +) -> Optional[GetModelsAllofResponse200]: + """ + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + GetModelsAllofResponse200 + """ + + return ( + await asyncio_detailed( + client=client, + ) + ).parsed 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 7435983e3..cd0ea68da 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 @@ -34,9 +34,11 @@ from .body_upload_file_tests_upload_post_some_object import BodyUploadFileTestsUploadPostSomeObject from .body_upload_file_tests_upload_post_some_optional_object import BodyUploadFileTestsUploadPostSomeOptionalObject from .different_enum import DifferentEnum +from .extended import Extended from .free_form_model import FreeFormModel from .get_location_header_types_int_enum_header import GetLocationHeaderTypesIntEnumHeader from .get_location_header_types_string_enum_header import GetLocationHeaderTypesStringEnumHeader +from .get_models_allof_response_200 import GetModelsAllofResponse200 from .http_validation_error import HTTPValidationError from .import_ import Import from .json_like_body import JsonLikeBody @@ -111,9 +113,11 @@ "BodyUploadFileTestsUploadPostSomeObject", "BodyUploadFileTestsUploadPostSomeOptionalObject", "DifferentEnum", + "Extended", "FreeFormModel", "GetLocationHeaderTypesIntEnumHeader", "GetLocationHeaderTypesStringEnumHeader", + "GetModelsAllofResponse200", "HTTPValidationError", "Import", "JsonLikeBody", diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/extended.py b/end_to_end_tests/golden-record/my_test_api_client/models/extended.py new file mode 100644 index 000000000..932c98a99 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/extended.py @@ -0,0 +1,514 @@ +import datetime +from typing import TYPE_CHECKING, Any, Dict, List, Type, TypeVar, Union, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field +from dateutil.parser import isoparse + +from ..models.an_all_of_enum import AnAllOfEnum +from ..models.an_enum import AnEnum +from ..models.different_enum import DifferentEnum +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.free_form_model import FreeFormModel + from ..models.model_with_union_property import ModelWithUnionProperty + + +T = TypeVar("T", bound="Extended") + + +@_attrs_define +class Extended: + """ + Attributes: + an_enum_value (AnEnum): For testing Enums in all the ways they can be used + an_allof_enum_with_overridden_default (AnAllOfEnum): Default: AnAllOfEnum.OVERRIDDEN_DEFAULT. + a_camel_date_time (Union[datetime.date, datetime.datetime]): + a_date (datetime.date): + a_nullable_date (Union[None, datetime.date]): + required_nullable (Union[None, str]): + required_not_nullable (str): + one_of_models (Union['FreeFormModel', 'ModelWithUnionProperty', Any]): + nullable_one_of_models (Union['FreeFormModel', 'ModelWithUnionProperty', None]): + model (ModelWithUnionProperty): + nullable_model (Union['ModelWithUnionProperty', None]): + any_value (Union[Unset, Any]): + an_optional_allof_enum (Union[Unset, AnAllOfEnum]): + nested_list_of_enums (Union[Unset, List[List[DifferentEnum]]]): + a_not_required_date (Union[Unset, datetime.date]): + attr_1_leading_digit (Union[Unset, str]): + attr_leading_underscore (Union[Unset, str]): + not_required_nullable (Union[None, Unset, str]): + not_required_not_nullable (Union[Unset, str]): + not_required_one_of_models (Union['FreeFormModel', 'ModelWithUnionProperty', Unset]): + not_required_nullable_one_of_models (Union['FreeFormModel', 'ModelWithUnionProperty', None, Unset, str]): + not_required_model (Union[Unset, ModelWithUnionProperty]): + not_required_nullable_model (Union['ModelWithUnionProperty', None, Unset]): + from_extended (Union[Unset, str]): + """ + + an_enum_value: AnEnum + a_camel_date_time: Union[datetime.date, datetime.datetime] + a_date: datetime.date + a_nullable_date: Union[None, datetime.date] + required_nullable: Union[None, str] + required_not_nullable: str + one_of_models: Union["FreeFormModel", "ModelWithUnionProperty", Any] + nullable_one_of_models: Union["FreeFormModel", "ModelWithUnionProperty", None] + model: "ModelWithUnionProperty" + nullable_model: Union["ModelWithUnionProperty", None] + an_allof_enum_with_overridden_default: AnAllOfEnum = AnAllOfEnum.OVERRIDDEN_DEFAULT + any_value: Union[Unset, Any] = UNSET + an_optional_allof_enum: Union[Unset, AnAllOfEnum] = UNSET + nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET + a_not_required_date: Union[Unset, datetime.date] = UNSET + attr_1_leading_digit: Union[Unset, str] = UNSET + attr_leading_underscore: Union[Unset, str] = UNSET + not_required_nullable: Union[None, Unset, str] = UNSET + not_required_not_nullable: Union[Unset, str] = UNSET + not_required_one_of_models: Union["FreeFormModel", "ModelWithUnionProperty", Unset] = UNSET + not_required_nullable_one_of_models: Union["FreeFormModel", "ModelWithUnionProperty", None, Unset, str] = UNSET + not_required_model: Union[Unset, "ModelWithUnionProperty"] = UNSET + not_required_nullable_model: Union["ModelWithUnionProperty", None, Unset] = UNSET + from_extended: Union[Unset, str] = UNSET + additional_properties: Dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + from ..models.free_form_model import FreeFormModel + from ..models.model_with_union_property import ModelWithUnionProperty + + an_enum_value = self.an_enum_value.value + + an_allof_enum_with_overridden_default = self.an_allof_enum_with_overridden_default.value + + a_camel_date_time: str + if isinstance(self.a_camel_date_time, datetime.datetime): + a_camel_date_time = self.a_camel_date_time.isoformat() + else: + a_camel_date_time = self.a_camel_date_time.isoformat() + + a_date = self.a_date.isoformat() + + a_nullable_date: Union[None, str] + if isinstance(self.a_nullable_date, datetime.date): + a_nullable_date = self.a_nullable_date.isoformat() + else: + a_nullable_date = self.a_nullable_date + + required_nullable: Union[None, str] + required_nullable = self.required_nullable + + required_not_nullable = self.required_not_nullable + + one_of_models: Union[Any, Dict[str, Any]] + if isinstance(self.one_of_models, FreeFormModel): + one_of_models = self.one_of_models.to_dict() + elif isinstance(self.one_of_models, ModelWithUnionProperty): + one_of_models = self.one_of_models.to_dict() + else: + one_of_models = self.one_of_models + + nullable_one_of_models: Union[Dict[str, Any], None] + if isinstance(self.nullable_one_of_models, FreeFormModel): + nullable_one_of_models = self.nullable_one_of_models.to_dict() + elif isinstance(self.nullable_one_of_models, ModelWithUnionProperty): + nullable_one_of_models = self.nullable_one_of_models.to_dict() + else: + nullable_one_of_models = self.nullable_one_of_models + + model = self.model.to_dict() + + nullable_model: Union[Dict[str, Any], None] + if isinstance(self.nullable_model, ModelWithUnionProperty): + nullable_model = self.nullable_model.to_dict() + else: + nullable_model = self.nullable_model + + any_value = self.any_value + + an_optional_allof_enum: Union[Unset, str] = UNSET + if not isinstance(self.an_optional_allof_enum, Unset): + an_optional_allof_enum = self.an_optional_allof_enum.value + + nested_list_of_enums: Union[Unset, List[List[str]]] = UNSET + if not isinstance(self.nested_list_of_enums, Unset): + nested_list_of_enums = [] + for nested_list_of_enums_item_data in self.nested_list_of_enums: + nested_list_of_enums_item = [] + for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: + nested_list_of_enums_item_item = nested_list_of_enums_item_item_data.value + nested_list_of_enums_item.append(nested_list_of_enums_item_item) + + nested_list_of_enums.append(nested_list_of_enums_item) + + a_not_required_date: Union[Unset, str] = UNSET + if not isinstance(self.a_not_required_date, Unset): + a_not_required_date = self.a_not_required_date.isoformat() + + attr_1_leading_digit = self.attr_1_leading_digit + + attr_leading_underscore = self.attr_leading_underscore + + not_required_nullable: Union[None, Unset, str] + if isinstance(self.not_required_nullable, Unset): + not_required_nullable = UNSET + else: + not_required_nullable = self.not_required_nullable + + not_required_not_nullable = self.not_required_not_nullable + + not_required_one_of_models: Union[Dict[str, Any], Unset] + if isinstance(self.not_required_one_of_models, Unset): + not_required_one_of_models = UNSET + elif isinstance(self.not_required_one_of_models, FreeFormModel): + not_required_one_of_models = self.not_required_one_of_models.to_dict() + else: + not_required_one_of_models = self.not_required_one_of_models.to_dict() + + not_required_nullable_one_of_models: Union[Dict[str, Any], None, Unset, str] + if isinstance(self.not_required_nullable_one_of_models, Unset): + not_required_nullable_one_of_models = UNSET + elif isinstance(self.not_required_nullable_one_of_models, FreeFormModel): + not_required_nullable_one_of_models = self.not_required_nullable_one_of_models.to_dict() + elif isinstance(self.not_required_nullable_one_of_models, ModelWithUnionProperty): + not_required_nullable_one_of_models = self.not_required_nullable_one_of_models.to_dict() + else: + not_required_nullable_one_of_models = self.not_required_nullable_one_of_models + + not_required_model: Union[Unset, Dict[str, Any]] = UNSET + if not isinstance(self.not_required_model, Unset): + not_required_model = self.not_required_model.to_dict() + + not_required_nullable_model: Union[Dict[str, Any], None, Unset] + if isinstance(self.not_required_nullable_model, Unset): + not_required_nullable_model = UNSET + elif isinstance(self.not_required_nullable_model, ModelWithUnionProperty): + not_required_nullable_model = self.not_required_nullable_model.to_dict() + else: + not_required_nullable_model = self.not_required_nullable_model + + from_extended = self.from_extended + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "an_enum_value": an_enum_value, + "an_allof_enum_with_overridden_default": an_allof_enum_with_overridden_default, + "aCamelDateTime": a_camel_date_time, + "a_date": a_date, + "a_nullable_date": a_nullable_date, + "required_nullable": required_nullable, + "required_not_nullable": required_not_nullable, + "one_of_models": one_of_models, + "nullable_one_of_models": nullable_one_of_models, + "model": model, + "nullable_model": nullable_model, + } + ) + if any_value is not UNSET: + field_dict["any_value"] = any_value + if an_optional_allof_enum is not UNSET: + field_dict["an_optional_allof_enum"] = an_optional_allof_enum + if nested_list_of_enums is not UNSET: + field_dict["nested_list_of_enums"] = nested_list_of_enums + if a_not_required_date is not UNSET: + field_dict["a_not_required_date"] = a_not_required_date + if attr_1_leading_digit is not UNSET: + field_dict["1_leading_digit"] = attr_1_leading_digit + if attr_leading_underscore is not UNSET: + field_dict["_leading_underscore"] = attr_leading_underscore + if not_required_nullable is not UNSET: + field_dict["not_required_nullable"] = not_required_nullable + if not_required_not_nullable is not UNSET: + field_dict["not_required_not_nullable"] = not_required_not_nullable + if not_required_one_of_models is not UNSET: + field_dict["not_required_one_of_models"] = not_required_one_of_models + if not_required_nullable_one_of_models is not UNSET: + field_dict["not_required_nullable_one_of_models"] = not_required_nullable_one_of_models + if not_required_model is not UNSET: + field_dict["not_required_model"] = not_required_model + if not_required_nullable_model is not UNSET: + field_dict["not_required_nullable_model"] = not_required_nullable_model + if from_extended is not UNSET: + field_dict["fromExtended"] = from_extended + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + from ..models.free_form_model import FreeFormModel + from ..models.model_with_union_property import ModelWithUnionProperty + + d = src_dict.copy() + an_enum_value = AnEnum(d.pop("an_enum_value")) + + an_allof_enum_with_overridden_default = AnAllOfEnum(d.pop("an_allof_enum_with_overridden_default")) + + def _parse_a_camel_date_time(data: object) -> Union[datetime.date, datetime.datetime]: + try: + if not isinstance(data, str): + raise TypeError() + a_camel_date_time_type_0 = isoparse(data) + + return a_camel_date_time_type_0 + except: # noqa: E722 + pass + if not isinstance(data, str): + raise TypeError() + a_camel_date_time_type_1 = isoparse(data).date() + + return a_camel_date_time_type_1 + + a_camel_date_time = _parse_a_camel_date_time(d.pop("aCamelDateTime")) + + a_date = isoparse(d.pop("a_date")).date() + + def _parse_a_nullable_date(data: object) -> Union[None, datetime.date]: + if data is None: + return data + try: + if not isinstance(data, str): + raise TypeError() + a_nullable_date_type_0 = isoparse(data).date() + + return a_nullable_date_type_0 + except: # noqa: E722 + pass + return cast(Union[None, datetime.date], data) + + a_nullable_date = _parse_a_nullable_date(d.pop("a_nullable_date")) + + def _parse_required_nullable(data: object) -> Union[None, str]: + if data is None: + return data + return cast(Union[None, str], data) + + required_nullable = _parse_required_nullable(d.pop("required_nullable")) + + required_not_nullable = d.pop("required_not_nullable") + + def _parse_one_of_models(data: object) -> Union["FreeFormModel", "ModelWithUnionProperty", Any]: + try: + if not isinstance(data, dict): + raise TypeError() + one_of_models_type_0 = FreeFormModel.from_dict(data) + + return one_of_models_type_0 + except: # noqa: E722 + pass + try: + if not isinstance(data, dict): + raise TypeError() + one_of_models_type_1 = ModelWithUnionProperty.from_dict(data) + + return one_of_models_type_1 + except: # noqa: E722 + pass + return cast(Union["FreeFormModel", "ModelWithUnionProperty", Any], data) + + one_of_models = _parse_one_of_models(d.pop("one_of_models")) + + def _parse_nullable_one_of_models(data: object) -> Union["FreeFormModel", "ModelWithUnionProperty", None]: + if data is None: + return data + try: + if not isinstance(data, dict): + raise TypeError() + nullable_one_of_models_type_0 = FreeFormModel.from_dict(data) + + return nullable_one_of_models_type_0 + except: # noqa: E722 + pass + try: + if not isinstance(data, dict): + raise TypeError() + nullable_one_of_models_type_1 = ModelWithUnionProperty.from_dict(data) + + return nullable_one_of_models_type_1 + except: # noqa: E722 + pass + return cast(Union["FreeFormModel", "ModelWithUnionProperty", None], data) + + nullable_one_of_models = _parse_nullable_one_of_models(d.pop("nullable_one_of_models")) + + model = ModelWithUnionProperty.from_dict(d.pop("model")) + + def _parse_nullable_model(data: object) -> Union["ModelWithUnionProperty", None]: + if data is None: + return data + try: + if not isinstance(data, dict): + raise TypeError() + nullable_model_type_1 = ModelWithUnionProperty.from_dict(data) + + return nullable_model_type_1 + except: # noqa: E722 + pass + return cast(Union["ModelWithUnionProperty", None], data) + + nullable_model = _parse_nullable_model(d.pop("nullable_model")) + + any_value = d.pop("any_value", UNSET) + + _an_optional_allof_enum = d.pop("an_optional_allof_enum", UNSET) + an_optional_allof_enum: Union[Unset, AnAllOfEnum] + if isinstance(_an_optional_allof_enum, Unset): + an_optional_allof_enum = UNSET + else: + an_optional_allof_enum = AnAllOfEnum(_an_optional_allof_enum) + + nested_list_of_enums = [] + _nested_list_of_enums = d.pop("nested_list_of_enums", UNSET) + for nested_list_of_enums_item_data in _nested_list_of_enums or []: + nested_list_of_enums_item = [] + _nested_list_of_enums_item = nested_list_of_enums_item_data + for nested_list_of_enums_item_item_data in _nested_list_of_enums_item: + nested_list_of_enums_item_item = DifferentEnum(nested_list_of_enums_item_item_data) + + nested_list_of_enums_item.append(nested_list_of_enums_item_item) + + nested_list_of_enums.append(nested_list_of_enums_item) + + _a_not_required_date = d.pop("a_not_required_date", UNSET) + a_not_required_date: Union[Unset, datetime.date] + if isinstance(_a_not_required_date, Unset): + a_not_required_date = UNSET + else: + a_not_required_date = isoparse(_a_not_required_date).date() + + attr_1_leading_digit = d.pop("1_leading_digit", UNSET) + + attr_leading_underscore = d.pop("_leading_underscore", UNSET) + + def _parse_not_required_nullable(data: object) -> Union[None, Unset, str]: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(Union[None, Unset, str], data) + + not_required_nullable = _parse_not_required_nullable(d.pop("not_required_nullable", UNSET)) + + not_required_not_nullable = d.pop("not_required_not_nullable", UNSET) + + def _parse_not_required_one_of_models(data: object) -> Union["FreeFormModel", "ModelWithUnionProperty", Unset]: + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + not_required_one_of_models_type_0 = FreeFormModel.from_dict(data) + + return not_required_one_of_models_type_0 + except: # noqa: E722 + pass + if not isinstance(data, dict): + raise TypeError() + not_required_one_of_models_type_1 = ModelWithUnionProperty.from_dict(data) + + return not_required_one_of_models_type_1 + + not_required_one_of_models = _parse_not_required_one_of_models(d.pop("not_required_one_of_models", UNSET)) + + def _parse_not_required_nullable_one_of_models( + data: object, + ) -> Union["FreeFormModel", "ModelWithUnionProperty", None, Unset, str]: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + not_required_nullable_one_of_models_type_0 = FreeFormModel.from_dict(data) + + return not_required_nullable_one_of_models_type_0 + except: # noqa: E722 + pass + try: + if not isinstance(data, dict): + raise TypeError() + not_required_nullable_one_of_models_type_1 = ModelWithUnionProperty.from_dict(data) + + return not_required_nullable_one_of_models_type_1 + except: # noqa: E722 + pass + return cast(Union["FreeFormModel", "ModelWithUnionProperty", None, Unset, str], data) + + not_required_nullable_one_of_models = _parse_not_required_nullable_one_of_models( + d.pop("not_required_nullable_one_of_models", UNSET) + ) + + _not_required_model = d.pop("not_required_model", UNSET) + not_required_model: Union[Unset, ModelWithUnionProperty] + if isinstance(_not_required_model, Unset): + not_required_model = UNSET + else: + not_required_model = ModelWithUnionProperty.from_dict(_not_required_model) + + def _parse_not_required_nullable_model(data: object) -> Union["ModelWithUnionProperty", None, Unset]: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + not_required_nullable_model_type_1 = ModelWithUnionProperty.from_dict(data) + + return not_required_nullable_model_type_1 + except: # noqa: E722 + pass + return cast(Union["ModelWithUnionProperty", None, Unset], data) + + not_required_nullable_model = _parse_not_required_nullable_model(d.pop("not_required_nullable_model", UNSET)) + + from_extended = d.pop("fromExtended", UNSET) + + extended = cls( + an_enum_value=an_enum_value, + an_allof_enum_with_overridden_default=an_allof_enum_with_overridden_default, + a_camel_date_time=a_camel_date_time, + a_date=a_date, + a_nullable_date=a_nullable_date, + required_nullable=required_nullable, + required_not_nullable=required_not_nullable, + one_of_models=one_of_models, + nullable_one_of_models=nullable_one_of_models, + model=model, + nullable_model=nullable_model, + any_value=any_value, + an_optional_allof_enum=an_optional_allof_enum, + nested_list_of_enums=nested_list_of_enums, + a_not_required_date=a_not_required_date, + attr_1_leading_digit=attr_1_leading_digit, + attr_leading_underscore=attr_leading_underscore, + not_required_nullable=not_required_nullable, + not_required_not_nullable=not_required_not_nullable, + not_required_one_of_models=not_required_one_of_models, + not_required_nullable_one_of_models=not_required_nullable_one_of_models, + not_required_model=not_required_model, + not_required_nullable_model=not_required_nullable_model, + from_extended=from_extended, + ) + + extended.additional_properties = d + return extended + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/get_models_allof_response_200.py b/end_to_end_tests/golden-record/my_test_api_client/models/get_models_allof_response_200.py new file mode 100644 index 000000000..2662dc1f4 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/get_models_allof_response_200.py @@ -0,0 +1,105 @@ +from typing import TYPE_CHECKING, Any, Dict, List, Type, TypeVar, Union + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.a_model import AModel + from ..models.extended import Extended + + +T = TypeVar("T", bound="GetModelsAllofResponse200") + + +@_attrs_define +class GetModelsAllofResponse200: + """ + Attributes: + aliased (Union[Unset, AModel]): A Model for testing all the ways custom objects can be used + extended (Union[Unset, Extended]): + model (Union[Unset, AModel]): A Model for testing all the ways custom objects can be used + """ + + aliased: Union[Unset, "AModel"] = UNSET + extended: Union[Unset, "Extended"] = UNSET + model: Union[Unset, "AModel"] = UNSET + additional_properties: Dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + aliased: Union[Unset, Dict[str, Any]] = UNSET + if not isinstance(self.aliased, Unset): + aliased = self.aliased.to_dict() + + extended: Union[Unset, Dict[str, Any]] = UNSET + if not isinstance(self.extended, Unset): + extended = self.extended.to_dict() + + model: Union[Unset, Dict[str, Any]] = UNSET + if not isinstance(self.model, Unset): + model = self.model.to_dict() + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if aliased is not UNSET: + field_dict["aliased"] = aliased + if extended is not UNSET: + field_dict["extended"] = extended + if model is not UNSET: + field_dict["model"] = model + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + from ..models.a_model import AModel + from ..models.extended import Extended + + d = src_dict.copy() + _aliased = d.pop("aliased", UNSET) + aliased: Union[Unset, AModel] + if isinstance(_aliased, Unset): + aliased = UNSET + else: + aliased = AModel.from_dict(_aliased) + + _extended = d.pop("extended", UNSET) + extended: Union[Unset, Extended] + if isinstance(_extended, Unset): + extended = UNSET + else: + extended = Extended.from_dict(_extended) + + _model = d.pop("model", UNSET) + model: Union[Unset, AModel] + if isinstance(_model, Unset): + model = UNSET + else: + model = AModel.from_dict(_model) + + get_models_allof_response_200 = cls( + aliased=aliased, + extended=extended, + model=model, + ) + + get_models_allof_response_200.additional_properties = d + return get_models_allof_response_200 + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/openapi_python_client/parser/bodies.py b/openapi_python_client/parser/bodies.py index 8c2c86f30..8515ad7cc 100644 --- a/openapi_python_client/parser/bodies.py +++ b/openapi_python_client/parser/bodies.py @@ -117,6 +117,7 @@ def body_from_data( **schemas.classes_by_name, prop.class_info.name: prop, }, + models_to_process=[*schemas.models_to_process, prop], ) bodies.append( Body( diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index e692ce5bb..b85dac635 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -126,7 +126,7 @@ def _property_from_ref( return prop, schemas -def property_from_data( # noqa: PLR0911 +def property_from_data( # noqa: PLR0911, PLR0912 name: str, required: bool, data: oai.Reference | oai.Schema, @@ -153,7 +153,7 @@ def property_from_data( # noqa: PLR0911 sub_data: list[oai.Schema | oai.Reference] = data.allOf + data.anyOf + data.oneOf # A union of a single reference should just be passed through to that reference (don't create copy class) if len(sub_data) == 1 and isinstance(sub_data[0], oai.Reference): - return _property_from_ref( + prop, schemas = _property_from_ref( name=name, required=required, parent=data, @@ -162,6 +162,16 @@ def property_from_data( # noqa: PLR0911 config=config, roots=roots, ) + # We won't be generating a separate Python class for this schema - references to it will just use + # the class for the schema it's referencing - so we don't add it to classes_by_name; but we do + # add it to models_to_process, if it's a model, because its properties still need to be resolved. + if isinstance(prop, ModelProperty): + schemas = evolve( + schemas, + models_to_process=[*schemas.models_to_process, prop], + ) + return prop, schemas + if data.type == oai.DataType.BOOLEAN: return ( BooleanProperty.build( @@ -341,7 +351,7 @@ def _process_model_errors( def _process_models(*, schemas: Schemas, config: Config) -> Schemas: - to_process = (prop for prop in schemas.classes_by_name.values() if isinstance(prop, ModelProperty)) + to_process = schemas.models_to_process still_making_progress = True final_model_errors: list[tuple[ModelProperty, PropertyError]] = [] latest_model_errors: list[tuple[ModelProperty, PropertyError]] = [] @@ -368,12 +378,11 @@ def _process_models(*, schemas: Schemas, config: Config) -> Schemas: continue schemas = schemas_or_err still_making_progress = True - to_process = (prop for prop in next_round) + to_process = next_round final_model_errors.extend(latest_model_errors) errors = _process_model_errors(final_model_errors, schemas=schemas) - schemas.errors.extend(errors) - return schemas + return evolve(schemas, errors=[*schemas.errors, *errors], models_to_process=to_process) def build_schemas( diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index e0c06641f..897632fce 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -119,7 +119,11 @@ def build( ) return error, schemas - schemas = evolve(schemas, classes_by_name={**schemas.classes_by_name, class_info.name: prop}) + schemas = evolve( + schemas, + classes_by_name={**schemas.classes_by_name, class_info.name: prop}, + models_to_process=[*schemas.models_to_process, prop], + ) return prop, schemas @classmethod diff --git a/openapi_python_client/parser/properties/schemas.py b/openapi_python_client/parser/properties/schemas.py index 9e4fc545e..dad89a572 100644 --- a/openapi_python_client/parser/properties/schemas.py +++ b/openapi_python_client/parser/properties/schemas.py @@ -22,8 +22,10 @@ from ..errors import ParameterError, ParseError, PropertyError if TYPE_CHECKING: # pragma: no cover + from .model_property import ModelProperty from .property import Property else: + ModelProperty = "ModelProperty" Property = "Property" @@ -77,6 +79,7 @@ class Schemas: classes_by_reference: Dict[ReferencePath, Property] = field(factory=dict) dependencies: Dict[ReferencePath, Set[Union[ReferencePath, ClassName]]] = field(factory=dict) classes_by_name: Dict[ClassName, Property] = field(factory=dict) + models_to_process: List[ModelProperty] = field(factory=list) errors: List[ParseError] = field(factory=list) def add_dependencies(self, ref_path: ReferencePath, roots: Set[Union[ReferencePath, ClassName]]) -> None: diff --git a/pyproject.toml b/pyproject.toml index 1cf336774..ef948da30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -129,7 +129,7 @@ composite = ["test --cov openapi_python_client tests --cov-report=term-missing"] [tool.pdm.scripts.regen_integration] shell = """ -openapi-python-client update --url https://raw.githubusercontent.com/openapi-generators/openapi-test-server/main/openapi.json --config integration-tests/config.yaml --meta pdm \ +openapi-python-client generate --overwrite --url https://raw.githubusercontent.com/openapi-generators/openapi-test-server/main/openapi.json --config integration-tests/config.yaml --meta none --output-path integration-tests/integration_tests \ """ [build-system] diff --git a/tests/test_parser/test_properties/test_init.py b/tests/test_parser/test_properties/test_init.py index 3290dcd39..3c60c2daf 100644 --- a/tests/test_parser/test_properties/test_init.py +++ b/tests/test_parser/test_properties/test_init.py @@ -530,6 +530,7 @@ def test_property_from_data_ref_enum_with_overridden_default(self, enum_property prop, new_schemas = property_from_data( name=name, required=required, data=data, schemas=schemas, parent_name="", config=config ) + new_schemas = attr.evolve(new_schemas, models_to_process=[]) # intermediate state irrelevant to this test assert prop == enum_property_factory( name="some_enum", @@ -911,37 +912,6 @@ def test_retries_failing_properties_while_making_progress(self, mocker, config): class TestProcessModels: - def test_retries_failing_models_while_making_progress( - self, mocker, model_property_factory, any_property_factory, config - ): - from openapi_python_client.parser.properties import _process_models - - first_model = model_property_factory() - second_class_name = ClassName("second", "") - schemas = Schemas( - classes_by_name={ - ClassName("first", ""): first_model, - second_class_name: model_property_factory(), - ClassName("non-model", ""): any_property_factory(), - } - ) - process_model = mocker.patch( - f"{MODULE_NAME}.process_model", side_effect=[PropertyError(), Schemas(), PropertyError()] - ) - process_model_errors = mocker.patch(f"{MODULE_NAME}._process_model_errors", return_value=["error"]) - - result = _process_models(schemas=schemas, config=config) - - process_model.assert_has_calls( - [ - call(first_model, schemas=schemas, config=config), - call(schemas.classes_by_name[second_class_name], schemas=schemas, config=config), - call(first_model, schemas=result, config=config), - ] - ) - assert process_model_errors.was_called_once_with([(first_model, PropertyError())]) - assert all(error in result.errors for error in process_model_errors.return_value) - def test_detect_recursive_allof_reference_no_retry(self, mocker, model_property_factory, config): from openapi_python_client.parser.properties import Class, _process_models from openapi_python_client.schema import Reference @@ -950,14 +920,16 @@ def test_detect_recursive_allof_reference_no_retry(self, mocker, model_property_ recursive_model = model_property_factory( class_info=Class(name=class_name, module_name=PythonIdentifier("module_name", "")) ) + second_model = model_property_factory() schemas = Schemas( classes_by_name={ "recursive": recursive_model, - "second": model_property_factory(), - } + "second": second_model, + }, + models_to_process=[recursive_model, second_model], ) recursion_error = PropertyError(data=Reference.model_construct(ref=f"#/{class_name}")) - process_model = mocker.patch(f"{MODULE_NAME}.process_model", side_effect=[recursion_error, Schemas()]) + process_model = mocker.patch(f"{MODULE_NAME}.process_model", side_effect=[recursion_error, schemas]) process_model_errors = mocker.patch(f"{MODULE_NAME}._process_model_errors", return_value=["error"]) result = _process_models(schemas=schemas, config=config) @@ -972,6 +944,58 @@ def test_detect_recursive_allof_reference_no_retry(self, mocker, model_property_ assert all(error in result.errors for error in process_model_errors.return_value) assert "\n\nRecursive allOf reference found" in recursion_error.detail + def test_resolve_reference_to_single_allof_reference(self, config, model_property_factory): + # test for https://github.com/openapi-generators/openapi-python-client/issues/1091 + from openapi_python_client.parser.properties import Schemas, build_schemas + + components = { + "Model1": oai.Schema.model_construct( + type="object", + properties={ + "prop1": oai.Schema.model_construct(type="string"), + }, + ), + "Model2": oai.Schema.model_construct( + allOf=[ + oai.Reference.model_construct(ref="#/components/schemas/Model1"), + ] + ), + "Model3": oai.Schema.model_construct( + allOf=[ + oai.Reference.model_construct(ref="#/components/schemas/Model2"), + oai.Schema.model_construct( + type="object", + properties={ + "prop2": oai.Schema.model_construct(type="string"), + }, + ), + ], + ), + } + schemas = Schemas() + + result = build_schemas(components=components, schemas=schemas, config=config) + + assert result.errors == [] + assert result.models_to_process == [] + + # Classes should only be generated for Model1 and Model3 + assert result.classes_by_name.keys() == {"Model1", "Model3"} + + # References to Model2 should be resolved to the same class as Model1 + assert result.classes_by_reference.keys() == { + "/components/schemas/Model1", + "/components/schemas/Model2", + "/components/schemas/Model3", + } + assert ( + result.classes_by_reference["/components/schemas/Model2"].class_info + == result.classes_by_reference["/components/schemas/Model1"].class_info + ) + + # Verify that Model3 extended the properties from Model1 + assert [p.name for p in result.classes_by_name["Model3"].optional_properties] == ["prop1", "prop2"] + class TestPropogateRemoval: def test_propogate_removal_class_name(self):