diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py index 5eb7ec0f0..4d9d0bec0 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py @@ -24,8 +24,8 @@ def _get_kwargs( union_prop: Union[float, str] = "not a float", union_prop_with_ref: Union[AnEnum, None, Unset, float] = 0.6, enum_prop: AnEnum, - model_prop: ModelWithUnionProperty, - required_model_prop: ModelWithUnionProperty, + model_prop: "ModelWithUnionProperty", + required_model_prop: "ModelWithUnionProperty", ) -> Dict[str, Any]: url = "{}/tests/defaults".format(client.base_url) @@ -130,8 +130,8 @@ def sync_detailed( union_prop: Union[float, str] = "not a float", union_prop_with_ref: Union[AnEnum, None, Unset, float] = 0.6, enum_prop: AnEnum, - model_prop: ModelWithUnionProperty, - required_model_prop: ModelWithUnionProperty, + model_prop: "ModelWithUnionProperty", + required_model_prop: "ModelWithUnionProperty", ) -> Response[Union[Any, HTTPValidationError]]: """Defaults @@ -187,8 +187,8 @@ def sync( union_prop: Union[float, str] = "not a float", union_prop_with_ref: Union[AnEnum, None, Unset, float] = 0.6, enum_prop: AnEnum, - model_prop: ModelWithUnionProperty, - required_model_prop: ModelWithUnionProperty, + model_prop: "ModelWithUnionProperty", + required_model_prop: "ModelWithUnionProperty", ) -> Optional[Union[Any, HTTPValidationError]]: """Defaults @@ -237,8 +237,8 @@ async def asyncio_detailed( union_prop: Union[float, str] = "not a float", union_prop_with_ref: Union[AnEnum, None, Unset, float] = 0.6, enum_prop: AnEnum, - model_prop: ModelWithUnionProperty, - required_model_prop: ModelWithUnionProperty, + model_prop: "ModelWithUnionProperty", + required_model_prop: "ModelWithUnionProperty", ) -> Response[Union[Any, HTTPValidationError]]: """Defaults @@ -292,8 +292,8 @@ async def asyncio( union_prop: Union[float, str] = "not a float", union_prop_with_ref: Union[AnEnum, None, Unset, float] = 0.6, enum_prop: AnEnum, - model_prop: ModelWithUnionProperty, - required_model_prop: ModelWithUnionProperty, + model_prop: "ModelWithUnionProperty", + required_model_prop: "ModelWithUnionProperty", ) -> Optional[Union[Any, HTTPValidationError]]: """Defaults diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py index ba0a3351e..0fd856e12 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py @@ -69,7 +69,7 @@ def _get_kwargs( } -def _parse_response(*, response: httpx.Response) -> Optional[Union[HTTPValidationError, List[AModel]]]: +def _parse_response(*, response: httpx.Response) -> Optional[Union[HTTPValidationError, List["AModel"]]]: if response.status_code == HTTPStatus.OK: response_200 = [] _response_200 = response.json() @@ -90,7 +90,7 @@ def _parse_response(*, response: httpx.Response) -> Optional[Union[HTTPValidatio return None -def _build_response(*, response: httpx.Response) -> Response[Union[HTTPValidationError, List[AModel]]]: +def _build_response(*, response: httpx.Response) -> Response[Union[HTTPValidationError, List["AModel"]]]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -106,7 +106,7 @@ def sync_detailed( an_enum_value_with_null: List[Optional[AnEnumWithNull]], an_enum_value_with_only_null: List[None], some_date: Union[datetime.date, datetime.datetime], -) -> Response[Union[HTTPValidationError, List[AModel]]]: +) -> Response[Union[HTTPValidationError, List["AModel"]]]: """Get List Get a list of things @@ -118,7 +118,7 @@ def sync_detailed( some_date (Union[datetime.date, datetime.datetime]): Returns: - Response[Union[HTTPValidationError, List[AModel]]] + Response[Union[HTTPValidationError, List['AModel']]] """ kwargs = _get_kwargs( @@ -144,7 +144,7 @@ def sync( an_enum_value_with_null: List[Optional[AnEnumWithNull]], an_enum_value_with_only_null: List[None], some_date: Union[datetime.date, datetime.datetime], -) -> Optional[Union[HTTPValidationError, List[AModel]]]: +) -> Optional[Union[HTTPValidationError, List["AModel"]]]: """Get List Get a list of things @@ -156,7 +156,7 @@ def sync( some_date (Union[datetime.date, datetime.datetime]): Returns: - Response[Union[HTTPValidationError, List[AModel]]] + Response[Union[HTTPValidationError, List['AModel']]] """ return sync_detailed( @@ -175,7 +175,7 @@ async def asyncio_detailed( an_enum_value_with_null: List[Optional[AnEnumWithNull]], an_enum_value_with_only_null: List[None], some_date: Union[datetime.date, datetime.datetime], -) -> Response[Union[HTTPValidationError, List[AModel]]]: +) -> Response[Union[HTTPValidationError, List["AModel"]]]: """Get List Get a list of things @@ -187,7 +187,7 @@ async def asyncio_detailed( some_date (Union[datetime.date, datetime.datetime]): Returns: - Response[Union[HTTPValidationError, List[AModel]]] + Response[Union[HTTPValidationError, List['AModel']]] """ kwargs = _get_kwargs( @@ -211,7 +211,7 @@ async def asyncio( an_enum_value_with_null: List[Optional[AnEnumWithNull]], an_enum_value_with_only_null: List[None], some_date: Union[datetime.date, datetime.datetime], -) -> Optional[Union[HTTPValidationError, List[AModel]]]: +) -> Optional[Union[HTTPValidationError, List["AModel"]]]: """Get List Get a list of things @@ -223,7 +223,7 @@ async def asyncio( some_date (Union[datetime.date, datetime.datetime]): Returns: - Response[Union[HTTPValidationError, List[AModel]]] + Response[Union[HTTPValidationError, List['AModel']]] """ return ( 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 04344ee19..4216f0b71 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 @@ -6,6 +6,18 @@ 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_array_with_a_circular_ref_in_items_object_a_item import AnArrayWithACircularRefInItemsObjectAItem +from .an_array_with_a_circular_ref_in_items_object_additional_properties_a_item import ( + AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem, +) +from .an_array_with_a_circular_ref_in_items_object_additional_properties_b_item import ( + AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem, +) +from .an_array_with_a_circular_ref_in_items_object_b_item import AnArrayWithACircularRefInItemsObjectBItem +from .an_array_with_a_recursive_ref_in_items_object_additional_properties_item import ( + AnArrayWithARecursiveRefInItemsObjectAdditionalPropertiesItem, +) +from .an_array_with_a_recursive_ref_in_items_object_item import AnArrayWithARecursiveRefInItemsObjectItem from .an_enum import AnEnum from .an_enum_with_null import AnEnumWithNull from .an_int_enum import AnIntEnum @@ -33,10 +45,16 @@ from .model_with_additional_properties_refed import ModelWithAdditionalPropertiesRefed from .model_with_any_json_properties import ModelWithAnyJsonProperties from .model_with_any_json_properties_additional_property_type_0 import ModelWithAnyJsonPropertiesAdditionalPropertyType0 +from .model_with_circular_ref_a import ModelWithCircularRefA +from .model_with_circular_ref_b import ModelWithCircularRefB +from .model_with_circular_ref_in_additional_properties_a import ModelWithCircularRefInAdditionalPropertiesA +from .model_with_circular_ref_in_additional_properties_b import ModelWithCircularRefInAdditionalPropertiesB from .model_with_date_time_property import ModelWithDateTimeProperty from .model_with_primitive_additional_properties import ModelWithPrimitiveAdditionalProperties from .model_with_primitive_additional_properties_a_date_holder import ModelWithPrimitiveAdditionalPropertiesADateHolder from .model_with_property_ref import ModelWithPropertyRef +from .model_with_recursive_ref import ModelWithRecursiveRef +from .model_with_recursive_ref_in_additional_properties import ModelWithRecursiveRefInAdditionalProperties from .model_with_union_property import ModelWithUnionProperty from .model_with_union_property_inlined import ModelWithUnionPropertyInlined from .model_with_union_property_inlined_fruit_type_0 import ModelWithUnionPropertyInlinedFruitType0 @@ -58,6 +76,12 @@ "AModel", "AModelWithPropertiesReferenceThatAreNotObject", "AnAllOfEnum", + "AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem", + "AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem", + "AnArrayWithACircularRefInItemsObjectAItem", + "AnArrayWithACircularRefInItemsObjectBItem", + "AnArrayWithARecursiveRefInItemsObjectAdditionalPropertiesItem", + "AnArrayWithARecursiveRefInItemsObjectItem", "AnEnum", "AnEnumWithNull", "AnIntEnum", @@ -83,10 +107,16 @@ "ModelWithAdditionalPropertiesRefed", "ModelWithAnyJsonProperties", "ModelWithAnyJsonPropertiesAdditionalPropertyType0", + "ModelWithCircularRefA", + "ModelWithCircularRefB", + "ModelWithCircularRefInAdditionalPropertiesA", + "ModelWithCircularRefInAdditionalPropertiesB", "ModelWithDateTimeProperty", "ModelWithPrimitiveAdditionalProperties", "ModelWithPrimitiveAdditionalPropertiesADateHolder", "ModelWithPropertyRef", + "ModelWithRecursiveRef", + "ModelWithRecursiveRefInAdditionalProperties", "ModelWithUnionProperty", "ModelWithUnionPropertyInlined", "ModelWithUnionPropertyInlinedFruitType0", diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py index 0cf302e56..889237c7b 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py @@ -1,5 +1,5 @@ import datetime -from typing import Any, Dict, List, Optional, Type, TypeVar, Union, cast +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, TypeVar, Union, cast import attr from dateutil.parser import isoparse @@ -7,10 +7,13 @@ from ..models.an_all_of_enum import AnAllOfEnum from ..models.an_enum import AnEnum from ..models.different_enum import DifferentEnum -from ..models.free_form_model import FreeFormModel -from ..models.model_with_union_property import ModelWithUnionProperty 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="AModel") @@ -24,7 +27,7 @@ class AModel: a_camel_date_time (Union[datetime.date, datetime.datetime]): a_date (datetime.date): required_not_nullable (str): - one_of_models (Union[Any, FreeFormModel, ModelWithUnionProperty]): + one_of_models (Union['FreeFormModel', 'ModelWithUnionProperty', Any]): model (ModelWithUnionProperty): any_value (Union[Unset, Any]): an_optional_allof_enum (Union[Unset, AnAllOfEnum]): @@ -35,9 +38,9 @@ class AModel: required_nullable (Optional[str]): not_required_nullable (Union[Unset, None, str]): not_required_not_nullable (Union[Unset, str]): - nullable_one_of_models (Union[FreeFormModel, ModelWithUnionProperty, None]): - not_required_one_of_models (Union[FreeFormModel, ModelWithUnionProperty, Unset]): - not_required_nullable_one_of_models (Union[FreeFormModel, ModelWithUnionProperty, None, Unset, str]): + nullable_one_of_models (Union['FreeFormModel', 'ModelWithUnionProperty', None]): + not_required_one_of_models (Union['FreeFormModel', 'ModelWithUnionProperty', Unset]): + not_required_nullable_one_of_models (Union['FreeFormModel', 'ModelWithUnionProperty', None, Unset, str]): nullable_model (Optional[ModelWithUnionProperty]): not_required_model (Union[Unset, ModelWithUnionProperty]): not_required_nullable_model (Union[Unset, None, ModelWithUnionProperty]): @@ -47,12 +50,12 @@ class AModel: a_camel_date_time: Union[datetime.date, datetime.datetime] a_date: datetime.date required_not_nullable: str - one_of_models: Union[Any, FreeFormModel, ModelWithUnionProperty] - model: ModelWithUnionProperty + one_of_models: Union["FreeFormModel", "ModelWithUnionProperty", Any] + model: "ModelWithUnionProperty" a_nullable_date: Optional[datetime.date] required_nullable: Optional[str] - nullable_one_of_models: Union[FreeFormModel, ModelWithUnionProperty, None] - nullable_model: Optional[ModelWithUnionProperty] + nullable_one_of_models: Union["FreeFormModel", "ModelWithUnionProperty", None] + nullable_model: Optional["ModelWithUnionProperty"] an_allof_enum_with_overridden_default: AnAllOfEnum = AnAllOfEnum.OVERRIDDEN_DEFAULT any_value: Union[Unset, Any] = UNSET an_optional_allof_enum: Union[Unset, AnAllOfEnum] = UNSET @@ -61,12 +64,15 @@ class AModel: attr_1_leading_digit: Union[Unset, str] = UNSET not_required_nullable: Union[Unset, None, 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[Unset, None, ModelWithUnionProperty] = 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[Unset, None, "ModelWithUnionProperty"] = UNSET 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 @@ -218,6 +224,9 @@ def to_dict(self) -> Dict[str, Any]: @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")) @@ -244,7 +253,7 @@ def _parse_a_camel_date_time(data: object) -> Union[datetime.date, datetime.date required_not_nullable = d.pop("required_not_nullable") - def _parse_one_of_models(data: object) -> Union[Any, FreeFormModel, ModelWithUnionProperty]: + def _parse_one_of_models(data: object) -> Union["FreeFormModel", "ModelWithUnionProperty", Any]: try: if not isinstance(data, dict): raise TypeError() @@ -261,7 +270,7 @@ def _parse_one_of_models(data: object) -> Union[Any, FreeFormModel, ModelWithUni return one_of_models_type_1 except: # noqa: E722 pass - return cast(Union[Any, FreeFormModel, ModelWithUnionProperty], data) + return cast(Union["FreeFormModel", "ModelWithUnionProperty", Any], data) one_of_models = _parse_one_of_models(d.pop("one_of_models")) @@ -310,7 +319,7 @@ def _parse_one_of_models(data: object) -> Union[Any, FreeFormModel, ModelWithUni not_required_not_nullable = d.pop("not_required_not_nullable", UNSET) - def _parse_nullable_one_of_models(data: object) -> Union[FreeFormModel, ModelWithUnionProperty, None]: + def _parse_nullable_one_of_models(data: object) -> Union["FreeFormModel", "ModelWithUnionProperty", None]: if data is None: return data try: @@ -329,7 +338,7 @@ def _parse_nullable_one_of_models(data: object) -> Union[FreeFormModel, ModelWit nullable_one_of_models = _parse_nullable_one_of_models(d.pop("nullable_one_of_models")) - def _parse_not_required_one_of_models(data: object) -> Union[FreeFormModel, ModelWithUnionProperty, Unset]: + def _parse_not_required_one_of_models(data: object) -> Union["FreeFormModel", "ModelWithUnionProperty", Unset]: if isinstance(data, Unset): return data try: @@ -360,7 +369,7 @@ def _parse_not_required_one_of_models(data: object) -> Union[FreeFormModel, Mode def _parse_not_required_nullable_one_of_models( data: object, - ) -> Union[FreeFormModel, ModelWithUnionProperty, None, Unset, str]: + ) -> Union["FreeFormModel", "ModelWithUnionProperty", None, Unset, str]: if data is None: return data if isinstance(data, Unset): @@ -395,7 +404,7 @@ def _parse_not_required_nullable_one_of_models( return not_required_nullable_one_of_models_type_1 except: # noqa: E722 pass - return cast(Union[FreeFormModel, ModelWithUnionProperty, None, Unset, str], data) + 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) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_a_item.py b/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_a_item.py new file mode 100644 index 000000000..16fcbb88d --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_a_item.py @@ -0,0 +1,82 @@ +from typing import TYPE_CHECKING, Any, Dict, List, Type, TypeVar, Union + +import attr + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.an_array_with_a_circular_ref_in_items_object_b_item import AnArrayWithACircularRefInItemsObjectBItem + + +T = TypeVar("T", bound="AnArrayWithACircularRefInItemsObjectAItem") + + +@attr.s(auto_attribs=True) +class AnArrayWithACircularRefInItemsObjectAItem: + """ + Attributes: + circular (Union[Unset, List['AnArrayWithACircularRefInItemsObjectBItem']]): + """ + + circular: Union[Unset, List["AnArrayWithACircularRefInItemsObjectBItem"]] = UNSET + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + circular: Union[Unset, List[Dict[str, Any]]] = UNSET + if not isinstance(self.circular, Unset): + circular = [] + for componentsschemas_an_array_with_a_circular_ref_in_items_object_b_item_data in self.circular: + componentsschemas_an_array_with_a_circular_ref_in_items_object_b_item = ( + componentsschemas_an_array_with_a_circular_ref_in_items_object_b_item_data.to_dict() + ) + + circular.append(componentsschemas_an_array_with_a_circular_ref_in_items_object_b_item) + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if circular is not UNSET: + field_dict["circular"] = circular + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + from ..models.an_array_with_a_circular_ref_in_items_object_b_item import ( + AnArrayWithACircularRefInItemsObjectBItem, + ) + + d = src_dict.copy() + circular = [] + _circular = d.pop("circular", UNSET) + for componentsschemas_an_array_with_a_circular_ref_in_items_object_b_item_data in _circular or []: + componentsschemas_an_array_with_a_circular_ref_in_items_object_b_item = ( + AnArrayWithACircularRefInItemsObjectBItem.from_dict( + componentsschemas_an_array_with_a_circular_ref_in_items_object_b_item_data + ) + ) + + circular.append(componentsschemas_an_array_with_a_circular_ref_in_items_object_b_item) + + an_array_with_a_circular_ref_in_items_object_a_item = cls( + circular=circular, + ) + + an_array_with_a_circular_ref_in_items_object_a_item.additional_properties = d + return an_array_with_a_circular_ref_in_items_object_a_item + + @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/an_array_with_a_circular_ref_in_items_object_additional_properties_a_item.py b/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_additional_properties_a_item.py new file mode 100644 index 000000000..f03a87604 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_additional_properties_a_item.py @@ -0,0 +1,92 @@ +from typing import TYPE_CHECKING, Any, Dict, List, Type, TypeVar + +import attr + +if TYPE_CHECKING: + from ..models.an_array_with_a_circular_ref_in_items_object_additional_properties_b_item import ( + AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem, + ) + + +T = TypeVar("T", bound="AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem") + + +@attr.s(auto_attribs=True) +class AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem: + """ """ + + additional_properties: Dict[str, List["AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem"]] = attr.ib( + init=False, factory=dict + ) + + def to_dict(self) -> Dict[str, Any]: + pass + + field_dict: Dict[str, Any] = {} + for prop_name, prop in self.additional_properties.items(): + field_dict[prop_name] = [] + for ( + componentsschemas_an_array_with_a_circular_ref_in_items_object_additional_properties_b_item_data + ) in prop: + componentsschemas_an_array_with_a_circular_ref_in_items_object_additional_properties_b_item = ( + componentsschemas_an_array_with_a_circular_ref_in_items_object_additional_properties_b_item_data.to_dict() + ) + + field_dict[prop_name].append( + componentsschemas_an_array_with_a_circular_ref_in_items_object_additional_properties_b_item + ) + + field_dict.update({}) + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + from ..models.an_array_with_a_circular_ref_in_items_object_additional_properties_b_item import ( + AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem, + ) + + d = src_dict.copy() + an_array_with_a_circular_ref_in_items_object_additional_properties_a_item = cls() + + additional_properties = {} + for prop_name, prop_dict in d.items(): + additional_property = [] + _additional_property = prop_dict + for ( + componentsschemas_an_array_with_a_circular_ref_in_items_object_additional_properties_b_item_data + ) in _additional_property: + componentsschemas_an_array_with_a_circular_ref_in_items_object_additional_properties_b_item = ( + AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem.from_dict( + componentsschemas_an_array_with_a_circular_ref_in_items_object_additional_properties_b_item_data + ) + ) + + additional_property.append( + componentsschemas_an_array_with_a_circular_ref_in_items_object_additional_properties_b_item + ) + + additional_properties[prop_name] = additional_property + + an_array_with_a_circular_ref_in_items_object_additional_properties_a_item.additional_properties = ( + additional_properties + ) + return an_array_with_a_circular_ref_in_items_object_additional_properties_a_item + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> List["AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem"]: + return self.additional_properties[key] + + def __setitem__( + self, key: str, value: List["AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem"] + ) -> 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/an_array_with_a_circular_ref_in_items_object_additional_properties_b_item.py b/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_additional_properties_b_item.py new file mode 100644 index 000000000..cdff09b4b --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_additional_properties_b_item.py @@ -0,0 +1,92 @@ +from typing import TYPE_CHECKING, Any, Dict, List, Type, TypeVar + +import attr + +if TYPE_CHECKING: + from ..models.an_array_with_a_circular_ref_in_items_object_additional_properties_a_item import ( + AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem, + ) + + +T = TypeVar("T", bound="AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem") + + +@attr.s(auto_attribs=True) +class AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem: + """ """ + + additional_properties: Dict[str, List["AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem"]] = attr.ib( + init=False, factory=dict + ) + + def to_dict(self) -> Dict[str, Any]: + pass + + field_dict: Dict[str, Any] = {} + for prop_name, prop in self.additional_properties.items(): + field_dict[prop_name] = [] + for ( + componentsschemas_an_array_with_a_circular_ref_in_items_object_additional_properties_a_item_data + ) in prop: + componentsschemas_an_array_with_a_circular_ref_in_items_object_additional_properties_a_item = ( + componentsschemas_an_array_with_a_circular_ref_in_items_object_additional_properties_a_item_data.to_dict() + ) + + field_dict[prop_name].append( + componentsschemas_an_array_with_a_circular_ref_in_items_object_additional_properties_a_item + ) + + field_dict.update({}) + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + from ..models.an_array_with_a_circular_ref_in_items_object_additional_properties_a_item import ( + AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem, + ) + + d = src_dict.copy() + an_array_with_a_circular_ref_in_items_object_additional_properties_b_item = cls() + + additional_properties = {} + for prop_name, prop_dict in d.items(): + additional_property = [] + _additional_property = prop_dict + for ( + componentsschemas_an_array_with_a_circular_ref_in_items_object_additional_properties_a_item_data + ) in _additional_property: + componentsschemas_an_array_with_a_circular_ref_in_items_object_additional_properties_a_item = ( + AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem.from_dict( + componentsschemas_an_array_with_a_circular_ref_in_items_object_additional_properties_a_item_data + ) + ) + + additional_property.append( + componentsschemas_an_array_with_a_circular_ref_in_items_object_additional_properties_a_item + ) + + additional_properties[prop_name] = additional_property + + an_array_with_a_circular_ref_in_items_object_additional_properties_b_item.additional_properties = ( + additional_properties + ) + return an_array_with_a_circular_ref_in_items_object_additional_properties_b_item + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> List["AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem"]: + return self.additional_properties[key] + + def __setitem__( + self, key: str, value: List["AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem"] + ) -> 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/an_array_with_a_circular_ref_in_items_object_b_item.py b/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_b_item.py new file mode 100644 index 000000000..b15bb0f7b --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_b_item.py @@ -0,0 +1,82 @@ +from typing import TYPE_CHECKING, Any, Dict, List, Type, TypeVar, Union + +import attr + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.an_array_with_a_circular_ref_in_items_object_a_item import AnArrayWithACircularRefInItemsObjectAItem + + +T = TypeVar("T", bound="AnArrayWithACircularRefInItemsObjectBItem") + + +@attr.s(auto_attribs=True) +class AnArrayWithACircularRefInItemsObjectBItem: + """ + Attributes: + circular (Union[Unset, List['AnArrayWithACircularRefInItemsObjectAItem']]): + """ + + circular: Union[Unset, List["AnArrayWithACircularRefInItemsObjectAItem"]] = UNSET + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + circular: Union[Unset, List[Dict[str, Any]]] = UNSET + if not isinstance(self.circular, Unset): + circular = [] + for componentsschemas_an_array_with_a_circular_ref_in_items_object_a_item_data in self.circular: + componentsschemas_an_array_with_a_circular_ref_in_items_object_a_item = ( + componentsschemas_an_array_with_a_circular_ref_in_items_object_a_item_data.to_dict() + ) + + circular.append(componentsschemas_an_array_with_a_circular_ref_in_items_object_a_item) + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if circular is not UNSET: + field_dict["circular"] = circular + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + from ..models.an_array_with_a_circular_ref_in_items_object_a_item import ( + AnArrayWithACircularRefInItemsObjectAItem, + ) + + d = src_dict.copy() + circular = [] + _circular = d.pop("circular", UNSET) + for componentsschemas_an_array_with_a_circular_ref_in_items_object_a_item_data in _circular or []: + componentsschemas_an_array_with_a_circular_ref_in_items_object_a_item = ( + AnArrayWithACircularRefInItemsObjectAItem.from_dict( + componentsschemas_an_array_with_a_circular_ref_in_items_object_a_item_data + ) + ) + + circular.append(componentsschemas_an_array_with_a_circular_ref_in_items_object_a_item) + + an_array_with_a_circular_ref_in_items_object_b_item = cls( + circular=circular, + ) + + an_array_with_a_circular_ref_in_items_object_b_item.additional_properties = d + return an_array_with_a_circular_ref_in_items_object_b_item + + @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/an_array_with_a_recursive_ref_in_items_object_additional_properties_item.py b/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_additional_properties_item.py new file mode 100644 index 000000000..52341b8bc --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_additional_properties_item.py @@ -0,0 +1,79 @@ +from typing import Any, Dict, List, Type, TypeVar + +import attr + +T = TypeVar("T", bound="AnArrayWithARecursiveRefInItemsObjectAdditionalPropertiesItem") + + +@attr.s(auto_attribs=True) +class AnArrayWithARecursiveRefInItemsObjectAdditionalPropertiesItem: + """ """ + + additional_properties: Dict[str, List["AnArrayWithARecursiveRefInItemsObjectAdditionalPropertiesItem"]] = attr.ib( + init=False, factory=dict + ) + + def to_dict(self) -> Dict[str, Any]: + + field_dict: Dict[str, Any] = {} + for prop_name, prop in self.additional_properties.items(): + field_dict[prop_name] = [] + for componentsschemas_an_array_with_a_recursive_ref_in_items_object_additional_properties_item_data in prop: + componentsschemas_an_array_with_a_recursive_ref_in_items_object_additional_properties_item = ( + componentsschemas_an_array_with_a_recursive_ref_in_items_object_additional_properties_item_data.to_dict() + ) + + field_dict[prop_name].append( + componentsschemas_an_array_with_a_recursive_ref_in_items_object_additional_properties_item + ) + + field_dict.update({}) + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + d = src_dict.copy() + an_array_with_a_recursive_ref_in_items_object_additional_properties_item = cls() + + additional_properties = {} + for prop_name, prop_dict in d.items(): + additional_property = [] + _additional_property = prop_dict + for ( + componentsschemas_an_array_with_a_recursive_ref_in_items_object_additional_properties_item_data + ) in _additional_property: + componentsschemas_an_array_with_a_recursive_ref_in_items_object_additional_properties_item = ( + AnArrayWithARecursiveRefInItemsObjectAdditionalPropertiesItem.from_dict( + componentsschemas_an_array_with_a_recursive_ref_in_items_object_additional_properties_item_data + ) + ) + + additional_property.append( + componentsschemas_an_array_with_a_recursive_ref_in_items_object_additional_properties_item + ) + + additional_properties[prop_name] = additional_property + + an_array_with_a_recursive_ref_in_items_object_additional_properties_item.additional_properties = ( + additional_properties + ) + return an_array_with_a_recursive_ref_in_items_object_additional_properties_item + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> List["AnArrayWithARecursiveRefInItemsObjectAdditionalPropertiesItem"]: + return self.additional_properties[key] + + def __setitem__( + self, key: str, value: List["AnArrayWithARecursiveRefInItemsObjectAdditionalPropertiesItem"] + ) -> 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/an_array_with_a_recursive_ref_in_items_object_item.py b/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_item.py new file mode 100644 index 000000000..c14ee07c2 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_item.py @@ -0,0 +1,74 @@ +from typing import Any, Dict, List, Type, TypeVar, Union + +import attr + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="AnArrayWithARecursiveRefInItemsObjectItem") + + +@attr.s(auto_attribs=True) +class AnArrayWithARecursiveRefInItemsObjectItem: + """ + Attributes: + recursive (Union[Unset, List['AnArrayWithARecursiveRefInItemsObjectItem']]): + """ + + recursive: Union[Unset, List["AnArrayWithARecursiveRefInItemsObjectItem"]] = UNSET + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + recursive: Union[Unset, List[Dict[str, Any]]] = UNSET + if not isinstance(self.recursive, Unset): + recursive = [] + for componentsschemas_an_array_with_a_recursive_ref_in_items_object_item_data in self.recursive: + componentsschemas_an_array_with_a_recursive_ref_in_items_object_item = ( + componentsschemas_an_array_with_a_recursive_ref_in_items_object_item_data.to_dict() + ) + + recursive.append(componentsschemas_an_array_with_a_recursive_ref_in_items_object_item) + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if recursive is not UNSET: + field_dict["recursive"] = recursive + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + d = src_dict.copy() + recursive = [] + _recursive = d.pop("recursive", UNSET) + for componentsschemas_an_array_with_a_recursive_ref_in_items_object_item_data in _recursive or []: + componentsschemas_an_array_with_a_recursive_ref_in_items_object_item = ( + AnArrayWithARecursiveRefInItemsObjectItem.from_dict( + componentsschemas_an_array_with_a_recursive_ref_in_items_object_item_data + ) + ) + + recursive.append(componentsschemas_an_array_with_a_recursive_ref_in_items_object_item) + + an_array_with_a_recursive_ref_in_items_object_item = cls( + recursive=recursive, + ) + + an_array_with_a_recursive_ref_in_items_object_item.additional_properties = d + return an_array_with_a_recursive_ref_in_items_object_item + + @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/body_upload_file_tests_upload_post.py b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py index 4863298fc..d858be5b6 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py @@ -1,24 +1,27 @@ import datetime import json from io import BytesIO -from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar, Union, cast +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, TypeVar, Union, cast import attr from dateutil.parser import isoparse -from ..models.body_upload_file_tests_upload_post_additional_property import ( - BodyUploadFileTestsUploadPostAdditionalProperty, -) -from ..models.body_upload_file_tests_upload_post_some_nullable_object import ( - BodyUploadFileTestsUploadPostSomeNullableObject, -) -from ..models.body_upload_file_tests_upload_post_some_object import BodyUploadFileTestsUploadPostSomeObject -from ..models.body_upload_file_tests_upload_post_some_optional_object import ( - BodyUploadFileTestsUploadPostSomeOptionalObject, -) from ..models.different_enum import DifferentEnum from ..types import UNSET, File, FileJsonType, Unset +if TYPE_CHECKING: + from ..models.body_upload_file_tests_upload_post_additional_property import ( + BodyUploadFileTestsUploadPostAdditionalProperty, + ) + from ..models.body_upload_file_tests_upload_post_some_nullable_object import ( + BodyUploadFileTestsUploadPostSomeNullableObject, + ) + from ..models.body_upload_file_tests_upload_post_some_object import BodyUploadFileTestsUploadPostSomeObject + from ..models.body_upload_file_tests_upload_post_some_optional_object import ( + BodyUploadFileTestsUploadPostSomeOptionalObject, + ) + + T = TypeVar("T", bound="BodyUploadFileTestsUploadPost") @@ -40,17 +43,17 @@ class BodyUploadFileTestsUploadPost: """ some_file: File - some_object: BodyUploadFileTestsUploadPostSomeObject - some_nullable_object: Optional[BodyUploadFileTestsUploadPostSomeNullableObject] + some_object: "BodyUploadFileTestsUploadPostSomeObject" + some_nullable_object: Optional["BodyUploadFileTestsUploadPostSomeNullableObject"] some_optional_file: Union[Unset, File] = UNSET some_string: Union[Unset, str] = "some_default_string" a_datetime: Union[Unset, datetime.datetime] = UNSET a_date: Union[Unset, datetime.date] = UNSET some_number: Union[Unset, float] = UNSET some_array: Union[Unset, List[float]] = UNSET - some_optional_object: Union[Unset, BodyUploadFileTestsUploadPostSomeOptionalObject] = UNSET + some_optional_object: Union[Unset, "BodyUploadFileTestsUploadPostSomeOptionalObject"] = UNSET some_enum: Union[Unset, DifferentEnum] = UNSET - additional_properties: Dict[str, BodyUploadFileTestsUploadPostAdditionalProperty] = attr.ib( + additional_properties: Dict[str, "BodyUploadFileTestsUploadPostAdditionalProperty"] = attr.ib( init=False, factory=dict ) @@ -195,6 +198,17 @@ def to_multipart(self) -> Dict[str, Any]: @classmethod def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + from ..models.body_upload_file_tests_upload_post_additional_property import ( + BodyUploadFileTestsUploadPostAdditionalProperty, + ) + from ..models.body_upload_file_tests_upload_post_some_nullable_object import ( + BodyUploadFileTestsUploadPostSomeNullableObject, + ) + from ..models.body_upload_file_tests_upload_post_some_object import BodyUploadFileTestsUploadPostSomeObject + from ..models.body_upload_file_tests_upload_post_some_optional_object import ( + BodyUploadFileTestsUploadPostSomeOptionalObject, + ) + d = src_dict.copy() some_file = File(payload=BytesIO(d.pop("some_file"))) @@ -275,10 +289,10 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: def additional_keys(self) -> List[str]: return list(self.additional_properties.keys()) - def __getitem__(self, key: str) -> BodyUploadFileTestsUploadPostAdditionalProperty: + def __getitem__(self, key: str) -> "BodyUploadFileTestsUploadPostAdditionalProperty": return self.additional_properties[key] - def __setitem__(self, key: str, value: BodyUploadFileTestsUploadPostAdditionalProperty) -> None: + def __setitem__(self, key: str, value: "BodyUploadFileTestsUploadPostAdditionalProperty") -> None: self.additional_properties[key] = value def __delitem__(self, key: str) -> None: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py b/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py index 21855e7e5..4d8e71670 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py @@ -1,10 +1,13 @@ -from typing import Any, Dict, List, Type, TypeVar, Union +from typing import TYPE_CHECKING, Any, Dict, List, Type, TypeVar, Union import attr -from ..models.validation_error import ValidationError from ..types import UNSET, Unset +if TYPE_CHECKING: + from ..models.validation_error import ValidationError + + T = TypeVar("T", bound="HTTPValidationError") @@ -12,10 +15,10 @@ class HTTPValidationError: """ Attributes: - detail (Union[Unset, List[ValidationError]]): + detail (Union[Unset, List['ValidationError']]): """ - detail: Union[Unset, List[ValidationError]] = UNSET + detail: Union[Unset, List["ValidationError"]] = UNSET def to_dict(self) -> Dict[str, Any]: detail: Union[Unset, List[Dict[str, Any]]] = UNSET @@ -35,6 +38,8 @@ def to_dict(self) -> Dict[str, Any]: @classmethod def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + from ..models.validation_error import ValidationError + d = src_dict.copy() detail = [] _detail = d.pop("detail", UNSET) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_inlined.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_inlined.py index 6e3faebf4..fb8ad21a8 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_inlined.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_inlined.py @@ -1,12 +1,15 @@ -from typing import Any, Dict, List, Type, TypeVar, Union +from typing import TYPE_CHECKING, Any, Dict, List, Type, TypeVar, Union import attr -from ..models.model_with_additional_properties_inlined_additional_property import ( - ModelWithAdditionalPropertiesInlinedAdditionalProperty, -) from ..types import UNSET, Unset +if TYPE_CHECKING: + from ..models.model_with_additional_properties_inlined_additional_property import ( + ModelWithAdditionalPropertiesInlinedAdditionalProperty, + ) + + T = TypeVar("T", bound="ModelWithAdditionalPropertiesInlined") @@ -18,7 +21,7 @@ class ModelWithAdditionalPropertiesInlined: """ a_number: Union[Unset, float] = UNSET - additional_properties: Dict[str, ModelWithAdditionalPropertiesInlinedAdditionalProperty] = attr.ib( + additional_properties: Dict[str, "ModelWithAdditionalPropertiesInlinedAdditionalProperty"] = attr.ib( init=False, factory=dict ) @@ -37,6 +40,10 @@ def to_dict(self) -> Dict[str, Any]: @classmethod def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + from ..models.model_with_additional_properties_inlined_additional_property import ( + ModelWithAdditionalPropertiesInlinedAdditionalProperty, + ) + d = src_dict.copy() a_number = d.pop("a_number", UNSET) @@ -57,10 +64,10 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: def additional_keys(self) -> List[str]: return list(self.additional_properties.keys()) - def __getitem__(self, key: str) -> ModelWithAdditionalPropertiesInlinedAdditionalProperty: + def __getitem__(self, key: str) -> "ModelWithAdditionalPropertiesInlinedAdditionalProperty": return self.additional_properties[key] - def __setitem__(self, key: str, value: ModelWithAdditionalPropertiesInlinedAdditionalProperty) -> None: + def __setitem__(self, key: str, value: "ModelWithAdditionalPropertiesInlinedAdditionalProperty") -> None: self.additional_properties[key] = value def __delitem__(self, key: str) -> None: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties.py index af82eb24f..d28dbb30e 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties.py @@ -1,10 +1,12 @@ -from typing import Any, Dict, List, Type, TypeVar, Union, cast +from typing import TYPE_CHECKING, Any, Dict, List, Type, TypeVar, Union, cast import attr -from ..models.model_with_any_json_properties_additional_property_type_0 import ( - ModelWithAnyJsonPropertiesAdditionalPropertyType0, -) +if TYPE_CHECKING: + from ..models.model_with_any_json_properties_additional_property_type_0 import ( + ModelWithAnyJsonPropertiesAdditionalPropertyType0, + ) + T = TypeVar("T", bound="ModelWithAnyJsonProperties") @@ -14,10 +16,13 @@ class ModelWithAnyJsonProperties: """ """ additional_properties: Dict[ - str, Union[List[str], ModelWithAnyJsonPropertiesAdditionalPropertyType0, bool, float, int, str] + str, Union["ModelWithAnyJsonPropertiesAdditionalPropertyType0", List[str], bool, float, int, str] ] = attr.ib(init=False, factory=dict) def to_dict(self) -> Dict[str, Any]: + from ..models.model_with_any_json_properties_additional_property_type_0 import ( + ModelWithAnyJsonPropertiesAdditionalPropertyType0, + ) field_dict: Dict[str, Any] = {} for prop_name, prop in self.additional_properties.items(): @@ -37,6 +42,10 @@ def to_dict(self) -> Dict[str, Any]: @classmethod def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + from ..models.model_with_any_json_properties_additional_property_type_0 import ( + ModelWithAnyJsonPropertiesAdditionalPropertyType0, + ) + d = src_dict.copy() model_with_any_json_properties = cls() @@ -45,7 +54,7 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: def _parse_additional_property( data: object, - ) -> Union[List[str], ModelWithAnyJsonPropertiesAdditionalPropertyType0, bool, float, int, str]: + ) -> Union["ModelWithAnyJsonPropertiesAdditionalPropertyType0", List[str], bool, float, int, str]: try: if not isinstance(data, dict): raise TypeError() @@ -63,7 +72,7 @@ def _parse_additional_property( except: # noqa: E722 pass return cast( - Union[List[str], ModelWithAnyJsonPropertiesAdditionalPropertyType0, bool, float, int, str], data + Union["ModelWithAnyJsonPropertiesAdditionalPropertyType0", List[str], bool, float, int, str], data ) additional_property = _parse_additional_property(prop_dict) @@ -79,13 +88,13 @@ def additional_keys(self) -> List[str]: def __getitem__( self, key: str - ) -> Union[List[str], ModelWithAnyJsonPropertiesAdditionalPropertyType0, bool, float, int, str]: + ) -> Union["ModelWithAnyJsonPropertiesAdditionalPropertyType0", List[str], bool, float, int, str]: return self.additional_properties[key] def __setitem__( self, key: str, - value: Union[List[str], ModelWithAnyJsonPropertiesAdditionalPropertyType0, bool, float, int, str], + value: Union["ModelWithAnyJsonPropertiesAdditionalPropertyType0", List[str], bool, float, int, str], ) -> None: self.additional_properties[key] = value diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_a.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_a.py new file mode 100644 index 000000000..11e31983d --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_a.py @@ -0,0 +1,70 @@ +from typing import TYPE_CHECKING, Any, Dict, List, Type, TypeVar, Union + +import attr + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.model_with_circular_ref_b import ModelWithCircularRefB + + +T = TypeVar("T", bound="ModelWithCircularRefA") + + +@attr.s(auto_attribs=True) +class ModelWithCircularRefA: + """ + Attributes: + circular (Union[Unset, ModelWithCircularRefB]): + """ + + circular: Union[Unset, "ModelWithCircularRefB"] = UNSET + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + circular: Union[Unset, Dict[str, Any]] = UNSET + if not isinstance(self.circular, Unset): + circular = self.circular.to_dict() + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if circular is not UNSET: + field_dict["circular"] = circular + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + from ..models.model_with_circular_ref_b import ModelWithCircularRefB + + d = src_dict.copy() + _circular = d.pop("circular", UNSET) + circular: Union[Unset, ModelWithCircularRefB] + if isinstance(_circular, Unset): + circular = UNSET + else: + circular = ModelWithCircularRefB.from_dict(_circular) + + model_with_circular_ref_a = cls( + circular=circular, + ) + + model_with_circular_ref_a.additional_properties = d + return model_with_circular_ref_a + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_b.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_b.py new file mode 100644 index 000000000..5fb34f4f4 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_b.py @@ -0,0 +1,70 @@ +from typing import TYPE_CHECKING, Any, Dict, List, Type, TypeVar, Union + +import attr + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.model_with_circular_ref_a import ModelWithCircularRefA + + +T = TypeVar("T", bound="ModelWithCircularRefB") + + +@attr.s(auto_attribs=True) +class ModelWithCircularRefB: + """ + Attributes: + circular (Union[Unset, ModelWithCircularRefA]): + """ + + circular: Union[Unset, "ModelWithCircularRefA"] = UNSET + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + circular: Union[Unset, Dict[str, Any]] = UNSET + if not isinstance(self.circular, Unset): + circular = self.circular.to_dict() + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if circular is not UNSET: + field_dict["circular"] = circular + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + from ..models.model_with_circular_ref_a import ModelWithCircularRefA + + d = src_dict.copy() + _circular = d.pop("circular", UNSET) + circular: Union[Unset, ModelWithCircularRefA] + if isinstance(_circular, Unset): + circular = UNSET + else: + circular = ModelWithCircularRefA.from_dict(_circular) + + model_with_circular_ref_b = cls( + circular=circular, + ) + + model_with_circular_ref_b.additional_properties = d + return model_with_circular_ref_b + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_in_additional_properties_a.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_in_additional_properties_a.py new file mode 100644 index 000000000..0d9e0155c --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_in_additional_properties_a.py @@ -0,0 +1,61 @@ +from typing import TYPE_CHECKING, Any, Dict, List, Type, TypeVar + +import attr + +if TYPE_CHECKING: + from ..models.model_with_circular_ref_in_additional_properties_b import ModelWithCircularRefInAdditionalPropertiesB + + +T = TypeVar("T", bound="ModelWithCircularRefInAdditionalPropertiesA") + + +@attr.s(auto_attribs=True) +class ModelWithCircularRefInAdditionalPropertiesA: + """ """ + + additional_properties: Dict[str, "ModelWithCircularRefInAdditionalPropertiesB"] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + pass + + field_dict: Dict[str, Any] = {} + for prop_name, prop in self.additional_properties.items(): + field_dict[prop_name] = prop.to_dict() + + field_dict.update({}) + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + from ..models.model_with_circular_ref_in_additional_properties_b import ( + ModelWithCircularRefInAdditionalPropertiesB, + ) + + d = src_dict.copy() + model_with_circular_ref_in_additional_properties_a = cls() + + additional_properties = {} + for prop_name, prop_dict in d.items(): + additional_property = ModelWithCircularRefInAdditionalPropertiesB.from_dict(prop_dict) + + additional_properties[prop_name] = additional_property + + model_with_circular_ref_in_additional_properties_a.additional_properties = additional_properties + return model_with_circular_ref_in_additional_properties_a + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> "ModelWithCircularRefInAdditionalPropertiesB": + return self.additional_properties[key] + + def __setitem__(self, key: str, value: "ModelWithCircularRefInAdditionalPropertiesB") -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_in_additional_properties_b.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_in_additional_properties_b.py new file mode 100644 index 000000000..0583b40f8 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_in_additional_properties_b.py @@ -0,0 +1,61 @@ +from typing import TYPE_CHECKING, Any, Dict, List, Type, TypeVar + +import attr + +if TYPE_CHECKING: + from ..models.model_with_circular_ref_in_additional_properties_a import ModelWithCircularRefInAdditionalPropertiesA + + +T = TypeVar("T", bound="ModelWithCircularRefInAdditionalPropertiesB") + + +@attr.s(auto_attribs=True) +class ModelWithCircularRefInAdditionalPropertiesB: + """ """ + + additional_properties: Dict[str, "ModelWithCircularRefInAdditionalPropertiesA"] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + pass + + field_dict: Dict[str, Any] = {} + for prop_name, prop in self.additional_properties.items(): + field_dict[prop_name] = prop.to_dict() + + field_dict.update({}) + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + from ..models.model_with_circular_ref_in_additional_properties_a import ( + ModelWithCircularRefInAdditionalPropertiesA, + ) + + d = src_dict.copy() + model_with_circular_ref_in_additional_properties_b = cls() + + additional_properties = {} + for prop_name, prop_dict in d.items(): + additional_property = ModelWithCircularRefInAdditionalPropertiesA.from_dict(prop_dict) + + additional_properties[prop_name] = additional_property + + model_with_circular_ref_in_additional_properties_b.additional_properties = additional_properties + return model_with_circular_ref_in_additional_properties_b + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> "ModelWithCircularRefInAdditionalPropertiesA": + return self.additional_properties[key] + + def __setitem__(self, key: str, value: "ModelWithCircularRefInAdditionalPropertiesA") -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_primitive_additional_properties.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_primitive_additional_properties.py index 40d384759..89144cfec 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_primitive_additional_properties.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_primitive_additional_properties.py @@ -1,12 +1,15 @@ -from typing import Any, Dict, List, Type, TypeVar, Union +from typing import TYPE_CHECKING, Any, Dict, List, Type, TypeVar, Union import attr -from ..models.model_with_primitive_additional_properties_a_date_holder import ( - ModelWithPrimitiveAdditionalPropertiesADateHolder, -) from ..types import UNSET, Unset +if TYPE_CHECKING: + from ..models.model_with_primitive_additional_properties_a_date_holder import ( + ModelWithPrimitiveAdditionalPropertiesADateHolder, + ) + + T = TypeVar("T", bound="ModelWithPrimitiveAdditionalProperties") @@ -17,7 +20,7 @@ class ModelWithPrimitiveAdditionalProperties: a_date_holder (Union[Unset, ModelWithPrimitiveAdditionalPropertiesADateHolder]): """ - a_date_holder: Union[Unset, ModelWithPrimitiveAdditionalPropertiesADateHolder] = UNSET + a_date_holder: Union[Unset, "ModelWithPrimitiveAdditionalPropertiesADateHolder"] = UNSET additional_properties: Dict[str, str] = attr.ib(init=False, factory=dict) def to_dict(self) -> Dict[str, Any]: @@ -35,6 +38,10 @@ def to_dict(self) -> Dict[str, Any]: @classmethod def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + from ..models.model_with_primitive_additional_properties_a_date_holder import ( + ModelWithPrimitiveAdditionalPropertiesADateHolder, + ) + d = src_dict.copy() _a_date_holder = d.pop("a_date_holder", UNSET) a_date_holder: Union[Unset, ModelWithPrimitiveAdditionalPropertiesADateHolder] diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_property_ref.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_property_ref.py index a3713efe2..d1b8b2b11 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_property_ref.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_property_ref.py @@ -1,10 +1,13 @@ -from typing import Any, Dict, List, Type, TypeVar, Union +from typing import TYPE_CHECKING, Any, Dict, List, Type, TypeVar, Union import attr -from ..models.model_name import ModelName from ..types import UNSET, Unset +if TYPE_CHECKING: + from ..models.model_name import ModelName + + T = TypeVar("T", bound="ModelWithPropertyRef") @@ -15,7 +18,7 @@ class ModelWithPropertyRef: inner (Union[Unset, ModelName]): """ - inner: Union[Unset, ModelName] = UNSET + inner: Union[Unset, "ModelName"] = UNSET additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) def to_dict(self) -> Dict[str, Any]: @@ -33,6 +36,8 @@ def to_dict(self) -> Dict[str, Any]: @classmethod def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + from ..models.model_name import ModelName + d = src_dict.copy() _inner = d.pop("inner", UNSET) inner: Union[Unset, ModelName] diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_recursive_ref.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_recursive_ref.py new file mode 100644 index 000000000..b60e5a100 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_recursive_ref.py @@ -0,0 +1,64 @@ +from typing import Any, Dict, List, Type, TypeVar, Union + +import attr + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="ModelWithRecursiveRef") + + +@attr.s(auto_attribs=True) +class ModelWithRecursiveRef: + """ + Attributes: + recursive (Union[Unset, ModelWithRecursiveRef]): + """ + + recursive: Union[Unset, "ModelWithRecursiveRef"] = UNSET + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + recursive: Union[Unset, Dict[str, Any]] = UNSET + if not isinstance(self.recursive, Unset): + recursive = self.recursive.to_dict() + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if recursive is not UNSET: + field_dict["recursive"] = recursive + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + d = src_dict.copy() + _recursive = d.pop("recursive", UNSET) + recursive: Union[Unset, ModelWithRecursiveRef] + if isinstance(_recursive, Unset): + recursive = UNSET + else: + recursive = ModelWithRecursiveRef.from_dict(_recursive) + + model_with_recursive_ref = cls( + recursive=recursive, + ) + + model_with_recursive_ref.additional_properties = d + return model_with_recursive_ref + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_recursive_ref_in_additional_properties.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_recursive_ref_in_additional_properties.py new file mode 100644 index 000000000..64d327ee6 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_recursive_ref_in_additional_properties.py @@ -0,0 +1,52 @@ +from typing import Any, Dict, List, Type, TypeVar + +import attr + +T = TypeVar("T", bound="ModelWithRecursiveRefInAdditionalProperties") + + +@attr.s(auto_attribs=True) +class ModelWithRecursiveRefInAdditionalProperties: + """ """ + + additional_properties: Dict[str, "ModelWithRecursiveRefInAdditionalProperties"] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + + field_dict: Dict[str, Any] = {} + for prop_name, prop in self.additional_properties.items(): + field_dict[prop_name] = prop.to_dict() + + field_dict.update({}) + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + d = src_dict.copy() + model_with_recursive_ref_in_additional_properties = cls() + + additional_properties = {} + for prop_name, prop_dict in d.items(): + additional_property = ModelWithRecursiveRefInAdditionalProperties.from_dict(prop_dict) + + additional_properties[prop_name] = additional_property + + model_with_recursive_ref_in_additional_properties.additional_properties = additional_properties + return model_with_recursive_ref_in_additional_properties + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> "ModelWithRecursiveRefInAdditionalProperties": + return self.additional_properties[key] + + def __setitem__(self, key: str, value: "ModelWithRecursiveRefInAdditionalProperties") -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property_inlined.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property_inlined.py index e89861520..0c6cb6d3a 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property_inlined.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property_inlined.py @@ -1,11 +1,14 @@ -from typing import Any, Dict, Type, TypeVar, Union +from typing import TYPE_CHECKING, Any, Dict, Type, TypeVar, Union import attr -from ..models.model_with_union_property_inlined_fruit_type_0 import ModelWithUnionPropertyInlinedFruitType0 -from ..models.model_with_union_property_inlined_fruit_type_1 import ModelWithUnionPropertyInlinedFruitType1 from ..types import UNSET, Unset +if TYPE_CHECKING: + from ..models.model_with_union_property_inlined_fruit_type_0 import ModelWithUnionPropertyInlinedFruitType0 + from ..models.model_with_union_property_inlined_fruit_type_1 import ModelWithUnionPropertyInlinedFruitType1 + + T = TypeVar("T", bound="ModelWithUnionPropertyInlined") @@ -13,12 +16,14 @@ class ModelWithUnionPropertyInlined: """ Attributes: - fruit (Union[ModelWithUnionPropertyInlinedFruitType0, ModelWithUnionPropertyInlinedFruitType1, Unset]): + fruit (Union['ModelWithUnionPropertyInlinedFruitType0', 'ModelWithUnionPropertyInlinedFruitType1', Unset]): """ - fruit: Union[ModelWithUnionPropertyInlinedFruitType0, ModelWithUnionPropertyInlinedFruitType1, Unset] = UNSET + fruit: Union["ModelWithUnionPropertyInlinedFruitType0", "ModelWithUnionPropertyInlinedFruitType1", Unset] = UNSET def to_dict(self) -> Dict[str, Any]: + from ..models.model_with_union_property_inlined_fruit_type_0 import ModelWithUnionPropertyInlinedFruitType0 + fruit: Union[Dict[str, Any], Unset] if isinstance(self.fruit, Unset): fruit = UNSET @@ -42,11 +47,14 @@ def to_dict(self) -> Dict[str, Any]: @classmethod def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + from ..models.model_with_union_property_inlined_fruit_type_0 import ModelWithUnionPropertyInlinedFruitType0 + from ..models.model_with_union_property_inlined_fruit_type_1 import ModelWithUnionPropertyInlinedFruitType1 + d = src_dict.copy() def _parse_fruit( data: object, - ) -> Union[ModelWithUnionPropertyInlinedFruitType0, ModelWithUnionPropertyInlinedFruitType1, Unset]: + ) -> Union["ModelWithUnionPropertyInlinedFruitType0", "ModelWithUnionPropertyInlinedFruitType1", Unset]: if isinstance(data, Unset): return data try: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200.py b/end_to_end_tests/golden-record/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200.py index 6ce78fcd5..579c4dbd6 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200.py @@ -1,10 +1,12 @@ -from typing import Any, Dict, List, Type, TypeVar, Union, cast +from typing import TYPE_CHECKING, Any, Dict, List, Type, TypeVar, Union, cast import attr -from ..models.post_responses_unions_simple_before_complex_response_200a_type_1 import ( - PostResponsesUnionsSimpleBeforeComplexResponse200AType1, -) +if TYPE_CHECKING: + from ..models.post_responses_unions_simple_before_complex_response_200a_type_1 import ( + PostResponsesUnionsSimpleBeforeComplexResponse200AType1, + ) + T = TypeVar("T", bound="PostResponsesUnionsSimpleBeforeComplexResponse200") @@ -13,13 +15,17 @@ class PostResponsesUnionsSimpleBeforeComplexResponse200: """ Attributes: - a (Union[PostResponsesUnionsSimpleBeforeComplexResponse200AType1, str]): + a (Union['PostResponsesUnionsSimpleBeforeComplexResponse200AType1', str]): """ - a: Union[PostResponsesUnionsSimpleBeforeComplexResponse200AType1, str] + a: Union["PostResponsesUnionsSimpleBeforeComplexResponse200AType1", str] additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) def to_dict(self) -> Dict[str, Any]: + from ..models.post_responses_unions_simple_before_complex_response_200a_type_1 import ( + PostResponsesUnionsSimpleBeforeComplexResponse200AType1, + ) + a: Union[Dict[str, Any], str] if isinstance(self.a, PostResponsesUnionsSimpleBeforeComplexResponse200AType1): @@ -40,9 +46,13 @@ def to_dict(self) -> Dict[str, Any]: @classmethod def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + from ..models.post_responses_unions_simple_before_complex_response_200a_type_1 import ( + PostResponsesUnionsSimpleBeforeComplexResponse200AType1, + ) + d = src_dict.copy() - def _parse_a(data: object) -> Union[PostResponsesUnionsSimpleBeforeComplexResponse200AType1, str]: + def _parse_a(data: object) -> Union["PostResponsesUnionsSimpleBeforeComplexResponse200AType1", str]: try: if not isinstance(data, dict): raise TypeError() @@ -51,7 +61,7 @@ def _parse_a(data: object) -> Union[PostResponsesUnionsSimpleBeforeComplexRespon return a_type_1 except: # noqa: E722 pass - return cast(Union[PostResponsesUnionsSimpleBeforeComplexResponse200AType1, str], data) + return cast(Union["PostResponsesUnionsSimpleBeforeComplexResponse200AType1", str], data) a = _parse_a(d.pop("a")) diff --git a/end_to_end_tests/openapi.json b/end_to_end_tests/openapi.json index 8298670fd..7030eb412 100644 --- a/end_to_end_tests/openapi.json +++ b/end_to_end_tests/openapi.json @@ -2102,6 +2102,108 @@ "model.reference.with.Periods": { "type": "object", "description": "A Model with periods in its reference" + }, + "ModelWithRecursiveRef": { + "type": "object", + "properties": { + "recursive": { + "$ref": "#/components/schemas/ModelWithRecursiveRef" + } + } + }, + "ModelWithCircularRefA": { + "type": "object", + "properties": { + "circular": { + "$ref": "#/components/schemas/ModelWithCircularRefB" + } + } + }, + "ModelWithCircularRefB": { + "type": "object", + "properties": { + "circular": { + "$ref": "#/components/schemas/ModelWithCircularRefA" + } + } + }, + "ModelWithRecursiveRefInAdditionalProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ModelWithRecursiveRefInAdditionalProperties" + } + }, + "ModelWithCircularRefInAdditionalPropertiesA": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ModelWithCircularRefInAdditionalPropertiesB" + } + }, + "ModelWithCircularRefInAdditionalPropertiesB": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ModelWithCircularRefInAdditionalPropertiesA" + } + }, + "AnArrayWithARecursiveRefInItemsObject": { + "type": "array", + "items": { + "type": "object", + "properties": { + "recursive": { + "$ref": "#/components/schemas/AnArrayWithARecursiveRefInItemsObject" + } + } + } + }, + "AnArrayWithACircularRefInItemsObjectA": { + "type": "array", + "items": { + "type": "object", + "properties": { + "circular": { + "$ref": "#/components/schemas/AnArrayWithACircularRefInItemsObjectB" + } + } + } + }, + "AnArrayWithACircularRefInItemsObjectB": { + "type": "array", + "items": { + "type": "object", + "properties": { + "circular": { + "$ref": "#/components/schemas/AnArrayWithACircularRefInItemsObjectA" + } + } + } + }, + "AnArrayWithARecursiveRefInItemsObjectAdditionalProperties": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/AnArrayWithARecursiveRefInItemsObjectAdditionalProperties" + } + } + }, + "AnArrayWithACircularRefInItemsObjectAdditionalPropertiesA": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/AnArrayWithACircularRefInItemsObjectAdditionalPropertiesB" + } + } + }, + "AnArrayWithACircularRefInItemsObjectAdditionalPropertiesB": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/AnArrayWithACircularRefInItemsObjectAdditionalPropertiesA" + } + } } }, "parameters": { diff --git a/integration-tests/integration_tests/models/public_error.py b/integration-tests/integration_tests/models/public_error.py index 49e928b3d..d5281d8ff 100644 --- a/integration-tests/integration_tests/models/public_error.py +++ b/integration-tests/integration_tests/models/public_error.py @@ -1,10 +1,13 @@ -from typing import Any, Dict, List, Type, TypeVar, Union, cast +from typing import TYPE_CHECKING, Any, Dict, List, Type, TypeVar, Union, cast import attr -from ..models.problem import Problem from ..types import UNSET, Unset +if TYPE_CHECKING: + from ..models.problem import Problem + + T = TypeVar("T", bound="PublicError") @@ -14,13 +17,13 @@ class PublicError: Attributes: errors (Union[Unset, List[str]]): extra_parameters (Union[Unset, List[str]]): - invalid_parameters (Union[Unset, List[Problem]]): + invalid_parameters (Union[Unset, List['Problem']]): missing_parameters (Union[Unset, List[str]]): """ errors: Union[Unset, List[str]] = UNSET extra_parameters: Union[Unset, List[str]] = UNSET - invalid_parameters: Union[Unset, List[Problem]] = UNSET + invalid_parameters: Union[Unset, List["Problem"]] = UNSET missing_parameters: Union[Unset, List[str]] = UNSET additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) @@ -61,6 +64,8 @@ def to_dict(self) -> Dict[str, Any]: @classmethod def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + from ..models.problem import Problem + d = src_dict.copy() errors = cast(List[str], d.pop("errors", UNSET)) diff --git a/openapi_python_client/parser/openapi.py b/openapi_python_client/parser/openapi.py index b6c2a5411..7c63cfbaf 100644 --- a/openapi_python_client/parser/openapi.py +++ b/openapi_python_client/parser/openapi.py @@ -99,6 +99,9 @@ def generate_operation_id(*, path: str, method: str) -> str: return f"{method}_{clean_path}" +models_relative_prefix: str = "..." + + # pylint: disable=too-many-instance-attributes @dataclass class Endpoint: @@ -239,15 +242,19 @@ def _add_body( schemas, ) + # No reasons to use lazy imports in endpoints, so add lazy imports to relative here. if form_body is not None: endpoint.form_body = form_body - endpoint.relative_imports.update(endpoint.form_body.get_imports(prefix="...")) + endpoint.relative_imports.update(endpoint.form_body.get_imports(prefix=models_relative_prefix)) + endpoint.relative_imports.update(endpoint.form_body.get_lazy_imports(prefix=models_relative_prefix)) if multipart_body is not None: endpoint.multipart_body = multipart_body - endpoint.relative_imports.update(endpoint.multipart_body.get_imports(prefix="...")) + endpoint.relative_imports.update(endpoint.multipart_body.get_imports(prefix=models_relative_prefix)) + endpoint.relative_imports.update(endpoint.multipart_body.get_lazy_imports(prefix=models_relative_prefix)) if json_body is not None: endpoint.json_body = json_body - endpoint.relative_imports.update(endpoint.json_body.get_imports(prefix="...")) + endpoint.relative_imports.update(endpoint.json_body.get_imports(prefix=models_relative_prefix)) + endpoint.relative_imports.update(endpoint.json_body.get_lazy_imports(prefix=models_relative_prefix)) return endpoint, schemas @staticmethod @@ -287,7 +294,10 @@ def _add_responses( ) ) continue - endpoint.relative_imports |= response.prop.get_imports(prefix="...") + + # No reasons to use lazy imports in endpoints, so add lazy imports to relative here. + endpoint.relative_imports |= response.prop.get_lazy_imports(prefix=models_relative_prefix) + endpoint.relative_imports |= response.prop.get_imports(prefix=models_relative_prefix) endpoint.responses.append(response) return endpoint, schemas @@ -424,7 +434,9 @@ def add_parameters( # There is no NULL for query params, so nullable and not required are the same. prop = attr.evolve(prop, required=False, nullable=True) - endpoint.relative_imports.update(prop.get_imports(prefix="...")) + # No reasons to use lazy imports in endpoints, so add lazy imports to relative here. + endpoint.relative_imports.update(prop.get_lazy_imports(prefix=models_relative_prefix)) + endpoint.relative_imports.update(prop.get_imports(prefix=models_relative_prefix)) endpoint.used_python_identifiers.add(prop.python_name) parameters_by_location[param.param_in][prop.name] = prop @@ -498,11 +510,11 @@ def from_data( def response_type(self) -> str: """Get the Python type of any response from this endpoint""" - types = sorted({response.prop.get_type_string() for response in self.responses}) + types = sorted({response.prop.get_type_string(quoted=False) for response in self.responses}) if len(types) == 0: return "Any" if len(types) == 1: - return self.responses[0].prop.get_type_string() + return self.responses[0].prop.get_type_string(quoted=False) return f"Union[{', '.join(types)}]" def iter_all_parameters(self) -> Iterator[Property]: diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index 3eb678c62..bc22528b3 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -22,11 +22,12 @@ from ..errors import ParameterError, ParseError, PropertyError, ValidationError from .converter import convert, convert_chain from .enum_property import EnumProperty -from .model_property import ModelProperty, build_model_property +from .model_property import ModelProperty, build_model_property, process_model from .property import Property from .schemas import ( Class, Parameters, + ReferencePath, Schemas, parse_reference_path, update_parameters_with_data, @@ -187,11 +188,12 @@ class ListProperty(Property, Generic[InnerProp]): inner_property: InnerProp template: ClassVar[str] = "list_property.py.jinja" - def get_base_type_string(self) -> str: - return f"List[{self.inner_property.get_type_string()}]" + # pylint: disable=unused-argument + def get_base_type_string(self, *, quoted: bool = False) -> str: + return f"List[{self.inner_property.get_type_string(quoted=not self.inner_property.is_base_type)}]" - def get_base_json_type_string(self) -> str: - return f"List[{self.inner_property.get_type_string(json=True)}]" + def get_base_json_type_string(self, *, quoted: bool = False) -> str: + return f"List[{self.inner_property.get_type_string(json=True, quoted=not self.inner_property.is_base_type)}]" def get_instance_type_string(self) -> str: """Get a string representation of runtime type that should be used for `isinstance` checks""" @@ -210,6 +212,11 @@ def get_imports(self, *, prefix: str) -> Set[str]: imports.add("from typing import cast, List") return imports + def get_lazy_imports(self, *, prefix: str) -> Set[str]: + lazy_imports = super().get_lazy_imports(prefix=prefix) + lazy_imports.update(self.inner_property.get_lazy_imports(prefix=prefix)) + return lazy_imports + @attr.s(auto_attribs=True, frozen=True) class UnionProperty(Property): @@ -219,7 +226,9 @@ class UnionProperty(Property): template: ClassVar[str] = "union_property.py.jinja" def _get_inner_type_strings(self, json: bool = False) -> Set[str]: - return {p.get_type_string(no_optional=True, json=json) for p in self.inner_properties} + return { + p.get_type_string(no_optional=True, json=json, quoted=not p.is_base_type) for p in self.inner_properties + } @staticmethod def _get_type_string_from_inner_type_strings(inner_types: Set[str]) -> str: @@ -227,10 +236,11 @@ def _get_type_string_from_inner_type_strings(inner_types: Set[str]) -> str: return inner_types.pop() return f"Union[{', '.join(sorted(inner_types))}]" - def get_base_type_string(self) -> str: + # pylint: disable=unused-argument + def get_base_type_string(self, *, quoted: bool = False) -> str: return self._get_type_string_from_inner_type_strings(self._get_inner_type_strings(json=False)) - def get_base_json_type_string(self) -> str: + def get_base_json_type_string(self, *, quoted: bool = False) -> str: return self._get_type_string_from_inner_type_strings(self._get_inner_type_strings(json=True)) def get_type_strings_in_union(self, no_optional: bool = False, json: bool = False) -> Set[str]: @@ -255,7 +265,13 @@ def get_type_strings_in_union(self, no_optional: bool = False, json: bool = Fals type_strings.add("Unset") return type_strings - def get_type_string(self, no_optional: bool = False, json: bool = False) -> str: + def get_type_string( + self, + no_optional: bool = False, + json: bool = False, + *, + quoted: bool = False, + ) -> str: """ Get a string representation of type that should be used when declaring this property. This implementation differs slightly from `Property.get_type_string` in order to collapse @@ -278,6 +294,12 @@ def get_imports(self, *, prefix: str) -> Set[str]: imports.add("from typing import cast, Union") return imports + def get_lazy_imports(self, *, prefix: str) -> Set[str]: + lazy_imports = super().get_lazy_imports(prefix=prefix) + for inner_prop in self.inner_properties: + lazy_imports.update(inner_prop.get_lazy_imports(prefix=prefix)) + return lazy_imports + def _string_based_property( name: str, required: bool, data: oai.Schema, config: Config @@ -493,7 +515,15 @@ def build_union_property( def build_list_property( - *, data: oai.Schema, name: str, required: bool, schemas: Schemas, parent_name: str, config: Config + *, + data: oai.Schema, + name: str, + required: bool, + schemas: Schemas, + parent_name: str, + config: Config, + process_properties: bool, + roots: Set[Union[ReferencePath, utils.ClassName]], ) -> Tuple[Union[ListProperty[Any], PropertyError], Schemas]: """ Build a ListProperty the right way, use this instead of the normal constructor. @@ -513,7 +543,14 @@ def build_list_property( if data.items is None: return PropertyError(data=data, detail="type array must have items defined"), schemas inner_prop, schemas = property_from_data( - name=f"{name}_item", required=True, data=data.items, schemas=schemas, parent_name=parent_name, config=config + name=f"{name}_item", + required=True, + data=data.items, + schemas=schemas, + parent_name=parent_name, + config=config, + process_properties=process_properties, + roots=roots, ) if isinstance(inner_prop, PropertyError): inner_prop.header = f'invalid data in items of array named "{name}"' @@ -541,6 +578,7 @@ def _property_from_ref( data: oai.Reference, schemas: Schemas, config: Config, + roots: Set[Union[ReferencePath, utils.ClassName]], ) -> Tuple[Union[Property, PropertyError], Schemas]: ref_path = parse_reference_path(data.ref) if isinstance(ref_path, ParseError): @@ -563,6 +601,7 @@ def _property_from_ref( return default, schemas prop = attr.evolve(prop, default=default) + schemas.add_dependencies(ref_path=ref_path, roots=roots) return prop, schemas @@ -574,17 +613,21 @@ def _property_from_data( schemas: Schemas, parent_name: str, config: Config, + process_properties: bool, + roots: Set[Union[ReferencePath, utils.ClassName]], ) -> Tuple[Union[Property, PropertyError], Schemas]: """Generate a Property from the OpenAPI dictionary representation of it""" name = utils.remove_string_escapes(name) if isinstance(data, oai.Reference): - return _property_from_ref(name=name, required=required, parent=None, data=data, schemas=schemas, config=config) + return _property_from_ref( + name=name, required=required, parent=None, data=data, schemas=schemas, config=config, roots=roots + ) sub_data: List[Union[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( - name=name, required=required, parent=data, data=sub_data[0], schemas=schemas, config=config + name=name, required=required, parent=data, data=sub_data[0], schemas=schemas, config=config, roots=roots ) if data.enum: @@ -644,11 +687,25 @@ def _property_from_data( ) if data.type == oai.DataType.ARRAY: return build_list_property( - data=data, name=name, required=required, schemas=schemas, parent_name=parent_name, config=config + data=data, + name=name, + required=required, + schemas=schemas, + parent_name=parent_name, + config=config, + process_properties=process_properties, + roots=roots, ) if data.type == oai.DataType.OBJECT or data.allOf: return build_model_property( - data=data, name=name, schemas=schemas, required=required, parent_name=parent_name, config=config + data=data, + name=name, + schemas=schemas, + required=required, + parent_name=parent_name, + config=config, + process_properties=process_properties, + roots=roots, ) return ( AnyProperty( @@ -672,6 +729,8 @@ def property_from_data( schemas: Schemas, parent_name: str, config: Config, + process_properties: bool = True, + roots: Set[Union[ReferencePath, utils.ClassName]] = None, ) -> Tuple[Union[Property, PropertyError], Schemas]: """ Build a Property from an OpenAPI schema or reference. This Property represents a single input or output for a @@ -691,23 +750,33 @@ def property_from_data( of duplication. config: Contains the parsed config that the user provided to tweak generation settings. Needed to apply class name overrides for generated classes. - + process_properties: If the new property is a ModelProperty, determines whether it will be initialized with + property data + roots: The set of `ReferencePath`s and `ClassName`s to remove from the schemas if a child reference becomes + invalid Returns: A tuple containing either the parsed Property or a PropertyError (if something went wrong) and the updated Schemas (including any new classes that should be generated). """ + roots = roots or set() try: return _property_from_data( - name=name, required=required, data=data, schemas=schemas, parent_name=parent_name, config=config + name=name, + required=required, + data=data, + schemas=schemas, + parent_name=parent_name, + config=config, + process_properties=process_properties, + roots=roots, ) except ValidationError: return PropertyError(detail="Failed to validate default value", data=data), schemas -def build_schemas( +def _create_schemas( *, components: Dict[str, Union[oai.Reference, oai.Schema]], schemas: Schemas, config: Config ) -> Schemas: - """Get a list of Schemas from an OpenAPI dict""" to_process: Iterable[Tuple[str, Union[oai.Reference, oai.Schema]]] = components.items() still_making_progress = True errors: List[PropertyError] = [] @@ -739,6 +808,74 @@ def build_schemas( return schemas +def _propogate_removal(*, root: Union[ReferencePath, utils.ClassName], schemas: Schemas, error: PropertyError) -> None: + if isinstance(root, utils.ClassName): + schemas.classes_by_name.pop(root, None) + return + if root in schemas.classes_by_reference: + error.detail = error.detail or "" + error.detail += f"\n{root}" + del schemas.classes_by_reference[root] + for child in schemas.dependencies.get(root, set()): + _propogate_removal(root=child, schemas=schemas, error=error) + + +def _process_model_errors( + model_errors: List[Tuple[ModelProperty, PropertyError]], *, schemas: Schemas +) -> List[PropertyError]: + for model, error in model_errors: + error.detail = error.detail or "" + error.detail += "\n\nFailure to process schema has resulted in the removal of:" + for root in model.roots: + _propogate_removal(root=root, schemas=schemas, error=error) + return [error for _, error in 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)) + still_making_progress = True + final_model_errors: List[Tuple[ModelProperty, PropertyError]] = [] + latest_model_errors: List[Tuple[ModelProperty, PropertyError]] = [] + + # Models which refer to other models in their allOf must be processed after their referenced models + while still_making_progress: + still_making_progress = False + # Only accumulate errors from the last round, since we might fix some along the way + latest_model_errors = [] + next_round = [] + for model_prop in to_process: + schemas_or_err = process_model(model_prop, schemas=schemas, config=config) + if isinstance(schemas_or_err, PropertyError): + schemas_or_err.header = f"\nUnable to process schema {model_prop.name}:" + if isinstance(schemas_or_err.data, oai.Reference) and schemas_or_err.data.ref.endswith( + f"/{model_prop.class_info.name}" + ): + schemas_or_err.detail = schemas_or_err.detail or "" + schemas_or_err.detail += "\n\nRecursive allOf reference found" + final_model_errors.append((model_prop, schemas_or_err)) + continue + latest_model_errors.append((model_prop, schemas_or_err)) + next_round.append(model_prop) + continue + schemas = schemas_or_err + still_making_progress = True + to_process = (prop for prop in next_round) + + final_model_errors.extend(latest_model_errors) + errors = _process_model_errors(final_model_errors, schemas=schemas) + schemas.errors.extend(errors) + return schemas + + +def build_schemas( + *, components: Dict[str, Union[oai.Reference, oai.Schema]], schemas: Schemas, config: Config +) -> Schemas: + """Get a list of Schemas from an OpenAPI dict""" + schemas = _create_schemas(components=components, schemas=schemas, config=config) + schemas = _process_models(schemas=schemas, config=config) + return schemas + + def build_parameters( *, components: Dict[str, Union[oai.Reference, oai.Parameter]], diff --git a/openapi_python_client/parser/properties/enum_property.py b/openapi_python_client/parser/properties/enum_property.py index 39b89a2bc..26c4e2ffc 100644 --- a/openapi_python_client/parser/properties/enum_property.py +++ b/openapi_python_client/parser/properties/enum_property.py @@ -30,10 +30,11 @@ class EnumProperty(Property): oai.ParameterLocation.HEADER, } - def get_base_type_string(self) -> str: + # pylint: disable=unused-argument + def get_base_type_string(self, *, quoted: bool = False) -> str: return self.class_info.name - def get_base_json_type_string(self) -> str: + def get_base_json_type_string(self, *, quoted: bool = False) -> str: return self.value_type.__name__ def get_imports(self, *, prefix: str) -> Set[str]: diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index 6e68a8f8e..18e4c4c43 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from itertools import chain from typing import ClassVar, Dict, List, NamedTuple, Optional, Set, Tuple, Union @@ -9,7 +11,7 @@ from ..errors import ParseError, PropertyError from .enum_property import EnumProperty from .property import Property -from .schemas import Class, Schemas, parse_reference_path +from .schemas import Class, ReferencePath, Schemas, parse_reference_path @attr.s(auto_attribs=True, frozen=True) @@ -17,19 +19,31 @@ class ModelProperty(Property): """A property which refers to another Schema""" class_info: Class - required_properties: List[Property] - optional_properties: List[Property] + data: oai.Schema description: str - relative_imports: Set[str] - additional_properties: Union[bool, Property] + roots: Set[Union[ReferencePath, utils.ClassName]] + required_properties: Optional[List[Property]] + optional_properties: Optional[List[Property]] + relative_imports: Optional[Set[str]] + lazy_imports: Optional[Set[str]] + additional_properties: Optional[Union[bool, Property]] _json_type_string: ClassVar[str] = "Dict[str, Any]" template: ClassVar[str] = "model_property.py.jinja" json_is_dict: ClassVar[bool] = True is_multipart_body: bool = False - def get_base_type_string(self) -> str: - return self.class_info.name + def __attrs_post_init__(self) -> None: + if self.relative_imports: + self.set_relative_imports(self.relative_imports) + + @property + def self_import(self) -> str: + """Constructs a self import statement from this ModelProperty's attributes""" + return f"models.{self.class_info.module_name} import {self.class_info.name}" + + def get_base_type_string(self, *, quoted: bool = False) -> str: + return f'"{self.class_info.name}"' if quoted else self.class_info.name def get_imports(self, *, prefix: str) -> Set[str]: """ @@ -42,13 +56,69 @@ def get_imports(self, *, prefix: str) -> Set[str]: imports = super().get_imports(prefix=prefix) imports.update( { - f"from {prefix}models.{self.class_info.module_name} import {self.class_info.name}", "from typing import Dict", "from typing import cast", } ) return imports + def get_lazy_imports(self, *, prefix: str) -> Set[str]: + """Get a set of lazy import strings that should be included when this property is used somewhere + + Args: + prefix: A prefix to put before any relative (local) module names. This should be the number of . to get + back to the root of the generated client. + """ + return {f"from {prefix}{self.self_import}"} + + def set_relative_imports(self, relative_imports: Set[str]) -> None: + """Set the relative imports set for this ModelProperty, filtering out self imports + + Args: + relative_imports: The set of relative import strings + """ + object.__setattr__(self, "relative_imports", {ri for ri in relative_imports if self.self_import not in ri}) + + def set_lazy_imports(self, lazy_imports: Set[str]) -> None: + """Set the lazy imports set for this ModelProperty, filtering out self imports + + Args: + lazy_imports: The set of lazy import strings + """ + object.__setattr__(self, "lazy_imports", {li for li in lazy_imports if self.self_import not in li}) + + def get_type_string( + self, + no_optional: bool = False, + json: bool = False, + *, + quoted: bool = False, + ) -> str: + """ + Get a string representation of type that should be used when declaring this property + + Args: + no_optional: Do not include Optional or Unset even if the value is optional (needed for isinstance checks) + json: True if the type refers to the property after JSON serialization + """ + if json: + type_string = self.get_base_json_type_string() + else: + type_string = self.get_base_type_string() + + if quoted: + if type_string == self.class_info.name: + type_string = f"'{type_string}'" + + if no_optional or (self.required and not self.nullable): + return type_string + if self.required and self.nullable: + return f"Optional[{type_string}]" + if not self.required and self.nullable: + return f"Union[Unset, None, {type_string}]" + + return f"Union[Unset, {type_string}]" + def _values_are_subset(first: EnumProperty, second: EnumProperty) -> bool: return set(first.values.items()) <= set(second.values.items()) @@ -108,17 +178,24 @@ class _PropertyData(NamedTuple): optional_props: List[Property] required_props: List[Property] relative_imports: Set[str] + lazy_imports: Set[str] schemas: Schemas -# pylint: disable=too-many-locals,too-many-branches +# pylint: disable=too-many-locals,too-many-branches,too-many-return-statements def _process_properties( - *, data: oai.Schema, schemas: Schemas, class_name: str, config: Config + *, + data: oai.Schema, + schemas: Schemas, + class_name: utils.ClassName, + config: Config, + roots: Set[Union[ReferencePath, utils.ClassName]], ) -> Union[_PropertyData, PropertyError]: from . import property_from_data properties: Dict[str, Property] = {} relative_imports: Set[str] = set() + lazy_imports: Set[str] = set() required_set = set(data.required or []) def _add_if_no_conflict(new_prop: Property) -> Optional[PropertyError]: @@ -145,10 +222,16 @@ def _add_if_no_conflict(new_prop: Property) -> Optional[PropertyError]: return PropertyError(f"Reference {sub_prop.ref} not found") if not isinstance(sub_model, ModelProperty): return PropertyError("Cannot take allOf a non-object") + # Properties of allOf references first should be processed first + if not ( + isinstance(sub_model.required_properties, list) and isinstance(sub_model.optional_properties, list) + ): + return PropertyError(f"Reference {sub_model.name} in allOf was not processed", data=sub_prop) for prop in chain(sub_model.required_properties, sub_model.optional_properties): err = _add_if_no_conflict(prop) if err is not None: return err + schemas.add_dependencies(ref_path=ref_path, roots=roots) else: unprocessed_props.update(sub_prop.properties or {}) required_set.update(sub_prop.required or []) @@ -157,7 +240,13 @@ def _add_if_no_conflict(new_prop: Property) -> Optional[PropertyError]: 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 + name=key, + required=prop_required, + data=value, + schemas=schemas, + parent_name=class_name, + config=config, + roots=roots, ) if isinstance(prop_or_error, Property): prop_or_error = _add_if_no_conflict(prop_or_error) @@ -171,12 +260,15 @@ def _add_if_no_conflict(new_prop: Property) -> Optional[PropertyError]: required_properties.append(prop) else: optional_properties.append(prop) + + lazy_imports.update(prop.get_lazy_imports(prefix="..")) relative_imports.update(prop.get_imports(prefix="..")) return _PropertyData( optional_props=optional_properties, required_props=required_properties, relative_imports=relative_imports, + lazy_imports=lazy_imports, schemas=schemas, ) @@ -185,8 +277,9 @@ def _get_additional_properties( *, schema_additional: Union[None, bool, oai.Reference, oai.Schema], schemas: Schemas, - class_name: str, + class_name: utils.ClassName, config: Config, + roots: Set[Union[ReferencePath, utils.ClassName]], ) -> Tuple[Union[bool, Property, PropertyError], Schemas]: from . import property_from_data @@ -207,12 +300,82 @@ def _get_additional_properties( schemas=schemas, parent_name=class_name, config=config, + roots=roots, ) return additional_properties, schemas +def _process_property_data( + *, + data: oai.Schema, + schemas: Schemas, + class_info: Class, + config: Config, + roots: Set[Union[ReferencePath, utils.ClassName]], +) -> Tuple[Union[Tuple[_PropertyData, Union[bool, Property]], PropertyError], Schemas]: + property_data = _process_properties( + data=data, schemas=schemas, class_name=class_info.name, config=config, roots=roots + ) + if isinstance(property_data, PropertyError): + return property_data, schemas + schemas = property_data.schemas + + additional_properties, schemas = _get_additional_properties( + schema_additional=data.additionalProperties, + schemas=schemas, + class_name=class_info.name, + config=config, + roots=roots, + ) + if isinstance(additional_properties, Property): + property_data.relative_imports.update(additional_properties.get_imports(prefix="..")) + property_data.lazy_imports.update(additional_properties.get_lazy_imports(prefix="..")) + elif isinstance(additional_properties, PropertyError): + return additional_properties, schemas + + return (property_data, additional_properties), schemas + + +def process_model(model_prop: ModelProperty, *, schemas: Schemas, config: Config) -> Union[Schemas, PropertyError]: + """Populate a ModelProperty instance's property data + Args: + model_prop: The ModelProperty to build property data for + schemas: Existing Schemas + config: Config data for this run of the generator, used to modifying names + Returns: + Either the updated `schemas` input or a `PropertyError` if something went wrong. + """ + data_or_err, schemas = _process_property_data( + data=model_prop.data, + schemas=schemas, + class_info=model_prop.class_info, + config=config, + roots=model_prop.roots, + ) + if isinstance(data_or_err, PropertyError): + return data_or_err + + property_data, additional_properties = data_or_err + + object.__setattr__(model_prop, "required_properties", property_data.required_props) + object.__setattr__(model_prop, "optional_properties", property_data.optional_props) + model_prop.set_relative_imports(property_data.relative_imports) + model_prop.set_lazy_imports(property_data.lazy_imports) + object.__setattr__(model_prop, "additional_properties", additional_properties) + return schemas + + +# pylint: disable=too-many-locals def build_model_property( - *, data: oai.Schema, name: str, schemas: Schemas, required: bool, parent_name: Optional[str], config: Config + *, + data: oai.Schema, + name: str, + schemas: Schemas, + required: bool, + parent_name: Optional[str], + config: Config, + process_properties: bool, + roots: Set[Union[ReferencePath, utils.ClassName]], ) -> Tuple[Union[ModelProperty, PropertyError], Schemas]: """ A single ModelProperty from its OAI data @@ -225,36 +388,49 @@ def build_model_property( required: Whether or not this property is required by the parent (affects typing) parent_name: The name of the property that this property is inside of (affects class naming) config: Config data for this run of the generator, used to modifying names + roots: Set of strings that identify schema objects on which the new ModelProperty will depend + process_properties: Determines whether the new ModelProperty will be initialized with property data """ class_string = data.title or name if parent_name: class_string = f"{utils.pascal_case(parent_name)}{utils.pascal_case(class_string)}" class_info = Class.from_string(string=class_string, config=config) - - property_data = _process_properties(data=data, schemas=schemas, class_name=class_info.name, config=config) - if isinstance(property_data, PropertyError): - return property_data, schemas - schemas = property_data.schemas - - additional_properties, schemas = _get_additional_properties( - schema_additional=data.additionalProperties, schemas=schemas, class_name=class_info.name, config=config - ) - if isinstance(additional_properties, Property): - property_data.relative_imports.update(additional_properties.get_imports(prefix="..")) - elif isinstance(additional_properties, PropertyError): - return additional_properties, schemas + model_roots = {*roots, class_info.name} + required_properties: Optional[List[Property]] = None + optional_properties: Optional[List[Property]] = None + relative_imports: Optional[Set[str]] = None + lazy_imports: Optional[Set[str]] = None + additional_properties: Optional[Union[bool, Property]] = None + if process_properties: + data_or_err, schemas = _process_property_data( + data=data, schemas=schemas, class_info=class_info, config=config, roots=model_roots + ) + if isinstance(data_or_err, PropertyError): + return data_or_err, schemas + property_data, additional_properties = data_or_err + required_properties = property_data.required_props + optional_properties = property_data.optional_props + relative_imports = property_data.relative_imports + lazy_imports = property_data.lazy_imports + for root in roots: + if isinstance(root, utils.ClassName): + continue + schemas.add_dependencies(root, {class_info.name}) prop = ModelProperty( class_info=class_info, - required_properties=property_data.required_props, - optional_properties=property_data.optional_props, - relative_imports=property_data.relative_imports, + data=data, + roots=model_roots, + required_properties=required_properties, + optional_properties=optional_properties, + relative_imports=relative_imports, + lazy_imports=lazy_imports, + additional_properties=additional_properties, description=data.description or "", default=None, nullable=data.nullable, required=required, name=name, - additional_properties=additional_properties, python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix), example=data.example, ) diff --git a/openapi_python_client/parser/properties/property.py b/openapi_python_client/parser/properties/property.py index bcedfc3d9..5f82df224 100644 --- a/openapi_python_client/parser/properties/property.py +++ b/openapi_python_client/parser/properties/property.py @@ -1,6 +1,6 @@ __all__ = ["Property"] -from typing import ClassVar, Optional, Set +from typing import TYPE_CHECKING, ClassVar, Optional, Set import attr @@ -9,6 +9,11 @@ from ...utils import PythonIdentifier from ..errors import ParseError +if TYPE_CHECKING: # pragma: no cover + from .model_property import ModelProperty +else: + ModelProperty = "ModelProperty" # pylint: disable=invalid-name + @attr.s(auto_attribs=True, frozen=True) class Property: @@ -60,15 +65,21 @@ def set_python_name(self, new_name: str, config: Config) -> None: """ object.__setattr__(self, "python_name", PythonIdentifier(value=new_name, prefix=config.field_prefix)) - def get_base_type_string(self) -> str: - """Get the string describing the Python type of this property.""" - return self._type_string - - def get_base_json_type_string(self) -> str: - """Get the string describing the JSON type of this property.""" - return self._json_type_string - - def get_type_string(self, no_optional: bool = False, json: bool = False) -> str: + def get_base_type_string(self, *, quoted: bool = False) -> str: + """Get the string describing the Python type of this property. Base types no require quoting.""" + return f'"{self._type_string}"' if not self.is_base_type and quoted else self._type_string + + def get_base_json_type_string(self, *, quoted: bool = False) -> str: + """Get the string describing the JSON type of this property. Base types no require quoting.""" + return f'"{self._json_type_string}"' if not self.is_base_type and quoted else self._json_type_string + + def get_type_string( + self, + no_optional: bool = False, + json: bool = False, + *, + quoted: bool = False, + ) -> str: """ Get a string representation of type that should be used when declaring this property @@ -77,9 +88,9 @@ def get_type_string(self, no_optional: bool = False, json: bool = False) -> str: json: True if the type refers to the property after JSON serialization """ if json: - type_string = self.get_base_json_type_string() + type_string = self.get_base_json_type_string(quoted=quoted) else: - type_string = self.get_base_type_string() + type_string = self.get_base_type_string(quoted=quoted) if no_optional or (self.required and not self.nullable): return type_string @@ -92,7 +103,7 @@ def get_type_string(self, no_optional: bool = False, json: bool = False) -> str: def get_instance_type_string(self) -> str: """Get a string representation of runtime type that should be used for `isinstance` checks""" - return self.get_type_string(no_optional=True) + return self.get_type_string(no_optional=True, quoted=False) # noinspection PyUnusedLocal def get_imports(self, *, prefix: str) -> Set[str]: @@ -111,6 +122,16 @@ def get_imports(self, *, prefix: str) -> Set[str]: imports.add(f"from {prefix}types import UNSET, Unset") return imports + # pylint: disable=unused-argument,no-self-use) + def get_lazy_imports(self, *, prefix: str) -> Set[str]: + """Get a set of lazy import strings that should be included when this property is used somewhere + + Args: + prefix: A prefix to put before any relative (local) module names. This should be the number of . to get + back to the root of the generated client. + """ + return set() + def to_string(self) -> str: """How this should be declared in a dataclass""" default: Optional[str] @@ -122,8 +143,8 @@ def to_string(self) -> str: default = None if default is not None: - return f"{self.python_name}: {self.get_type_string()} = {default}" - return f"{self.python_name}: {self.get_type_string()}" + return f"{self.python_name}: {self.get_type_string(quoted=True)} = {default}" + return f"{self.python_name}: {self.get_type_string(quoted=True)}" def to_docstring(self) -> str: """Returns property docstring""" @@ -133,3 +154,14 @@ def to_docstring(self) -> str: if self.example: doc += f" Example: {self.example}." return doc + + @property + def is_base_type(self) -> bool: + """Base types, represented by any other of `Property` than `ModelProperty` should not be quoted.""" + from . import ListProperty, ModelProperty, UnionProperty + + return self.__class__.__name__ not in { + ModelProperty.__name__, + ListProperty.__name__, + UnionProperty.__name__, + } diff --git a/openapi_python_client/parser/properties/schemas.py b/openapi_python_client/parser/properties/schemas.py index a0606b8c1..fd1af5c08 100644 --- a/openapi_python_client/parser/properties/schemas.py +++ b/openapi_python_client/parser/properties/schemas.py @@ -9,7 +9,7 @@ "parameter_from_data", ] -from typing import TYPE_CHECKING, Dict, List, NewType, Tuple, Union, cast +from typing import TYPE_CHECKING, Dict, List, NewType, Set, Tuple, Union, cast from urllib.parse import urlparse import attr @@ -26,10 +26,10 @@ Property = "Property" # pylint: disable=invalid-name -_ReferencePath = NewType("_ReferencePath", str) +ReferencePath = NewType("ReferencePath", str) -def parse_reference_path(ref_path_raw: str) -> Union[_ReferencePath, ParseError]: +def parse_reference_path(ref_path_raw: str) -> Union[ReferencePath, ParseError]: """ Takes a raw string provided in a `$ref` and turns it into a validated `_ReferencePath` or a `ParseError` if validation fails. @@ -40,7 +40,7 @@ def parse_reference_path(ref_path_raw: str) -> Union[_ReferencePath, ParseError] parsed = urlparse(ref_path_raw) if parsed.scheme or parsed.path: return ParseError(detail=f"Remote references such as {ref_path_raw} are not supported yet.") - return cast(_ReferencePath, parsed.fragment) + return cast(ReferencePath, parsed.fragment) @attr.s(auto_attribs=True, frozen=True) @@ -73,13 +73,24 @@ def from_string(*, string: str, config: Config) -> "Class": class Schemas: """Structure for containing all defined, shareable, and reusable schemas (attr classes and Enums)""" - classes_by_reference: Dict[_ReferencePath, Property] = attr.ib(factory=dict) + classes_by_reference: Dict[ReferencePath, Property] = attr.ib(factory=dict) + dependencies: Dict[ReferencePath, Set[Union[ReferencePath, ClassName]]] = attr.ib(factory=dict) classes_by_name: Dict[ClassName, Property] = attr.ib(factory=dict) errors: List[ParseError] = attr.ib(factory=list) + def add_dependencies(self, ref_path: ReferencePath, roots: Set[Union[ReferencePath, ClassName]]) -> None: + """Record new dependencies on the given ReferencePath + + Args: + ref_path: The ReferencePath being referenced + roots: A set of identifiers for the objects dependent on the object corresponding to `ref_path` + """ + self.dependencies.setdefault(ref_path, set()) + self.dependencies[ref_path].update(roots) + def update_schemas_with_data( - *, ref_path: _ReferencePath, data: oai.Schema, schemas: Schemas, config: Config + *, ref_path: ReferencePath, data: oai.Schema, schemas: Schemas, config: Config ) -> Union[Schemas, PropertyError]: """ Update a `Schemas` using some new reference. @@ -100,7 +111,15 @@ def update_schemas_with_data( prop: Union[PropertyError, Property] prop, schemas = property_from_data( - data=data, name=ref_path, schemas=schemas, required=True, parent_name="", config=config + data=data, + name=ref_path, + schemas=schemas, + required=True, + parent_name="", + config=config, + # Don't process ModelProperty properties because schemas are still being created + process_properties=False, + roots={ref_path}, ) if isinstance(prop, PropertyError): @@ -108,8 +127,7 @@ def update_schemas_with_data( prop.header = f"Unable to parse schema {ref_path}" if isinstance(prop.data, oai.Reference) and prop.data.ref.endswith(ref_path): # pragma: nocover prop.detail += ( - "\n\nRecursive and circular references are not supported. " - "See https://github.com/openapi-generators/openapi-python-client/issues/466" + "\n\nRecursive and circular references are not supported directly in an array schema's 'items' section" ) return prop @@ -121,7 +139,7 @@ def update_schemas_with_data( class Parameters: """Structure for containing all defined, shareable, and reusable parameters""" - classes_by_reference: Dict[_ReferencePath, Parameter] = attr.ib(factory=dict) + classes_by_reference: Dict[ReferencePath, Parameter] = attr.ib(factory=dict) classes_by_name: Dict[ClassName, Parameter] = attr.ib(factory=dict) errors: List[ParseError] = attr.ib(factory=list) @@ -154,7 +172,7 @@ def parameter_from_data( def update_parameters_with_data( - *, ref_path: _ReferencePath, data: oai.Parameter, parameters: Parameters + *, ref_path: ReferencePath, data: oai.Parameter, parameters: Parameters ) -> Union[Parameters, ParameterError]: """ Update a `Parameters` using some new reference. diff --git a/openapi_python_client/templates/model.py.jinja b/openapi_python_client/templates/model.py.jinja index dc033c41a..98f2d2682 100644 --- a/openapi_python_client/templates/model.py.jinja +++ b/openapi_python_client/templates/model.py.jinja @@ -1,4 +1,4 @@ -from typing import Any, Dict, Type, TypeVar, Tuple, Optional, BinaryIO, TextIO +from typing import Any, Dict, Type, TypeVar, Tuple, Optional, BinaryIO, TextIO, TYPE_CHECKING {% if model.additional_properties %} from typing import List @@ -16,9 +16,16 @@ from ..types import UNSET, Unset {{ relative }} {% endfor %} +{% for lazy_import in model.lazy_imports %} +{% if loop.first %} +if TYPE_CHECKING: +{% endif %} + {{ lazy_import }} +{% endfor %} + {% if model.additional_properties %} -{% set additional_property_type = 'Any' if model.additional_properties == True else model.additional_properties.get_type_string() %} +{% set additional_property_type = 'Any' if model.additional_properties == True else model.additional_properties.get_type_string(quoted=not model.additional_properties.is_base_type) %} {% endif %} {% set class_name = model.class_info.name %} @@ -113,6 +120,9 @@ return field_dict {% endmacro %} def to_dict(self) -> Dict[str, Any]: + {% for lazy_import in model.lazy_imports %} + {{ lazy_import }} + {% endfor %} {{ _to_dict() | indent(8) }} {% if model.is_multipart_body %} @@ -122,6 +132,9 @@ return field_dict @classmethod def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + {% for lazy_import in model.lazy_imports %} + {{ lazy_import }} + {% endfor %} d = src_dict.copy() {% for property in model.required_properties + model.optional_properties %} {% if property.required %} @@ -146,6 +159,12 @@ return field_dict {% if model.additional_properties %} {% if model.additional_properties.template %}{# Can be a bool instead of an object #} {% import "property_templates/" + model.additional_properties.template as prop_template %} + +{% if model.additional_properties.lazy_imports %} + {% for lazy_import in model.additional_properties.lazy_imports %} + {{ lazy_import }} + {% endfor %} +{% endif %} {% else %} {% set prop_template = None %} {% endif %} diff --git a/poetry.lock b/poetry.lock index af8869e7d..5f692bd21 100644 --- a/poetry.lock +++ b/poetry.lock @@ -383,14 +383,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pydantic" -version = "1.9.0" -description = "Data validation and settings management using python 3.6 type hinting" +version = "1.10.2" +description = "Data validation and settings management using python type hints" category = "main" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.7" [package.dependencies] -typing-extensions = ">=3.7.4.3" +typing-extensions = ">=4.1.0" [package.extras] dotenv = ["python-dotenv (>=0.10.4)"] @@ -631,10 +631,10 @@ python-versions = ">=3.6" click = ">=7.1.1,<9.0.0" [package.extras] -all = ["colorama (>=0.4.3,<0.5.0)", "shellingham (>=1.3.0,<2.0.0)", "rich (>=10.11.0,<13.0.0)"] -dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] -doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mdx-include (>=1.4.1,<2.0.0)"] -test = ["shellingham (>=1.3.0,<2.0.0)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov (>=2.10.0,<3.0.0)", "coverage (>=5.2,<6.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "mypy (==0.910)", "black (>=22.3.0,<23.0.0)", "isort (>=5.0.6,<6.0.0)", "rich (>=10.11.0,<13.0.0)"] +test = ["rich (>=10.11.0,<13.0.0)", "isort (>=5.0.6,<6.0.0)", "black (>=22.3.0,<23.0.0)", "mypy (==0.910)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "coverage (>=5.2,<6.0)", "pytest-cov (>=2.10.0,<3.0.0)", "pytest (>=4.4.0,<5.4.0)", "shellingham (>=1.3.0,<2.0.0)"] +doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mkdocs (>=1.1.2,<2.0.0)"] +dev = ["pre-commit (>=2.17.0,<3.0.0)", "flake8 (>=3.8.3,<4.0.0)", "autoflake (>=1.3.1,<2.0.0)"] +all = ["rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)", "colorama (>=0.4.3,<0.5.0)"] [[package]] name = "types-certifi" @@ -662,11 +662,11 @@ python-versions = "*" [[package]] name = "typing-extensions" -version = "3.10.0.0" -description = "Backported and Experimental Type Hints for Python 3.5+" +version = "4.3.0" +description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.7" [[package]] name = "urllib3" @@ -1036,43 +1036,7 @@ py = [ {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, ] -pydantic = [ - {file = "pydantic-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cb23bcc093697cdea2708baae4f9ba0e972960a835af22560f6ae4e7e47d33f5"}, - {file = "pydantic-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1d5278bd9f0eee04a44c712982343103bba63507480bfd2fc2790fa70cd64cf4"}, - {file = "pydantic-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab624700dc145aa809e6f3ec93fb8e7d0f99d9023b713f6a953637429b437d37"}, - {file = "pydantic-1.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8d7da6f1c1049eefb718d43d99ad73100c958a5367d30b9321b092771e96c25"}, - {file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3c3b035103bd4e2e4a28da9da7ef2fa47b00ee4a9cf4f1a735214c1bcd05e0f6"}, - {file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3011b975c973819883842c5ab925a4e4298dffccf7782c55ec3580ed17dc464c"}, - {file = "pydantic-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:086254884d10d3ba16da0588604ffdc5aab3f7f09557b998373e885c690dd398"}, - {file = "pydantic-1.9.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0fe476769acaa7fcddd17cadd172b156b53546ec3614a4d880e5d29ea5fbce65"}, - {file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8e9dcf1ac499679aceedac7e7ca6d8641f0193c591a2d090282aaf8e9445a46"}, - {file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1e4c28f30e767fd07f2ddc6f74f41f034d1dd6bc526cd59e63a82fe8bb9ef4c"}, - {file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:c86229333cabaaa8c51cf971496f10318c4734cf7b641f08af0a6fbf17ca3054"}, - {file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:c0727bda6e38144d464daec31dff936a82917f431d9c39c39c60a26567eae3ed"}, - {file = "pydantic-1.9.0-cp36-cp36m-win_amd64.whl", hash = "sha256:dee5ef83a76ac31ab0c78c10bd7d5437bfdb6358c95b91f1ba7ff7b76f9996a1"}, - {file = "pydantic-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9c9bdb3af48e242838f9f6e6127de9be7063aad17b32215ccc36a09c5cf1070"}, - {file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ee7e3209db1e468341ef41fe263eb655f67f5c5a76c924044314e139a1103a2"}, - {file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b6037175234850ffd094ca77bf60fb54b08b5b22bc85865331dd3bda7a02fa1"}, - {file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b2571db88c636d862b35090ccf92bf24004393f85c8870a37f42d9f23d13e032"}, - {file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8b5ac0f1c83d31b324e57a273da59197c83d1bb18171e512908fe5dc7278a1d6"}, - {file = "pydantic-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bbbc94d0c94dd80b3340fc4f04fd4d701f4b038ebad72c39693c794fd3bc2d9d"}, - {file = "pydantic-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e0896200b6a40197405af18828da49f067c2fa1f821491bc8f5bde241ef3f7d7"}, - {file = "pydantic-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bdfdadb5994b44bd5579cfa7c9b0e1b0e540c952d56f627eb227851cda9db77"}, - {file = "pydantic-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:574936363cd4b9eed8acdd6b80d0143162f2eb654d96cb3a8ee91d3e64bf4cf9"}, - {file = "pydantic-1.9.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c556695b699f648c58373b542534308922c46a1cda06ea47bc9ca45ef5b39ae6"}, - {file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f947352c3434e8b937e3aa8f96f47bdfe6d92779e44bb3f41e4c213ba6a32145"}, - {file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5e48ef4a8b8c066c4a31409d91d7ca372a774d0212da2787c0d32f8045b1e034"}, - {file = "pydantic-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:96f240bce182ca7fe045c76bcebfa0b0534a1bf402ed05914a6f1dadff91877f"}, - {file = "pydantic-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:815ddebb2792efd4bba5488bc8fde09c29e8ca3227d27cf1c6990fc830fd292b"}, - {file = "pydantic-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c5b77947b9e85a54848343928b597b4f74fc364b70926b3c4441ff52620640c"}, - {file = "pydantic-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c68c3bc88dbda2a6805e9a142ce84782d3930f8fdd9655430d8576315ad97ce"}, - {file = "pydantic-1.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a79330f8571faf71bf93667d3ee054609816f10a259a109a0738dac983b23c3"}, - {file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f5a64b64ddf4c99fe201ac2724daada8595ada0d102ab96d019c1555c2d6441d"}, - {file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a733965f1a2b4090a5238d40d983dcd78f3ecea221c7af1497b845a9709c1721"}, - {file = "pydantic-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cc6a4cb8a118ffec2ca5fcb47afbacb4f16d0ab8b7350ddea5e8ef7bcc53a16"}, - {file = "pydantic-1.9.0-py3-none-any.whl", hash = "sha256:085ca1de245782e9b46cefcf99deecc67d418737a1fd3f6a4f511344b613a5b3"}, - {file = "pydantic-1.9.0.tar.gz", hash = "sha256:742645059757a56ecd886faf4ed2441b9c0cd406079c2b4bee51bcc3fbcd510a"}, -] +pydantic = [] pyflakes = [ {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, @@ -1207,10 +1171,7 @@ typed-ast = [ {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, ] -typer = [ - {file = "typer-0.6.1-py3-none-any.whl", hash = "sha256:54b19e5df18654070a82f8c2aa1da456a4ac16a2a83e6dcd9f170e291c56338e"}, - {file = "typer-0.6.1.tar.gz", hash = "sha256:2d5720a5e63f73eaf31edaa15f6ab87f35f0690f8ca233017d7d23d743a91d73"}, -] +typer = [] types-certifi = [ {file = "types-certifi-2020.4.0.tar.gz", hash = "sha256:787d1a0c7897a1c658f8f7958ae57141b3fff13acb866e5bcd31cfb45037546f"}, {file = "types_certifi-2020.4.0-py3-none-any.whl", hash = "sha256:0ffdbe451d3b02f6d2cfd87bcfb2f086a4ff1fa76a35d51cfc3771e261d7a8fd"}, @@ -1223,11 +1184,7 @@ types-pyyaml = [ {file = "types-PyYAML-6.0.3.tar.gz", hash = "sha256:6ea4eefa8579e0ce022f785a62de2bcd647fad4a81df5cf946fd67e4b059920b"}, {file = "types_PyYAML-6.0.3-py3-none-any.whl", hash = "sha256:8b50294b55a9db89498cdc5a65b1b4545112b6cd1cf4465bd693d828b0282a17"}, ] -typing-extensions = [ - {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, - {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, - {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, -] +typing-extensions = [] urllib3 = [ {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, diff --git a/tests/conftest.py b/tests/conftest.py index a1391ab4d..7f8442ab7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,8 +2,10 @@ import pytest +from openapi_python_client import schema as oai from openapi_python_client.parser.properties import ( AnyProperty, + BooleanProperty, DateProperty, DateTimeProperty, EnumProperty, @@ -34,12 +36,14 @@ def _factory(**kwargs): kwargs = { "description": "", "class_info": Class(name="MyClass", module_name="my_module"), - "required_properties": [], - "optional_properties": [], - "relative_imports": set(), - "additional_properties": False, + "data": oai.Schema.construct(), + "roots": set(), + "required_properties": None, + "optional_properties": None, + "relative_imports": None, + "lazy_imports": None, + "additional_properties": None, "python_name": "", - "description": "", "example": "", **kwargs, } @@ -145,6 +149,21 @@ def _factory(**kwargs): return _factory +@pytest.fixture +def boolean_property_factory() -> Callable[..., BooleanProperty]: + """ + 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 BooleanProperty(**kwargs) + + return _factory + + @pytest.fixture def date_time_property_factory() -> Callable[..., DateTimeProperty]: """ diff --git a/tests/test_parser/test_properties/test_init.py b/tests/test_parser/test_properties/test_init.py index 3c063365e..85ffe8570 100644 --- a/tests/test_parser/test_properties/test_init.py +++ b/tests/test_parser/test_properties/test_init.py @@ -13,6 +13,9 @@ class TestStringProperty: + def test_is_base_type(self, string_property_factory): + assert string_property_factory().is_base_type is True + @pytest.mark.parametrize( "required, nullable, expected", ( @@ -29,6 +32,9 @@ def test_get_type_string(self, string_property_factory, required, nullable, expe class TestDateTimeProperty: + def test_is_base_type(self, date_time_property_factory): + assert date_time_property_factory().is_base_type is True + @pytest.mark.parametrize("required", (True, False)) @pytest.mark.parametrize("nullable", (True, False)) def test_get_imports(self, date_time_property_factory, required, nullable): @@ -51,6 +57,9 @@ def test_get_imports(self, date_time_property_factory, required, nullable): class TestDateProperty: + def test_is_base_type(self, date_property_factory): + assert date_property_factory().is_base_type is True + @pytest.mark.parametrize("required", (True, False)) @pytest.mark.parametrize("nullable", (True, False)) def test_get_imports(self, date_property_factory, required, nullable): @@ -73,6 +82,9 @@ def test_get_imports(self, date_property_factory, required, nullable): class TestFileProperty: + def test_is_base_type(self, file_property_factory): + assert file_property_factory().is_base_type is True + @pytest.mark.parametrize("required", (True, False)) @pytest.mark.parametrize("nullable", (True, False)) def test_get_imports(self, file_property_factory, required, nullable): @@ -93,7 +105,50 @@ def test_get_imports(self, file_property_factory, required, nullable): assert p.get_imports(prefix="...") == expected +class TestNoneProperty: + def test_is_base_type(self, none_property_factory): + assert none_property_factory().is_base_type is True + + +class TestBooleanProperty: + def test_is_base_type(self, boolean_property_factory): + assert boolean_property_factory().is_base_type is True + + +class TestAnyProperty: + def test_is_base_type(self, any_property_factory): + assert any_property_factory().is_base_type is True + + +class TestIntProperty: + def test_is_base_type(self, int_property_factory): + assert int_property_factory().is_base_type is True + + class TestListProperty: + def test_is_base_type(self, list_property_factory): + assert list_property_factory().is_base_type is False + + @pytest.mark.parametrize("quoted", (True, False)) + def test_get_base_json_type_string_base_inner(self, list_property_factory, quoted): + p = list_property_factory() + assert p.get_base_json_type_string(quoted=quoted) == "List[str]" + + @pytest.mark.parametrize("quoted", (True, False)) + def test_get_base_json_type_string_model_inner(self, list_property_factory, model_property_factory, quoted): + m = model_property_factory() + p = list_property_factory(inner_property=m) + assert p.get_base_json_type_string(quoted=quoted) == "List[Dict[str, Any]]" + + def test_get_lazy_import_base_inner(self, list_property_factory): + p = list_property_factory() + assert p.get_lazy_imports(prefix="..") == set() + + def test_get_lazy_import_model_inner(self, list_property_factory, model_property_factory): + m = model_property_factory() + p = list_property_factory(inner_property=m) + assert p.get_lazy_imports(prefix="..") == {"from ..models.my_module import MyClass"} + @pytest.mark.parametrize( "required, nullable, expected", ( @@ -103,11 +158,51 @@ class TestListProperty: (False, True, "Union[Unset, None, List[str]]"), ), ) - def test_get_type_string(self, list_property_factory, required, nullable, expected): + def test_get_type_string_base_inner(self, list_property_factory, required, nullable, expected): p = list_property_factory(required=required, nullable=nullable) assert p.get_type_string() == expected + @pytest.mark.parametrize( + "required, nullable, expected", + ( + (True, False, "List['MyClass']"), + (True, True, "Optional[List['MyClass']]"), + (False, False, "Union[Unset, List['MyClass']]"), + (False, True, "Union[Unset, None, List['MyClass']]"), + ), + ) + def test_get_type_string_model_inner( + self, list_property_factory, model_property_factory, required, nullable, expected + ): + m = model_property_factory() + p = list_property_factory(required=required, nullable=nullable, inner_property=m) + + assert p.get_type_string() == expected + + @pytest.mark.parametrize( + "quoted,expected", + [ + (False, "List[str]"), + (True, "List[str]"), + ], + ) + def test_get_base_type_string_base_inner(self, list_property_factory, quoted, expected): + p = list_property_factory() + assert p.get_base_type_string(quoted=quoted) == expected + + @pytest.mark.parametrize( + "quoted,expected", + [ + (False, "List['MyClass']"), + (True, "List['MyClass']"), + ], + ) + def test_get_base_type_string_model_inner(self, list_property_factory, model_property_factory, quoted, expected): + m = model_property_factory() + p = list_property_factory(inner_property=m) + assert p.get_base_type_string(quoted=quoted) == expected + @pytest.mark.parametrize("required", (True, False)) @pytest.mark.parametrize("nullable", (True, False)) def test_get_type_imports(self, list_property_factory, date_time_property_factory, required, nullable): @@ -131,6 +226,18 @@ def test_get_type_imports(self, list_property_factory, date_time_property_factor class TestUnionProperty: + def test_is_base_type(self, union_property_factory): + assert union_property_factory().is_base_type is False + + def test_get_lazy_import_base_inner(self, union_property_factory): + p = union_property_factory() + assert p.get_lazy_imports(prefix="..") == set() + + def test_get_lazy_import_model_inner(self, union_property_factory, model_property_factory): + m = model_property_factory() + p = union_property_factory(inner_properties=[m]) + assert p.get_lazy_imports(prefix="..") == {"from ..models.my_module import MyClass"} + @pytest.mark.parametrize( "nullable,required,no_optional,json,expected", [ @@ -173,18 +280,34 @@ def test_get_type_string( assert p.get_type_string(no_optional=no_optional, json=json) == expected - def test_get_base_type_string(self, union_property_factory, date_time_property_factory, string_property_factory): + def test_get_base_type_string_base_inners( + self, union_property_factory, date_time_property_factory, string_property_factory + ): p = union_property_factory(inner_properties=[date_time_property_factory(), string_property_factory()]) assert p.get_base_type_string() == "Union[datetime.datetime, str]" - def test_get_base_type_string_one_element(self, union_property_factory, date_time_property_factory): + def test_get_base_type_string_one_base_inner(self, union_property_factory, date_time_property_factory): p = union_property_factory( inner_properties=[date_time_property_factory()], ) assert p.get_base_type_string() == "datetime.datetime" + def test_get_base_type_string_one_model_inner(self, union_property_factory, model_property_factory): + p = union_property_factory( + inner_properties=[model_property_factory()], + ) + + assert p.get_base_type_string() == "'MyClass'" + + def test_get_base_type_string_model_inners( + self, union_property_factory, date_time_property_factory, model_property_factory + ): + p = union_property_factory(inner_properties=[date_time_property_factory(), model_property_factory()]) + + assert p.get_base_type_string() == "Union['MyClass', datetime.datetime]" + def test_get_base_json_type_string(self, union_property_factory, date_time_property_factory): p = union_property_factory( inner_properties=[date_time_property_factory()], @@ -216,6 +339,9 @@ def test_get_type_imports(self, union_property_factory, date_time_property_facto class TestEnumProperty: + def test_is_base_type(self, enum_property_factory): + assert enum_property_factory().is_base_type is True + @pytest.mark.parametrize( "required, nullable, expected", ( @@ -498,6 +624,29 @@ def test_property_from_data_ref_not_found(self, mocker): parse_reference_path.assert_called_once_with(data.ref) assert prop == PropertyError(data=data, detail="Could not find reference in parsed models or enums") assert schemas == new_schemas + assert schemas.dependencies == {} + + @pytest.mark.parametrize("references_exist", (True, False)) + def test_property_from_data_ref(self, property_factory, references_exist): + from openapi_python_client.parser.properties import Schemas, property_from_data + + name = "new_name" + required = False + ref_path = "/components/schemas/RefName" + data = oai.Reference.construct(ref=f"#{ref_path}") + roots = {"new_root"} + + existing_property = property_factory(name="old_name") + references = {ref_path: {"old_root"}} if references_exist else {} + schemas = Schemas(classes_by_reference={ref_path: existing_property}, dependencies=references) + + prop, new_schemas = property_from_data( + name=name, required=required, data=data, schemas=schemas, parent_name="", config=Config(), roots=roots + ) + + assert prop == property_factory(name=name, required=required) + assert schemas == new_schemas + assert schemas.dependencies == {ref_path: {*roots, *references.get(ref_path, set())}} def test_property_from_data_invalid_ref(self, mocker): from openapi_python_client.parser.properties import PropertyError, Schemas, property_from_data @@ -588,14 +737,30 @@ def test_property_from_data_array(self, mocker): mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name) schemas = Schemas() config = MagicMock() + roots = {"root"} + process_properties = False response = property_from_data( - name=name, required=required, data=data, schemas=schemas, parent_name="parent", config=config + name=name, + required=required, + data=data, + schemas=schemas, + parent_name="parent", + config=config, + roots=roots, + process_properties=process_properties, ) assert response == build_list_property.return_value build_list_property.assert_called_once_with( - data=data, name=name, required=required, schemas=schemas, parent_name="parent", config=config + data=data, + name=name, + required=required, + schemas=schemas, + parent_name="parent", + config=config, + process_properties=process_properties, + roots=roots, ) def test_property_from_data_object(self, mocker): @@ -610,14 +775,30 @@ def test_property_from_data_object(self, mocker): mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name) schemas = Schemas() config = MagicMock() + roots = {"root"} + process_properties = False response = property_from_data( - name=name, required=required, data=data, schemas=schemas, parent_name="parent", config=config + name=name, + required=required, + data=data, + schemas=schemas, + parent_name="parent", + config=config, + process_properties=process_properties, + roots=roots, ) assert response == build_model_property.return_value build_model_property.assert_called_once_with( - data=data, name=name, required=required, schemas=schemas, parent_name="parent", config=config + data=data, + name=name, + required=required, + schemas=schemas, + parent_name="parent", + config=config, + process_properties=process_properties, + roots=roots, ) def test_property_from_data_union(self, mocker): @@ -708,7 +889,14 @@ def test_build_list_property_no_items(self, mocker): schemas = properties.Schemas() p, new_schemas = properties.build_list_property( - name=name, required=required, data=data, schemas=schemas, parent_name="parent", config=MagicMock() + name=name, + required=required, + data=data, + schemas=schemas, + parent_name="parent", + config=MagicMock(), + process_properties=True, + roots={"root"}, ) assert p == PropertyError(data=data, detail="type array must have items defined") @@ -730,9 +918,18 @@ def test_build_list_property_invalid_items(self, mocker): properties, "property_from_data", return_value=(properties.PropertyError(data="blah"), second_schemas) ) config = MagicMock() + process_properties = False + roots = {"root"} p, new_schemas = properties.build_list_property( - name=name, required=required, data=data, schemas=schemas, parent_name="parent", config=config + name=name, + required=required, + data=data, + schemas=schemas, + parent_name="parent", + config=config, + roots=roots, + process_properties=process_properties, ) assert isinstance(p, PropertyError) @@ -741,7 +938,14 @@ def test_build_list_property_invalid_items(self, mocker): assert new_schemas == second_schemas assert schemas != new_schemas, "Schema was mutated" property_from_data.assert_called_once_with( - name=f"{name}_item", required=True, data=data.items, schemas=schemas, parent_name="parent", config=config + name=f"{name}_item", + required=True, + data=data.items, + schemas=schemas, + parent_name="parent", + config=config, + process_properties=process_properties, + roots=roots, ) def test_build_list_property(self, any_property_factory): @@ -756,7 +960,14 @@ def test_build_list_property(self, any_property_factory): config = Config() p, new_schemas = properties.build_list_property( - name=name, required=True, data=data, schemas=schemas, parent_name="parent", config=config + name=name, + required=True, + data=data, + schemas=schemas, + parent_name="parent", + config=config, + roots={"root"}, + process_properties=True, ) assert isinstance(p, properties.ListProperty) @@ -916,17 +1127,18 @@ def test__string_based_property_unsupported_format(self, string_property_factory assert p == string_property_factory(name=name, required=required, nullable=nullable) -class TestBuildSchemas: +class TestCreateSchemas: def test_skips_references_and_keeps_going(self, mocker): - from openapi_python_client.parser.properties import Schemas, build_schemas + from openapi_python_client.parser.properties import Schemas, _create_schemas from openapi_python_client.schema import Reference, Schema components = {"a_ref": Reference.construct(), "a_schema": Schema.construct()} update_schemas_with_data = mocker.patch(f"{MODULE_NAME}.update_schemas_with_data") parse_reference_path = mocker.patch(f"{MODULE_NAME}.parse_reference_path") config = Config() + schemas = Schemas() - result = build_schemas(components=components, schemas=Schemas(), config=config) + result = _create_schemas(components=components, schemas=schemas, config=config) # Should not even try to parse a path for the Reference parse_reference_path.assert_called_once_with("#/components/schemas/a_schema") update_schemas_with_data.assert_called_once_with( @@ -940,7 +1152,7 @@ def test_skips_references_and_keeps_going(self, mocker): assert result == update_schemas_with_data.return_value def test_records_bad_uris_and_keeps_going(self, mocker): - from openapi_python_client.parser.properties import Schemas, build_schemas + from openapi_python_client.parser.properties import Schemas, _create_schemas from openapi_python_client.schema import Schema components = {"first": Schema.construct(), "second": Schema.construct()} @@ -949,8 +1161,9 @@ def test_records_bad_uris_and_keeps_going(self, mocker): f"{MODULE_NAME}.parse_reference_path", side_effect=[PropertyError(detail="some details"), "a_path"] ) config = Config() + schemas = Schemas() - result = build_schemas(components=components, schemas=Schemas(), config=config) + result = _create_schemas(components=components, schemas=schemas, config=config) parse_reference_path.assert_has_calls( [ call("#/components/schemas/first"), @@ -966,7 +1179,7 @@ def test_records_bad_uris_and_keeps_going(self, mocker): assert result == update_schemas_with_data.return_value def test_retries_failing_properties_while_making_progress(self, mocker): - from openapi_python_client.parser.properties import Schemas, build_schemas + from openapi_python_client.parser.properties import Schemas, _create_schemas from openapi_python_client.schema import Schema components = {"first": Schema.construct(), "second": Schema.construct()} @@ -975,8 +1188,9 @@ def test_retries_failing_properties_while_making_progress(self, mocker): ) parse_reference_path = mocker.patch(f"{MODULE_NAME}.parse_reference_path") config = Config() + schemas = Schemas() - result = build_schemas(components=components, schemas=Schemas(), config=config) + result = _create_schemas(components=components, schemas=schemas, config=config) parse_reference_path.assert_has_calls( [ call("#/components/schemas/first"), @@ -988,6 +1202,164 @@ def test_retries_failing_properties_while_making_progress(self, mocker): assert result.errors == [PropertyError()] +class TestProcessModels: + def test_retries_failing_models_while_making_progress(self, mocker, model_property_factory, property_factory): + from openapi_python_client.parser.properties import _process_models + + first_model = model_property_factory() + schemas = Schemas( + classes_by_name={ + "first": first_model, + "second": model_property_factory(), + "non-model": 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"]) + config = Config() + + 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"], 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): + from openapi_python_client.parser.properties import Class, _process_models + from openapi_python_client.schema import Reference + + class_name = "class_name" + recursive_model = model_property_factory(class_info=Class(name=class_name, module_name="module_name")) + schemas = Schemas( + classes_by_name={ + "recursive": recursive_model, + "second": model_property_factory(), + } + ) + recursion_error = PropertyError(data=Reference.construct(ref=f"#/{class_name}")) + 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"]) + config = Config() + + result = _process_models(schemas=schemas, config=config) + + process_model.assert_has_calls( + [ + call(recursive_model, schemas=schemas, config=config), + call(schemas.classes_by_name["second"], schemas=schemas, config=config), + ] + ) + assert process_model_errors.was_called_once_with([(recursive_model, recursion_error)]) + assert all(error in result.errors for error in process_model_errors.return_value) + assert "\n\nRecursive allOf reference found" in recursion_error.detail + + +class TestPropogateRemoval: + def test_propogate_removal_class_name(self): + from openapi_python_client.parser.properties import ReferencePath, _propogate_removal + from openapi_python_client.utils import ClassName + + root = ClassName("ClassName", "") + ref_path = ReferencePath("/reference") + other_class_name = ClassName("OtherClassName", "") + schemas = Schemas( + classes_by_name={root: None, other_class_name: None}, + classes_by_reference={ref_path: None}, + dependencies={ref_path: {other_class_name}, root: {ref_path}}, + ) + error = PropertyError() + + _propogate_removal(root=root, schemas=schemas, error=error) + + assert schemas.classes_by_name == {other_class_name: None} + assert schemas.classes_by_reference == {ref_path: None} + assert not error.detail + + def test_propogate_removal_ref_path(self): + from openapi_python_client.parser.properties import ReferencePath, _propogate_removal + from openapi_python_client.utils import ClassName + + root = ReferencePath("/root/reference") + class_name = ClassName("ClassName", "") + ref_path = ReferencePath("/ref/path") + schemas = Schemas( + classes_by_name={class_name: None}, + classes_by_reference={root: None, ref_path: None}, + dependencies={root: {ref_path, class_name}}, + ) + error = PropertyError() + + _propogate_removal(root=root, schemas=schemas, error=error) + + assert schemas.classes_by_name == {} + assert schemas.classes_by_reference == {} + assert error.detail == f"\n{root}\n{ref_path}" + + def test_propogate_removal_ref_path_no_refs(self): + from openapi_python_client.parser.properties import ReferencePath, _propogate_removal + from openapi_python_client.utils import ClassName + + root = ReferencePath("/root/reference") + class_name = ClassName("ClassName", "") + ref_path = ReferencePath("/ref/path") + schemas = Schemas(classes_by_name={class_name: None}, classes_by_reference={root: None, ref_path: None}) + error = PropertyError() + + _propogate_removal(root=root, schemas=schemas, error=error) + + assert schemas.classes_by_name == {class_name: None} + assert schemas.classes_by_reference == {ref_path: None} + assert error.detail == f"\n{root}" + + def test_propogate_removal_ref_path_already_removed(self): + from openapi_python_client.parser.properties import ReferencePath, _propogate_removal + from openapi_python_client.utils import ClassName + + root = ReferencePath("/root/reference") + class_name = ClassName("ClassName", "") + ref_path = ReferencePath("/ref/path") + schemas = Schemas( + classes_by_name={class_name: None}, + classes_by_reference={ref_path: None}, + dependencies={root: {ref_path, class_name}}, + ) + error = PropertyError() + + _propogate_removal(root=root, schemas=schemas, error=error) + + assert schemas.classes_by_name == {class_name: None} + assert schemas.classes_by_reference == {ref_path: None} + assert not error.detail + + +def test_process_model_errors(mocker, model_property_factory): + from openapi_python_client.parser.properties import _process_model_errors + + propogate_removal = mocker.patch(f"{MODULE_NAME}._propogate_removal") + model_errors = [ + (model_property_factory(roots={"root1", "root2"}), PropertyError(detail="existing detail")), + (model_property_factory(roots=set()), PropertyError()), + (model_property_factory(roots={"root1", "root3"}), PropertyError(detail="other existing detail")), + ] + schemas = Schemas() + + result = _process_model_errors(model_errors, schemas=schemas) + + propogate_removal.assert_has_calls( + [call(root=root, schemas=schemas, error=error) for model, error in model_errors for root in model.roots] + ) + assert result == [error for _, error in model_errors] + assert all("\n\nFailure to process schema has resulted in the removal of:" in error.detail for error in result) + + class TestBuildParameters: def test_skips_references_and_keeps_going(self, mocker): from openapi_python_client.parser.properties import Parameters, build_parameters @@ -1109,3 +1481,21 @@ def test_build_enum_property_bad_default(): assert schemas == schemas assert err == PropertyError(detail="B is an invalid default for enum Existing", data=data) + + +def test_build_schemas(mocker): + from openapi_python_client.parser.properties import Schemas, build_schemas + from openapi_python_client.schema import Reference, Schema + + create_schemas = mocker.patch(f"{MODULE_NAME}._create_schemas") + process_models = mocker.patch(f"{MODULE_NAME}._process_models") + + components = {"a_ref": Reference.construct(), "a_schema": Schema.construct()} + schemas = Schemas() + config = Config() + + result = build_schemas(components=components, schemas=schemas, config=config) + + create_schemas.assert_called_once_with(components=components, schemas=schemas, config=config) + process_models.assert_called_once_with(schemas=create_schemas.return_value, config=config) + assert result == process_models.return_value diff --git a/tests/test_parser/test_properties/test_model_property.py b/tests/test_parser/test_properties/test_model_property.py index 7b96cb687..7f8a92270 100644 --- a/tests/test_parser/test_properties/test_model_property.py +++ b/tests/test_parser/test_properties/test_model_property.py @@ -7,42 +7,74 @@ from openapi_python_client.parser.errors import PropertyError from openapi_python_client.parser.properties import StringProperty +MODULE_NAME = "openapi_python_client.parser.properties.model_property" -@pytest.mark.parametrize( - "no_optional,nullable,required,json,expected", - [ - (False, False, False, False, "Union[Unset, MyClass]"), - (False, False, True, False, "MyClass"), - (False, True, False, False, "Union[Unset, None, MyClass]"), - (False, True, True, False, "Optional[MyClass]"), - (True, False, False, False, "MyClass"), - (True, False, True, False, "MyClass"), - (True, True, False, False, "MyClass"), - (True, True, True, False, "MyClass"), - (False, False, True, True, "Dict[str, Any]"), - ], -) -def test_get_type_string(no_optional, nullable, required, json, expected, model_property_factory): - - prop = model_property_factory( - required=required, - nullable=nullable, + +class TestModelProperty: + @pytest.mark.parametrize( + "no_optional,nullable,required,json,quoted,expected", + [ + (False, False, False, False, False, "Union[Unset, MyClass]"), + (False, False, True, False, False, "MyClass"), + (False, True, False, False, False, "Union[Unset, None, MyClass]"), + (False, True, True, False, False, "Optional[MyClass]"), + (True, False, False, False, False, "MyClass"), + (True, False, True, False, False, "MyClass"), + (True, True, False, False, False, "MyClass"), + (True, True, True, False, False, "MyClass"), + (False, False, True, True, False, "Dict[str, Any]"), + (False, False, False, False, True, "Union[Unset, 'MyClass']"), + (False, False, True, False, True, "'MyClass'"), + (False, True, False, False, True, "Union[Unset, None, 'MyClass']"), + (False, True, True, False, True, "Optional['MyClass']"), + (True, False, False, False, True, "'MyClass'"), + (True, False, True, False, True, "'MyClass'"), + (True, True, False, False, True, "'MyClass'"), + (True, True, True, False, True, "'MyClass'"), + (False, False, True, True, True, "Dict[str, Any]"), + ], ) + def test_get_type_string(self, no_optional, nullable, required, json, expected, model_property_factory, quoted): + + prop = model_property_factory( + required=required, + nullable=nullable, + ) + + assert prop.get_type_string(no_optional=no_optional, json=json, quoted=quoted) == expected - assert prop.get_type_string(no_optional=no_optional, json=json) == expected + def test_get_imports(self, model_property_factory): + prop = model_property_factory(required=False, nullable=True) + + assert prop.get_imports(prefix="..") == { + "from typing import Optional", + "from typing import Union", + "from ..types import UNSET, Unset", + "from typing import Dict", + "from typing import cast", + } + + def test_get_lazy_imports(self, model_property_factory): + prop = model_property_factory(required=False, nullable=True) + + assert prop.get_lazy_imports(prefix="..") == { + "from ..models.my_module import MyClass", + } + def test_is_base_type(self, model_property_factory): + assert model_property_factory().is_base_type is False -def test_get_imports(model_property_factory): - prop = model_property_factory(required=False, nullable=True) + @pytest.mark.parametrize( + "quoted,expected", + [ + (False, "MyClass"), + (True, '"MyClass"'), + ], + ) + def test_get_base_type_string(self, quoted, expected, model_property_factory): - assert prop.get_imports(prefix="..") == { - "from typing import Optional", - "from typing import Union", - "from ..types import UNSET, Unset", - "from ..models.my_module import MyClass", - "from typing import Dict", - "from typing import cast", - } + m = model_property_factory() + assert m.get_base_type_string(quoted=quoted) == expected class TestBuildModelProperty: @@ -75,7 +107,14 @@ def test_additional_schemas(self, additional_properties_schema, expected_additio ) model, _ = build_model_property( - data=data, name="prop", schemas=Schemas(), required=True, parent_name="parent", config=Config() + data=data, + name="prop", + schemas=Schemas(), + required=True, + parent_name="parent", + config=Config(), + roots={"root"}, + process_properties=True, ) assert model.additional_properties == expected_additional_properties @@ -98,9 +137,18 @@ def test_happy_path(self, model_property_factory, string_property_factory, date_ nullable=nullable, ) schemas = Schemas(classes_by_reference={"OtherModel": None}, classes_by_name={"OtherModel": None}) + class_info = Class(name="ParentMyModel", module_name="parent_my_model") + roots = {"root"} model, new_schemas = build_model_property( - data=data, name=name, schemas=schemas, required=required, parent_name="parent", config=Config() + data=data, + name=name, + schemas=schemas, + required=required, + parent_name="parent", + config=Config(), + roots=roots, + process_properties=True, ) assert new_schemas != schemas @@ -111,11 +159,14 @@ def test_happy_path(self, model_property_factory, string_property_factory, date_ assert new_schemas.classes_by_reference == { "OtherModel": None, } + assert new_schemas.dependencies == {"root": {class_info.name}} assert model == model_property_factory( name=name, required=required, nullable=nullable, - class_info=Class(name="ParentMyModel", module_name="parent_my_model"), + roots={*roots, class_info.name}, + data=data, + class_info=class_info, required_properties=[string_property_factory(name="req", required=True)], optional_properties=[date_time_property_factory(name="opt", required=False)], description=data.description, @@ -126,6 +177,7 @@ def test_happy_path(self, model_property_factory, string_property_factory, date_ "from ..types import UNSET, Unset", "from typing import Union", }, + lazy_imports=set(), additional_properties=True, ) @@ -136,7 +188,14 @@ def test_model_name_conflict(self): schemas = Schemas(classes_by_name={"OtherModel": None}) err, new_schemas = build_model_property( - data=data, name="OtherModel", schemas=schemas, required=True, parent_name=None, config=Config() + data=data, + name="OtherModel", + schemas=schemas, + required=True, + parent_name=None, + config=Config(), + roots={"root"}, + process_properties=True, ) assert new_schemas == schemas @@ -151,7 +210,14 @@ def test_model_bad_properties(self): }, ) result = build_model_property( - data=data, name="prop", schemas=Schemas(), required=True, parent_name="parent", config=Config() + data=data, + name="prop", + schemas=Schemas(), + required=True, + parent_name="parent", + config=Config(), + roots={"root"}, + process_properties=True, )[0] assert isinstance(result, PropertyError) @@ -166,10 +232,67 @@ def test_model_bad_additional_properties(self): ) data = oai.Schema(additionalProperties=additional_properties) result = build_model_property( - data=data, name="prop", schemas=Schemas(), required=True, parent_name="parent", config=Config() + data=data, + name="prop", + schemas=Schemas(), + required=True, + parent_name="parent", + config=Config(), + roots={"root"}, + process_properties=True, )[0] assert isinstance(result, PropertyError) + def test_process_properties_false(self, model_property_factory): + from openapi_python_client.parser.properties import Class, Schemas, build_model_property + + name = "prop" + nullable = False + required = True + + data = oai.Schema.construct( + required=["req"], + title="MyModel", + properties={ + "req": oai.Schema.construct(type="string"), + "opt": oai.Schema(type="string", format="date-time"), + }, + description="A class called MyModel", + nullable=nullable, + ) + schemas = Schemas(classes_by_reference={"OtherModel": None}, classes_by_name={"OtherModel": None}) + roots = {"root"} + class_info = Class(name="ParentMyModel", module_name="parent_my_model") + + model, new_schemas = build_model_property( + data=data, + name=name, + schemas=schemas, + required=required, + parent_name="parent", + config=Config(), + roots=roots, + process_properties=False, + ) + + assert new_schemas != schemas + assert new_schemas.classes_by_name == { + "OtherModel": None, + "ParentMyModel": model, + } + assert new_schemas.classes_by_reference == { + "OtherModel": None, + } + assert model == model_property_factory( + name=name, + required=required, + nullable=nullable, + class_info=class_info, + data=data, + description=data.description, + roots={*roots, class_info.name}, + ) + class TestProcessProperties: def test_conflicting_properties_different_types( @@ -183,12 +306,16 @@ def test_conflicting_properties_different_types( ) schemas = Schemas( classes_by_reference={ - "/First": model_property_factory(optional_properties=[string_property_factory()]), - "/Second": model_property_factory(optional_properties=[date_time_property_factory()]), + "/First": model_property_factory( + required_properties=[], optional_properties=[string_property_factory()] + ), + "/Second": model_property_factory( + required_properties=[], optional_properties=[date_time_property_factory()] + ), } ) - result = _process_properties(data=data, schemas=schemas, class_name="", config=Config()) + result = _process_properties(data=data, schemas=schemas, class_name="", config=Config(), roots={"root"}) assert isinstance(result, PropertyError) @@ -202,18 +329,39 @@ def test_process_properties_reference_not_exist(self): }, ) - result = _process_properties(data=data, class_name="", schemas=Schemas(), config=Config()) + result = _process_properties(data=data, class_name="", schemas=Schemas(), config=Config(), roots={"root"}) + + assert isinstance(result, PropertyError) + + def test_process_properties_all_of_reference_not_exist(self): + from openapi_python_client.parser.properties import Schemas + from openapi_python_client.parser.properties.model_property import _process_properties + + data = oai.Schema.construct(allOf=[oai.Reference.construct(ref="#/components/schema/NotExist")]) + + result = _process_properties(data=data, class_name="", schemas=Schemas(), config=Config(), roots={"root"}) assert isinstance(result, PropertyError) - def test_invalid_reference(self, model_property_factory): + def test_process_properties_model_property_roots(self, model_property_factory): + from openapi_python_client.parser.properties import Schemas + from openapi_python_client.parser.properties.model_property import _process_properties + + roots = {"root"} + data = oai.Schema(properties={"test_model_property": oai.Schema.construct(type="object")}) + + result = _process_properties(data=data, class_name="", schemas=Schemas(), config=Config(), roots=roots) + + assert all(root in result.optional_props[0].roots for root in roots) + + def test_invalid_reference(self): from openapi_python_client.parser.properties import Schemas from openapi_python_client.parser.properties.model_property import _process_properties data = oai.Schema.construct(allOf=[oai.Reference.construct(ref="ThisIsNotGood")]) schemas = Schemas() - result = _process_properties(data=data, schemas=schemas, class_name="", config=Config()) + result = _process_properties(data=data, schemas=schemas, class_name="", config=Config(), roots={"root"}) assert isinstance(result, PropertyError) @@ -228,7 +376,22 @@ def test_non_model_reference(self, enum_property_factory): } ) - result = _process_properties(data=data, schemas=schemas, class_name="", config=Config()) + result = _process_properties(data=data, schemas=schemas, class_name="", config=Config(), roots={"root"}) + + assert isinstance(result, PropertyError) + + def test_reference_not_processed(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="#/Unprocessed")]) + schemas = Schemas( + classes_by_reference={ + "/Unprocessed": model_property_factory(), + } + ) + + result = _process_properties(data=data, schemas=schemas, class_name="", config=Config(), roots={"root"}) assert isinstance(result, PropertyError) @@ -241,12 +404,16 @@ def test_conflicting_properties_same_types(self, model_property_factory, string_ ) schemas = Schemas( classes_by_reference={ - "/First": model_property_factory(optional_properties=[string_property_factory(default="abc")]), - "/Second": model_property_factory(optional_properties=[string_property_factory()]), + "/First": model_property_factory( + required_properties=[], optional_properties=[string_property_factory(default="abc")] + ), + "/Second": model_property_factory( + required_properties=[], optional_properties=[string_property_factory()] + ), } ) - result = _process_properties(data=data, schemas=schemas, class_name="", config=Config()) + result = _process_properties(data=data, schemas=schemas, class_name="", config=Config(), roots={"root"}) assert isinstance(result, PropertyError) @@ -263,13 +430,14 @@ def test_allof_string_and_string_enum(self, model_property_factory, enum_propert schemas = Schemas( classes_by_reference={ "/First": model_property_factory( - optional_properties=[string_property_factory(required=False, nullable=True)] + required_properties=[], + optional_properties=[string_property_factory(required=False, nullable=True)], ), - "/Second": model_property_factory(optional_properties=[enum_property]), + "/Second": model_property_factory(required_properties=[], optional_properties=[enum_property]), } ) - result = _process_properties(data=data, schemas=schemas, class_name="", config=Config()) + result = _process_properties(data=data, schemas=schemas, class_name="", config=Config(), roots={"root"}) assert result.required_props[0] == enum_property def test_allof_string_enum_and_string(self, model_property_factory, enum_property_factory, string_property_factory): @@ -286,14 +454,15 @@ def test_allof_string_enum_and_string(self, model_property_factory, enum_propert ) schemas = Schemas( classes_by_reference={ - "/First": model_property_factory(optional_properties=[enum_property]), + "/First": model_property_factory(required_properties=[], optional_properties=[enum_property]), "/Second": model_property_factory( - optional_properties=[string_property_factory(required=False, nullable=True)] + required_properties=[], + optional_properties=[string_property_factory(required=False, nullable=True)], ), } ) - result = _process_properties(data=data, schemas=schemas, class_name="", config=Config()) + result = _process_properties(data=data, schemas=schemas, class_name="", config=Config(), roots={"root"}) assert result.optional_props[0] == enum_property def test_allof_int_and_int_enum(self, model_property_factory, enum_property_factory, int_property_factory): @@ -309,12 +478,12 @@ def test_allof_int_and_int_enum(self, model_property_factory, enum_property_fact ) schemas = Schemas( classes_by_reference={ - "/First": model_property_factory(optional_properties=[int_property_factory()]), - "/Second": model_property_factory(optional_properties=[enum_property]), + "/First": model_property_factory(required_properties=[], optional_properties=[int_property_factory()]), + "/Second": model_property_factory(required_properties=[], optional_properties=[enum_property]), } ) - result = _process_properties(data=data, schemas=schemas, class_name="", config=Config()) + result = _process_properties(data=data, schemas=schemas, class_name="", config=Config(), roots={"root"}) assert result.required_props[0] == enum_property def test_allof_enum_incompatible_type(self, model_property_factory, enum_property_factory, int_property_factory): @@ -330,12 +499,12 @@ def test_allof_enum_incompatible_type(self, model_property_factory, enum_propert ) schemas = Schemas( classes_by_reference={ - "/First": model_property_factory(optional_properties=[int_property_factory()]), - "/Second": model_property_factory(optional_properties=[enum_property]), + "/First": model_property_factory(required_properties=[], optional_properties=[int_property_factory()]), + "/Second": model_property_factory(required_properties=[], optional_properties=[enum_property]), } ) - result = _process_properties(data=data, schemas=schemas, class_name="", config=Config()) + result = _process_properties(data=data, schemas=schemas, class_name="", config=Config(), roots={"root"}) assert isinstance(result, PropertyError) def test_allof_string_enums(self, model_property_factory, enum_property_factory): @@ -357,12 +526,12 @@ def test_allof_string_enums(self, model_property_factory, enum_property_factory) ) schemas = Schemas( classes_by_reference={ - "/First": model_property_factory(optional_properties=[enum_property1]), - "/Second": model_property_factory(optional_properties=[enum_property2]), + "/First": model_property_factory(required_properties=[], optional_properties=[enum_property1]), + "/Second": model_property_factory(required_properties=[], optional_properties=[enum_property2]), } ) - result = _process_properties(data=data, schemas=schemas, class_name="", config=Config()) + result = _process_properties(data=data, schemas=schemas, class_name="", config=Config(), roots={"root"}) assert result.required_props[0] == enum_property1 def test_allof_int_enums(self, model_property_factory, enum_property_factory): @@ -384,12 +553,12 @@ def test_allof_int_enums(self, model_property_factory, enum_property_factory): ) schemas = Schemas( classes_by_reference={ - "/First": model_property_factory(optional_properties=[enum_property1]), - "/Second": model_property_factory(optional_properties=[enum_property2]), + "/First": model_property_factory(required_properties=[], optional_properties=[enum_property1]), + "/Second": model_property_factory(required_properties=[], optional_properties=[enum_property2]), } ) - result = _process_properties(data=data, schemas=schemas, class_name="", config=Config()) + result = _process_properties(data=data, schemas=schemas, class_name="", config=Config(), roots={"root"}) assert result.required_props[0] == enum_property2 def test_allof_enums_are_not_subsets(self, model_property_factory, enum_property_factory): @@ -411,12 +580,12 @@ def test_allof_enums_are_not_subsets(self, model_property_factory, enum_property ) schemas = Schemas( classes_by_reference={ - "/First": model_property_factory(optional_properties=[enum_property1]), - "/Second": model_property_factory(optional_properties=[enum_property2]), + "/First": model_property_factory(required_properties=[], optional_properties=[enum_property1]), + "/Second": model_property_factory(required_properties=[], optional_properties=[enum_property2]), } ) - result = _process_properties(data=data, schemas=schemas, class_name="", config=Config()) + result = _process_properties(data=data, schemas=schemas, class_name="", config=Config(), roots={"root"}) assert isinstance(result, PropertyError) def test_duplicate_properties(self, model_property_factory, string_property_factory): @@ -429,12 +598,12 @@ def test_duplicate_properties(self, model_property_factory, string_property_fact prop = string_property_factory(nullable=True) schemas = Schemas( classes_by_reference={ - "/First": model_property_factory(optional_properties=[prop]), - "/Second": model_property_factory(optional_properties=[prop]), + "/First": model_property_factory(required_properties=[], optional_properties=[prop]), + "/Second": model_property_factory(required_properties=[], optional_properties=[prop]), } ) - result = _process_properties(data=data, schemas=schemas, class_name="", config=Config()) + result = _process_properties(data=data, schemas=schemas, class_name="", config=Config(), roots={"root"}) assert result.optional_props == [prop], "There should only be one copy of duplicate properties" @@ -460,15 +629,18 @@ def test_mixed_requirements( schemas = Schemas( classes_by_reference={ "/First": model_property_factory( - optional_properties=[string_property_factory(required=first_required, nullable=first_nullable)] + required_properties=[], + optional_properties=[string_property_factory(required=first_required, nullable=first_nullable)], ), "/Second": model_property_factory( - optional_properties=[string_property_factory(required=second_required, nullable=second_nullable)] + required_properties=[], + optional_properties=[string_property_factory(required=second_required, nullable=second_nullable)], ), } ) + roots = {"root"} - result = _process_properties(data=data, schemas=schemas, class_name="", config=MagicMock()) + result = _process_properties(data=data, schemas=schemas, class_name="", config=MagicMock(), roots=roots) nullable = first_nullable and second_nullable required = first_required or second_required @@ -477,6 +649,7 @@ def test_mixed_requirements( required=required, ) + assert result.schemas.dependencies == {"/First": roots, "/Second": roots} if nullable or not required: assert result.optional_props == [expected_prop] else: @@ -499,7 +672,64 @@ def test_direct_properties_non_ref(self, string_property_factory): ) schemas = Schemas() - result = _process_properties(data=data, schemas=schemas, class_name="", config=MagicMock()) + result = _process_properties(data=data, schemas=schemas, class_name="", config=MagicMock(), roots={"root"}) assert result.optional_props == [string_property_factory(name="second", required=False, nullable=False)] assert result.required_props == [string_property_factory(name="first", required=True, nullable=False)] + + +class TestProcessModel: + def test_process_model_error(self, mocker, model_property_factory): + from openapi_python_client.parser.properties import Schemas + from openapi_python_client.parser.properties.model_property import process_model + + model_prop = model_property_factory() + schemas = Schemas() + process_property_data = mocker.patch(f"{MODULE_NAME}._process_property_data") + process_property_data.return_value = (PropertyError(), schemas) + + result = process_model(model_prop=model_prop, schemas=schemas, config=Config()) + + assert result == PropertyError() + assert model_prop.required_properties is None + assert model_prop.optional_properties is None + assert model_prop.relative_imports is None + assert model_prop.additional_properties is None + + def test_process_model(self, mocker, model_property_factory): + from openapi_python_client.parser.properties import Schemas + from openapi_python_client.parser.properties.model_property import _PropertyData, process_model + + model_prop = model_property_factory() + schemas = Schemas() + property_data = _PropertyData( + required_props=["required"], + optional_props=["optional"], + relative_imports={"relative"}, + lazy_imports={"lazy"}, + schemas=schemas, + ) + additional_properties = True + process_property_data = mocker.patch(f"{MODULE_NAME}._process_property_data") + process_property_data.return_value = ((property_data, additional_properties), schemas) + + result = process_model(model_prop=model_prop, schemas=schemas, config=Config()) + + assert result == schemas + assert model_prop.required_properties == property_data.required_props + assert model_prop.optional_properties == property_data.optional_props + assert model_prop.relative_imports == property_data.relative_imports + assert model_prop.lazy_imports == property_data.lazy_imports + assert model_prop.additional_properties == additional_properties + + +def test_set_relative_imports(model_property_factory): + from openapi_python_client.parser.properties import Class + from openapi_python_client.parser.properties.model_property import ModelProperty + + class_info = Class("ClassName", module_name="module_name") + relative_imports = {"from typing import List", f"from ..models.{class_info.module_name} import {class_info.name}"} + + model_property = model_property_factory(class_info=class_info, relative_imports=relative_imports) + + assert model_property.relative_imports == {"from typing import List"} diff --git a/tests/test_parser/test_properties/test_property.py b/tests/test_parser/test_properties/test_property.py index 00da3fe46..aa1b3fb4f 100644 --- a/tests/test_parser/test_properties/test_property.py +++ b/tests/test_parser/test_properties/test_property.py @@ -2,34 +2,39 @@ class TestProperty: + def test_is_base_type(self, property_factory): + assert property_factory().is_base_type is True + @pytest.mark.parametrize( - "nullable,required,no_optional,json,expected", + "nullable,required,no_optional,json,quoted,expected", [ - (False, False, False, False, "Union[Unset, TestType]"), - (False, False, True, False, "TestType"), - (False, True, False, False, "TestType"), - (False, True, True, False, "TestType"), - (True, False, False, False, "Union[Unset, None, TestType]"), - (True, False, True, False, "TestType"), - (True, True, False, False, "Optional[TestType]"), - (True, True, True, False, "TestType"), - (False, False, False, True, "Union[Unset, str]"), - (False, False, True, True, "str"), - (False, True, False, True, "str"), - (False, True, True, True, "str"), - (True, False, False, True, "Union[Unset, None, str]"), - (True, False, True, True, "str"), - (True, True, False, True, "Optional[str]"), - (True, True, True, True, "str"), + (False, False, False, False, False, "Union[Unset, TestType]"), + (False, False, True, False, False, "TestType"), + (False, True, False, False, False, "TestType"), + (False, True, True, False, False, "TestType"), + (True, False, False, False, False, "Union[Unset, None, TestType]"), + (True, False, True, False, False, "TestType"), + (True, True, False, False, False, "Optional[TestType]"), + (True, True, True, False, False, "TestType"), + (False, False, False, True, False, "Union[Unset, str]"), + (False, False, True, True, False, "str"), + (False, True, False, True, False, "str"), + (False, True, True, True, False, "str"), + (True, False, False, True, False, "Union[Unset, None, str]"), + (True, False, False, True, True, "Union[Unset, None, str]"), + (True, False, True, True, False, "str"), + (True, True, False, True, False, "Optional[str]"), + (True, True, True, True, False, "str"), + (True, True, True, True, True, "str"), ], ) - def test_get_type_string(self, property_factory, mocker, nullable, required, no_optional, json, expected): + def test_get_type_string(self, property_factory, mocker, nullable, required, no_optional, json, expected, quoted): from openapi_python_client.parser.properties import Property mocker.patch.object(Property, "_type_string", "TestType") mocker.patch.object(Property, "_json_type_string", "str") p = property_factory(required=required, nullable=nullable) - assert p.get_type_string(no_optional=no_optional, json=json) == expected + assert p.get_type_string(no_optional=no_optional, json=json, quoted=quoted) == expected @pytest.mark.parametrize( "default,required,expected", @@ -60,3 +65,31 @@ def test_get_imports(self, property_factory): "from typing import Optional", "from typing import Union", } + + @pytest.mark.parametrize( + "quoted,expected", + [ + (False, "TestType"), + (True, "TestType"), + ], + ) + def test_get_base_type_string(self, quoted, expected, property_factory, mocker): + from openapi_python_client.parser.properties import Property + + mocker.patch.object(Property, "_type_string", "TestType") + p = property_factory() + assert p.get_base_type_string(quoted=quoted) is expected + + @pytest.mark.parametrize( + "quoted,expected", + [ + (False, "str"), + (True, "str"), + ], + ) + def test_get_base_json_type_string(self, quoted, expected, property_factory, mocker): + from openapi_python_client.parser.properties import Property + + mocker.patch.object(Property, "_json_type_string", "str") + p = property_factory() + assert p.get_base_json_type_string(quoted=quoted) is expected