From 174f090ebbf3966091653d56eea580f4c343b40a Mon Sep 17 00:00:00 2001 From: maz808 Date: Mon, 24 Jan 2022 23:42:30 -0600 Subject: [PATCH 01/27] Add support for recursive/circular refs This commit adds support for recursive and circular references in object properties and additionalProperties, but not in allOf. The changes include: -Delayed processing of schema model properties -Cascading removal of invalid schema reference dependencies -Prevention of self import in ModelProperty relative imports -Prevention of forward recursive type reference errors in generated modules -Logging for detection of recursive references in allOf --- .../my_test_api_client/models/__init__.py | 6 + .../models/model_with_circular_ref_a.py | 65 +++++ .../models/model_with_circular_ref_b.py | 65 +++++ ...circular_ref_in_additional_properties_a.py | 54 ++++ ...circular_ref_in_additional_properties_b.py | 54 ++++ .../models/model_with_recursive_ref.py | 64 +++++ ..._recursive_ref_in_additional_properties.py | 52 ++++ end_to_end_tests/openapi.json | 42 +++ .../parser/properties/__init__.py | 139 +++++++++- .../parser/properties/model_property.py | 168 +++++++++-- .../parser/properties/property.py | 26 +- .../parser/properties/schemas.py | 31 ++- .../templates/model.py.jinja | 4 +- tests/conftest.py | 11 +- .../test_parser/test_properties/test_init.py | 262 +++++++++++++++++- .../test_properties/test_model_property.py | 260 ++++++++++++++--- 16 files changed, 1178 insertions(+), 125 deletions(-) create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_a.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_b.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_in_additional_properties_a.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_in_additional_properties_b.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/model_with_recursive_ref.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/model_with_recursive_ref_in_additional_properties.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py index 85e5243ec..51a2a60a4 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 @@ -31,10 +31,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 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..f9adb4bb1 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_a.py @@ -0,0 +1,65 @@ +from typing import Any, Dict, List, Type, TypeVar, Union + +import attr + +from ..models.model_with_circular_ref_b import ModelWithCircularRefB +from ..types import UNSET, Unset + +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: + 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..25fa39b5f --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_b.py @@ -0,0 +1,65 @@ +from typing import Any, Dict, List, Type, TypeVar, Union + +import attr + +from ..models.model_with_circular_ref_a import ModelWithCircularRefA +from ..types import UNSET, Unset + +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: + 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..3349d1429 --- /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,54 @@ +from typing import Any, Dict, List, Type, TypeVar + +import attr + +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]: + + 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_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..99a9c5ed2 --- /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,54 @@ +from typing import Any, Dict, List, Type, TypeVar + +import attr + +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]: + + 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_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_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/openapi.json b/end_to_end_tests/openapi.json index b958e530b..a058567cb 100644 --- a/end_to_end_tests/openapi.json +++ b/end_to_end_tests/openapi.json @@ -1916,6 +1916,48 @@ "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" + } } } } diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index 524ff5ba0..c4c6a903e 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -20,9 +20,9 @@ from ..errors import 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, Schemas, parse_reference_path, update_schemas_with_data +from .schemas import Class, ReferencePath, Schemas, parse_reference_path, update_schemas_with_data @attr.s(auto_attribs=True, frozen=True) @@ -246,7 +246,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, + *, + model_parent: Optional[ModelProperty] = None, # pylint:disable=unused-argument + ) -> 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 @@ -484,7 +490,14 @@ 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, + 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. @@ -504,7 +517,13 @@ 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, + roots=roots, ) if isinstance(inner_prop, PropertyError): inner_prop.header = f'invalid data in items of array named "{name}"' @@ -532,6 +551,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): @@ -554,6 +574,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 @@ -565,17 +586,20 @@ def _property_from_data( schemas: Schemas, parent_name: str, config: Config, + 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: @@ -635,11 +659,23 @@ 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, + 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, + roots=roots, ) return ( AnyProperty( @@ -663,6 +699,7 @@ def property_from_data( schemas: Schemas, parent_name: str, config: Config, + 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 @@ -682,23 +719,30 @@ 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. - + 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, + 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] = [] @@ -727,4 +771,73 @@ def build_schemas( to_process = next_round schemas.errors.extend(errors) + object.__setattr__(schemas, "schemas_created", True) + 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_reference.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 diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index 6e68a8f8e..01a756d5a 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -9,7 +9,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,17 +17,28 @@ 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]] + 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 __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) -> str: return self.class_info.name @@ -42,13 +53,21 @@ 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}", + f"from {prefix}{self.self_import}", "from typing import Dict", "from typing import cast", } ) return imports + 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 _values_are_subset(first: EnumProperty, second: EnumProperty) -> bool: return set(first.values.items()) <= set(second.values.items()) @@ -111,9 +130,14 @@ class _PropertyData(NamedTuple): 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 @@ -145,10 +169,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 +187,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) @@ -185,8 +221,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 +244,79 @@ 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="..")) + 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) + 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, + roots: Set[Union[ReferencePath, utils.ClassName]], ) -> Tuple[Union[ModelProperty, PropertyError], Schemas]: """ A single ModelProperty from its OAI data @@ -230,31 +334,39 @@ def build_model_property( 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 + additional_properties: Optional[Union[bool, Property]] = None + if schemas.schemas_created: + 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 + 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, + 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..9814faeb6 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: @@ -68,7 +73,9 @@ 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_type_string( + self, no_optional: bool = False, json: bool = False, *, model_parent: Optional[ModelProperty] = None + ) -> str: """ Get a string representation of type that should be used when declaring this property @@ -81,6 +88,9 @@ def get_type_string(self, no_optional: bool = False, json: bool = False) -> str: else: type_string = self.get_base_type_string() + if model_parent and type_string == model_parent.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: @@ -111,8 +121,12 @@ def get_imports(self, *, prefix: str) -> Set[str]: imports.add(f"from {prefix}types import UNSET, Unset") return imports - def to_string(self) -> str: - """How this should be declared in a dataclass""" + def to_string(self, *, model_parent: Optional[ModelProperty] = None) -> str: + """How this should be declared in a dataclass + + Args: + model_parent: The ModelProperty which contains this Property (used for template type annotations) + """ default: Optional[str] if self.default is not None: default = self.default @@ -122,8 +136,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(model_parent=model_parent)} = {default}" + return f"{self.python_name}: {self.get_type_string(model_parent=model_parent)}" def to_docstring(self) -> str: """Returns property docstring""" diff --git a/openapi_python_client/parser/properties/schemas.py b/openapi_python_client/parser/properties/schemas.py index 9951f149f..e41225f2f 100644 --- a/openapi_python_client/parser/properties/schemas.py +++ b/openapi_python_client/parser/properties/schemas.py @@ -1,6 +1,6 @@ __all__ = ["Class", "Schemas", "parse_reference_path", "update_schemas_with_data"] -from typing import TYPE_CHECKING, Dict, List, NewType, Union, cast +from typing import TYPE_CHECKING, Dict, List, NewType, Set, Union, cast from urllib.parse import urlparse import attr @@ -16,10 +16,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. @@ -30,7 +30,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) @@ -63,13 +63,25 @@ 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) + schemas_created: bool = False + 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. @@ -90,17 +102,12 @@ 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, roots={ref_path} ) if isinstance(prop, PropertyError): prop.detail = f"{prop.header}: {prop.detail}" 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" - ) return prop schemas = attr.evolve(schemas, classes_by_reference={ref_path: prop, **schemas.classes_by_reference}) diff --git a/openapi_python_client/templates/model.py.jinja b/openapi_python_client/templates/model.py.jinja index 07f929d66..1a5ce1879 100644 --- a/openapi_python_client/templates/model.py.jinja +++ b/openapi_python_client/templates/model.py.jinja @@ -18,7 +18,7 @@ from ..types import UNSET, Unset {% 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(model_parent=model) %} {% endif %} {% set class_name = model.class_info.name %} @@ -57,7 +57,7 @@ class {{ class_name }}: {% endfor %} {% for property in model.required_properties + model.optional_properties %} {% if property.default is not none or not property.required %} - {{ property.to_string() }} + {{ property.to_string(model_parent=model) }} {% endif %} {% endfor %} {% if model.additional_properties %} diff --git a/tests/conftest.py b/tests/conftest.py index 2a683f102..ea0e71367 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,7 @@ import pytest +from openapi_python_client import schema as oai from openapi_python_client.parser.properties import ( AnyProperty, DateProperty, @@ -32,10 +33,12 @@ 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, + "additional_properties": None, "python_name": "", "description": "", "example": "", diff --git a/tests/test_parser/test_properties/test_init.py b/tests/test_parser/test_properties/test_init.py index 3d2de6519..faa8e1b4e 100644 --- a/tests/test_parser/test_properties/test_init.py +++ b/tests/test_parser/test_properties/test_init.py @@ -505,6 +505,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 @@ -595,14 +618,21 @@ 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"} 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 ) 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, + roots=roots, ) def test_property_from_data_object(self, mocker): @@ -617,14 +647,21 @@ 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"} 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 ) 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, + roots=roots, ) def test_property_from_data_union(self, mocker): @@ -715,7 +752,13 @@ 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(), + roots={"root"}, ) assert p == PropertyError(data=data, detail="type array must have items defined") @@ -737,9 +780,10 @@ def test_build_list_property_invalid_items(self, mocker): properties, "property_from_data", return_value=(properties.PropertyError(data="blah"), second_schemas) ) config = MagicMock() + 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 ) assert isinstance(p, PropertyError) @@ -748,7 +792,13 @@ 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, + roots=roots, ) def test_build_list_property(self, any_property_factory): @@ -763,7 +813,7 @@ 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"} ) assert isinstance(p, properties.ListProperty) @@ -923,17 +973,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( @@ -945,9 +996,10 @@ def test_skips_references_and_keeps_going(self, mocker): ), ) assert result == update_schemas_with_data.return_value + assert result.schemas_created 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()} @@ -956,8 +1008,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"), @@ -971,9 +1024,10 @@ def test_records_bad_uris_and_keeps_going(self, mocker): schemas=Schemas(errors=[PropertyError(detail="some details", data=components["first"])]), ) assert result == update_schemas_with_data.return_value + assert result.schemas_created 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()} @@ -982,8 +1036,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"), @@ -993,6 +1048,165 @@ def test_retries_failing_properties_while_making_progress(self, mocker): ) assert update_schemas_with_data.call_count == 3 assert result.errors == [PropertyError()] + assert result.schemas_created + + +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_reference={ + "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_reference["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_reference={ + "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_reference["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) def test_build_enum_property_conflict(): @@ -1038,3 +1252,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..12a2eef82 100644 --- a/tests/test_parser/test_properties/test_model_property.py +++ b/tests/test_parser/test_properties/test_model_property.py @@ -7,6 +7,8 @@ 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", @@ -75,7 +77,13 @@ 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(schemas_created=True), + required=True, + parent_name="parent", + config=Config(), + roots={"root"}, ) assert model.additional_properties == expected_additional_properties @@ -97,10 +105,14 @@ def test_happy_path(self, model_property_factory, string_property_factory, date_ description="A class called MyModel", nullable=nullable, ) - schemas = Schemas(classes_by_reference={"OtherModel": None}, classes_by_name={"OtherModel": None}) + schemas = Schemas( + classes_by_reference={"OtherModel": None}, classes_by_name={"OtherModel": None}, schemas_created=True + ) + 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 ) assert new_schemas != schemas @@ -111,11 +123,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, @@ -136,7 +151,13 @@ 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"}, ) assert new_schemas == schemas @@ -151,7 +172,13 @@ 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(schemas_created=True), + required=True, + parent_name="parent", + config=Config(), + roots={"root"}, )[0] assert isinstance(result, PropertyError) @@ -166,10 +193,59 @@ 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(schemas_created=True), + required=True, + parent_name="parent", + config=Config(), + roots={"root"}, )[0] assert isinstance(result, PropertyError) + def test_schemas_not_created(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 + ) + + 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 +259,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 +282,31 @@ 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_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(schemas_created=True), 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 +321,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 +349,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 +375,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 +399,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 +423,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 +444,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 +471,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 +498,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 +525,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 +543,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 +574,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 +594,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 +617,59 @@ 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"}, 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.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"} From d9cb65d189fd617d43fd0126bc4d2dec736b8f71 Mon Sep 17 00:00:00 2001 From: maz808 Date: Sat, 29 Jan 2022 15:07:24 -0600 Subject: [PATCH 02/27] Fix array schema object item issues Previous changes prevented models defined in array schemas from being built properly. This commit fixes that issue and adds support for recursive and circular references in array schema item objects, a behavior that was previously expected but not functioning correctly. This does not add support for recursive and circular references directly in an array schema's 'items' section. However, this feature would be functionally useless, so a warning is logged on detection. --- .../my_test_api_client/models/__init__.py | 12 +++ ...h_a_circular_ref_in_items_object_a_item.py | 75 +++++++++++++++++ ...ems_object_additional_properties_a_item.py | 83 +++++++++++++++++++ ...ems_object_additional_properties_b_item.py | 83 +++++++++++++++++++ ...h_a_circular_ref_in_items_object_b_item.py | 75 +++++++++++++++++ ...items_object_additional_properties_item.py | 79 ++++++++++++++++++ ...th_a_recursive_ref_in_items_object_item.py | 74 +++++++++++++++++ end_to_end_tests/openapi.json | 60 ++++++++++++++ .../parser/properties/__init__.py | 12 ++- .../parser/properties/model_property.py | 5 +- .../parser/properties/property.py | 7 +- .../parser/properties/schemas.py | 15 +++- .../test_parser/test_properties/test_init.py | 54 +++++++++--- .../test_properties/test_model_property.py | 38 ++++++--- 14 files changed, 642 insertions(+), 30 deletions(-) create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_a_item.py create mode 100644 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 create mode 100644 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 create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_b_item.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_additional_properties_item.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_item.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py index 51a2a60a4..ff5f15941 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 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..ee802e42d --- /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,75 @@ +from typing import Any, Dict, List, Type, TypeVar, Union + +import attr + +from ..models.an_array_with_a_circular_ref_in_items_object_b_item import AnArrayWithACircularRefInItemsObjectBItem +from ..types import UNSET, Unset + +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: + 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..125e2a8be --- /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,83 @@ +from typing import Any, Dict, List, Type, TypeVar + +import attr + +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]: + + 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: + 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..b984f2d12 --- /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,83 @@ +from typing import Any, Dict, List, Type, TypeVar + +import attr + +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]: + + 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: + 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..6224bbc1e --- /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,75 @@ +from typing import Any, Dict, List, Type, TypeVar, Union + +import attr + +from ..models.an_array_with_a_circular_ref_in_items_object_a_item import AnArrayWithACircularRefInItemsObjectAItem +from ..types import UNSET, Unset + +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: + 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..7f6641985 --- /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/openapi.json b/end_to_end_tests/openapi.json index a058567cb..6a924b8a0 100644 --- a/end_to_end_tests/openapi.json +++ b/end_to_end_tests/openapi.json @@ -1958,6 +1958,66 @@ "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" + } + } } } } diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index c4c6a903e..1167ddf0b 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -497,6 +497,7 @@ def build_list_property( schemas: Schemas, parent_name: str, config: Config, + process_properties: bool, roots: Set[Union[ReferencePath, utils.ClassName]], ) -> Tuple[Union[ListProperty[Any], PropertyError], Schemas]: """ @@ -523,6 +524,7 @@ def build_list_property( schemas=schemas, parent_name=parent_name, config=config, + process_properties=process_properties, roots=roots, ) if isinstance(inner_prop, PropertyError): @@ -586,6 +588,7 @@ 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""" @@ -665,6 +668,7 @@ def _property_from_data( schemas=schemas, parent_name=parent_name, config=config, + process_properties=process_properties, roots=roots, ) if data.type == oai.DataType.OBJECT or data.allOf: @@ -675,6 +679,7 @@ def _property_from_data( required=required, parent_name=parent_name, config=config, + process_properties=process_properties, roots=roots, ) return ( @@ -699,6 +704,7 @@ 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]: """ @@ -719,6 +725,8 @@ 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: @@ -734,6 +742,7 @@ def property_from_data( schemas=schemas, parent_name=parent_name, config=config, + process_properties=process_properties, roots=roots, ) except ValidationError: @@ -771,7 +780,6 @@ def _create_schemas( to_process = next_round schemas.errors.extend(errors) - object.__setattr__(schemas, "schemas_created", True) return schemas @@ -799,7 +807,7 @@ def _process_model_errors( def _process_models(*, schemas: Schemas, config: Config) -> Schemas: - to_process = (prop for prop in schemas.classes_by_reference.values() if isinstance(prop, ModelProperty)) + 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]] = [] diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index 01a756d5a..b4f51297b 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -316,6 +316,7 @@ def build_model_property( required: bool, parent_name: Optional[str], config: Config, + process_properties: bool, roots: Set[Union[ReferencePath, utils.ClassName]], ) -> Tuple[Union[ModelProperty, PropertyError], Schemas]: """ @@ -329,6 +330,8 @@ 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: @@ -339,7 +342,7 @@ def build_model_property( optional_properties: Optional[List[Property]] = None relative_imports: Optional[Set[str]] = None additional_properties: Optional[Union[bool, Property]] = None - if schemas.schemas_created: + if process_properties: data_or_err, schemas = _process_property_data( data=data, schemas=schemas, class_info=class_info, config=config, roots=model_roots ) diff --git a/openapi_python_client/parser/properties/property.py b/openapi_python_client/parser/properties/property.py index 9814faeb6..ff0817cb3 100644 --- a/openapi_python_client/parser/properties/property.py +++ b/openapi_python_client/parser/properties/property.py @@ -88,8 +88,11 @@ def get_type_string( else: type_string = self.get_base_type_string() - if model_parent and type_string == model_parent.class_info.name: - type_string = f"'{type_string}'" + if model_parent: + if type_string == model_parent.class_info.name: + type_string = f"'{type_string}'" + if type_string == f"List[{model_parent.class_info.name}]": + type_string = f"List['{model_parent.class_info.name}']" if no_optional or (self.required and not self.nullable): return type_string diff --git a/openapi_python_client/parser/properties/schemas.py b/openapi_python_client/parser/properties/schemas.py index e41225f2f..6ac396008 100644 --- a/openapi_python_client/parser/properties/schemas.py +++ b/openapi_python_client/parser/properties/schemas.py @@ -64,7 +64,6 @@ 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) - schemas_created: bool = False 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) @@ -102,12 +101,24 @@ 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, roots={ref_path} + 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): prop.detail = f"{prop.header}: {prop.detail}" 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 directly in an array schema's 'items' section" + ) return prop schemas = attr.evolve(schemas, classes_by_reference={ref_path: prop, **schemas.classes_by_reference}) diff --git a/tests/test_parser/test_properties/test_init.py b/tests/test_parser/test_properties/test_init.py index faa8e1b4e..73a6c08ae 100644 --- a/tests/test_parser/test_properties/test_init.py +++ b/tests/test_parser/test_properties/test_init.py @@ -619,9 +619,17 @@ def test_property_from_data_array(self, mocker): 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, roots=roots + 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 @@ -632,6 +640,7 @@ def test_property_from_data_array(self, mocker): schemas=schemas, parent_name="parent", config=config, + process_properties=process_properties, roots=roots, ) @@ -648,9 +657,17 @@ def test_property_from_data_object(self, mocker): 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, roots=roots + 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 @@ -661,6 +678,7 @@ def test_property_from_data_object(self, mocker): schemas=schemas, parent_name="parent", config=config, + process_properties=process_properties, roots=roots, ) @@ -758,6 +776,7 @@ def test_build_list_property_no_items(self, mocker): schemas=schemas, parent_name="parent", config=MagicMock(), + process_properties=True, roots={"root"}, ) @@ -780,10 +799,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, roots=roots + name=name, + required=required, + data=data, + schemas=schemas, + parent_name="parent", + config=config, + roots=roots, + process_properties=process_properties, ) assert isinstance(p, PropertyError) @@ -798,6 +825,7 @@ def test_build_list_property_invalid_items(self, mocker): schemas=schemas, parent_name="parent", config=config, + process_properties=process_properties, roots=roots, ) @@ -813,7 +841,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, roots={"root"} + name=name, + required=True, + data=data, + schemas=schemas, + parent_name="parent", + config=config, + roots={"root"}, + process_properties=True, ) assert isinstance(p, properties.ListProperty) @@ -996,7 +1031,6 @@ def test_skips_references_and_keeps_going(self, mocker): ), ) assert result == update_schemas_with_data.return_value - assert result.schemas_created def test_records_bad_uris_and_keeps_going(self, mocker): from openapi_python_client.parser.properties import Schemas, _create_schemas @@ -1024,7 +1058,6 @@ def test_records_bad_uris_and_keeps_going(self, mocker): schemas=Schemas(errors=[PropertyError(detail="some details", data=components["first"])]), ) assert result == update_schemas_with_data.return_value - assert result.schemas_created def test_retries_failing_properties_while_making_progress(self, mocker): from openapi_python_client.parser.properties import Schemas, _create_schemas @@ -1048,7 +1081,6 @@ def test_retries_failing_properties_while_making_progress(self, mocker): ) assert update_schemas_with_data.call_count == 3 assert result.errors == [PropertyError()] - assert result.schemas_created class TestProcessModels: @@ -1057,7 +1089,7 @@ def test_retries_failing_models_while_making_progress(self, mocker, model_proper first_model = model_property_factory() schemas = Schemas( - classes_by_reference={ + classes_by_name={ "first": first_model, "second": model_property_factory(), "non-model": property_factory(), @@ -1074,7 +1106,7 @@ def test_retries_failing_models_while_making_progress(self, mocker, model_proper process_model.assert_has_calls( [ call(first_model, schemas=schemas, config=config), - call(schemas.classes_by_reference["second"], schemas=schemas, config=config), + call(schemas.classes_by_name["second"], schemas=schemas, config=config), call(first_model, schemas=result, config=config), ] ) @@ -1088,7 +1120,7 @@ def test_detect_recursive_allof_reference_no_retry(self, mocker, model_property_ class_name = "class_name" recursive_model = model_property_factory(class_info=Class(name=class_name, module_name="module_name")) schemas = Schemas( - classes_by_reference={ + classes_by_name={ "recursive": recursive_model, "second": model_property_factory(), } @@ -1103,7 +1135,7 @@ def test_detect_recursive_allof_reference_no_retry(self, mocker, model_property_ process_model.assert_has_calls( [ call(recursive_model, schemas=schemas, config=config), - call(schemas.classes_by_reference["second"], 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)]) diff --git a/tests/test_parser/test_properties/test_model_property.py b/tests/test_parser/test_properties/test_model_property.py index 12a2eef82..24db65976 100644 --- a/tests/test_parser/test_properties/test_model_property.py +++ b/tests/test_parser/test_properties/test_model_property.py @@ -79,11 +79,12 @@ def test_additional_schemas(self, additional_properties_schema, expected_additio model, _ = build_model_property( data=data, name="prop", - schemas=Schemas(schemas_created=True), + schemas=Schemas(), required=True, parent_name="parent", config=Config(), roots={"root"}, + process_properties=True, ) assert model.additional_properties == expected_additional_properties @@ -105,14 +106,19 @@ def test_happy_path(self, model_property_factory, string_property_factory, date_ description="A class called MyModel", nullable=nullable, ) - schemas = Schemas( - classes_by_reference={"OtherModel": None}, classes_by_name={"OtherModel": None}, schemas_created=True - ) + 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(), roots=roots + data=data, + name=name, + schemas=schemas, + required=required, + parent_name="parent", + config=Config(), + roots=roots, + process_properties=True, ) assert new_schemas != schemas @@ -158,6 +164,7 @@ def test_model_name_conflict(self): parent_name=None, config=Config(), roots={"root"}, + process_properties=True, ) assert new_schemas == schemas @@ -174,11 +181,12 @@ def test_model_bad_properties(self): result = build_model_property( data=data, name="prop", - schemas=Schemas(schemas_created=True), + schemas=Schemas(), required=True, parent_name="parent", config=Config(), roots={"root"}, + process_properties=True, )[0] assert isinstance(result, PropertyError) @@ -195,15 +203,16 @@ def test_model_bad_additional_properties(self): result = build_model_property( data=data, name="prop", - schemas=Schemas(schemas_created=True), + schemas=Schemas(), required=True, parent_name="parent", config=Config(), roots={"root"}, + process_properties=True, )[0] assert isinstance(result, PropertyError) - def test_schemas_not_created(self, model_property_factory): + def test_process_properties_false(self, model_property_factory): from openapi_python_client.parser.properties import Class, Schemas, build_model_property name = "prop" @@ -225,7 +234,14 @@ def test_schemas_not_created(self, model_property_factory): 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 + data=data, + name=name, + schemas=schemas, + required=required, + parent_name="parent", + config=Config(), + roots=roots, + process_properties=False, ) assert new_schemas != schemas @@ -293,9 +309,7 @@ def test_process_properties_model_property_roots(self, model_property_factory): roots = {"root"} data = oai.Schema(properties={"test_model_property": oai.Schema.construct(type="object")}) - result = _process_properties( - data=data, class_name="", schemas=Schemas(schemas_created=True), config=Config(), roots=roots - ) + 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) From f3eaf74481af2c7b37d19f4510ca386dc148ad81 Mon Sep 17 00:00:00 2001 From: Matvey Ovtsin Date: Thu, 8 Sep 2022 23:29:31 +0300 Subject: [PATCH 03/27] try to add lazy imports to avoid circular import issue --- .../parser/properties/model_property.py | 61 ++++++++++++++++++- .../parser/properties/property.py | 15 +++-- .../templates/model.py.jinja | 4 ++ 3 files changed, 75 insertions(+), 5 deletions(-) diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index b4f51297b..86d409d67 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 @@ -23,6 +25,7 @@ class ModelProperty(Property): 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]" @@ -33,6 +36,8 @@ class ModelProperty(Property): def __attrs_post_init__(self) -> None: if self.relative_imports: self.set_relative_imports(self.relative_imports) + if self.lazy_imports: + self.set_lazy_imports(self.lazy_imports) @property def self_import(self) -> str: @@ -53,13 +58,21 @@ def get_imports(self, *, prefix: str) -> Set[str]: imports = super().get_imports(prefix=prefix) imports.update( { - f"from {prefix}{self.self_import}", "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 @@ -68,6 +81,43 @@ def set_relative_imports(self, relative_imports: Set[str]) -> None: """ 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, *, model_parent: Optional[ModelProperty] = None + ) -> 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 model_parent: + type_string = type_string.replace(model_parent.class_info.name, f"'{type_string}'") + + type_string = type_string.replace(self.class_info.name, 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()) @@ -127,6 +177,7 @@ class _PropertyData(NamedTuple): optional_props: List[Property] required_props: List[Property] relative_imports: Set[str] + lazy_imports: Set[str] schemas: Schemas @@ -143,6 +194,7 @@ def _process_properties( 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]: @@ -207,12 +259,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, ) @@ -303,6 +358,7 @@ def process_model(model_prop: ModelProperty, *, schemas: Schemas, config: Config 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 @@ -341,6 +397,7 @@ def build_model_property( 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( @@ -352,6 +409,7 @@ def build_model_property( 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 @@ -364,6 +422,7 @@ def build_model_property( 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, diff --git a/openapi_python_client/parser/properties/property.py b/openapi_python_client/parser/properties/property.py index ff0817cb3..43388f671 100644 --- a/openapi_python_client/parser/properties/property.py +++ b/openapi_python_client/parser/properties/property.py @@ -89,10 +89,7 @@ def get_type_string( type_string = self.get_base_type_string() if model_parent: - if type_string == model_parent.class_info.name: - type_string = f"'{type_string}'" - if type_string == f"List[{model_parent.class_info.name}]": - type_string = f"List['{model_parent.class_info.name}']" + type_string = type_string.replace(model_parent.class_info.name, f"'{type_string}'") if no_optional or (self.required and not self.nullable): return type_string @@ -124,6 +121,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, *, model_parent: Optional[ModelProperty] = None) -> str: """How this should be declared in a dataclass diff --git a/openapi_python_client/templates/model.py.jinja b/openapi_python_client/templates/model.py.jinja index 1a5ce1879..d839ec400 100644 --- a/openapi_python_client/templates/model.py.jinja +++ b/openapi_python_client/templates/model.py.jinja @@ -122,6 +122,10 @@ 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 %} From d384dcb79147f49e508030740f501c1f2613bd32 Mon Sep 17 00:00:00 2001 From: Matvey Ovtsin Date: Thu, 8 Sep 2022 23:30:07 +0300 Subject: [PATCH 04/27] Fix tests related to the addition of lazy imports --- tests/conftest.py | 1 + .../test_properties/test_model_property.py | 33 +++++++++++++------ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index ea0e71367..a1bd6e27f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -38,6 +38,7 @@ def _factory(**kwargs): "required_properties": None, "optional_properties": None, "relative_imports": None, + "lazy_imports": None, "additional_properties": None, "python_name": "", "description": "", diff --git a/tests/test_parser/test_properties/test_model_property.py b/tests/test_parser/test_properties/test_model_property.py index 24db65976..fd53f9c03 100644 --- a/tests/test_parser/test_properties/test_model_property.py +++ b/tests/test_parser/test_properties/test_model_property.py @@ -13,14 +13,14 @@ @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, 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]"), ], ) @@ -41,12 +41,19 @@ def test_get_imports(model_property_factory): "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", } +def test_get_lazy_imports(model_property_factory): + prop = model_property_factory(required=False, nullable=True) + + assert prop.get_lazy_imports(prefix="..") == { + "from ..models.my_module import MyClass", + } + + class TestBuildModelProperty: @pytest.mark.parametrize( "additional_properties_schema, expected_additional_properties", @@ -147,6 +154,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, ) @@ -662,7 +670,11 @@ def test_process_model(self, mocker, model_property_factory): model_prop = model_property_factory() schemas = Schemas() property_data = _PropertyData( - required_props=["required"], optional_props=["optional"], relative_imports={"relative"}, schemas=schemas + 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") @@ -674,6 +686,7 @@ def test_process_model(self, mocker, model_property_factory): 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 From e11f9cc7a1000bcc8c2f5a18998253503467b54e Mon Sep 17 00:00:00 2001 From: Matvey Ovtsin Date: Fri, 9 Sep 2022 12:31:43 +0300 Subject: [PATCH 05/27] Fix class name --- openapi_python_client/parser/properties/__init__.py | 2 +- openapi_python_client/parser/properties/schemas.py | 4 ++-- tests/test_parser/test_properties/test_init.py | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index 75857f0c6..f36c27d0f 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -27,8 +27,8 @@ from .schemas import ( Class, Parameters, - Schemas, ReferencePath, + Schemas, parse_reference_path, update_parameters_with_data, update_schemas_with_data, diff --git a/openapi_python_client/parser/properties/schemas.py b/openapi_python_client/parser/properties/schemas.py index 8ec3c7773..fd1af5c08 100644 --- a/openapi_python_client/parser/properties/schemas.py +++ b/openapi_python_client/parser/properties/schemas.py @@ -139,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) @@ -172,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/tests/test_parser/test_properties/test_init.py b/tests/test_parser/test_properties/test_init.py index d90114765..afd46caec 100644 --- a/tests/test_parser/test_properties/test_init.py +++ b/tests/test_parser/test_properties/test_init.py @@ -1232,6 +1232,8 @@ def test_process_model_errors(mocker, model_property_factory): ) 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 From 1bcf7bb67fade944ff0a8044531cc53f9ed0f291 Mon Sep 17 00:00:00 2001 From: Matvey Ovtsin Date: Sat, 10 Sep 2022 01:11:48 +0300 Subject: [PATCH 06/27] fix: quoting properties type correctly --- .../parser/properties/model_property.py | 12 +++++++++--- openapi_python_client/parser/properties/property.py | 5 ++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index 86d409d67..57d49adb6 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -105,9 +105,15 @@ def get_type_string( type_string = self.get_base_type_string() if model_parent: - type_string = type_string.replace(model_parent.class_info.name, f"'{type_string}'") - - type_string = type_string.replace(self.class_info.name, f"'{type_string}'") + if type_string == model_parent.class_info.name: + type_string = f"'{type_string}'" + if type_string == f"List[{model_parent.class_info.name}]": + type_string = f"List['{model_parent.class_info.name}']" + + if type_string == self.class_info.name: + type_string = f"'{type_string}'" + if type_string == f"List[{self.class_info.name}]": + type_string = f"List['{self.class_info.name}']" if no_optional or (self.required and not self.nullable): return type_string diff --git a/openapi_python_client/parser/properties/property.py b/openapi_python_client/parser/properties/property.py index 43388f671..4945ddda3 100644 --- a/openapi_python_client/parser/properties/property.py +++ b/openapi_python_client/parser/properties/property.py @@ -89,7 +89,10 @@ def get_type_string( type_string = self.get_base_type_string() if model_parent: - type_string = type_string.replace(model_parent.class_info.name, f"'{type_string}'") + if type_string == model_parent.class_info.name: + type_string = f"'{type_string}'" + if type_string == f"List[{model_parent.class_info.name}]": + type_string = f"List['{model_parent.class_info.name}']" if no_optional or (self.required and not self.nullable): return type_string From b407d52631ab3dce74e3da36df98660fb30393bf Mon Sep 17 00:00:00 2001 From: Matvey Ovtsin Date: Sat, 10 Sep 2022 01:14:42 +0300 Subject: [PATCH 07/27] Add lazy import to the Union and List properties --- openapi_python_client/parser/properties/__init__.py | 11 +++++++++++ .../property_templates/list_property.py.jinja | 7 +++++++ 2 files changed, 18 insertions(+) diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index f36c27d0f..6326a09ba 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -211,6 +211,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): @@ -285,6 +290,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 diff --git a/openapi_python_client/templates/property_templates/list_property.py.jinja b/openapi_python_client/templates/property_templates/list_property.py.jinja index 9686f6930..0e49c7128 100644 --- a/openapi_python_client/templates/property_templates/list_property.py.jinja +++ b/openapi_python_client/templates/property_templates/list_property.py.jinja @@ -3,6 +3,13 @@ {% import "property_templates/" + inner_property.template as inner_template %} {% if inner_template.construct %} {% set inner_source = inner_property.python_name + "_data" %} + +{% if inner_property.get_lazy_imports(prefix="..") %} +{% for lazy_import in inner_property.get_lazy_imports(prefix="..") %} +{{ lazy_import }} +{% endfor %} +{% endif %} + {{ property.python_name }} = {{ initial_value }} _{{ property.python_name }} = {{ source }} {% if property.required and not property.nullable %} From ca22a3a90bf67bd166fb63c3375c26252d7faac4 Mon Sep 17 00:00:00 2001 From: Matvey Ovtsin Date: Mon, 12 Sep 2022 15:00:15 +0300 Subject: [PATCH 08/27] Fix: add missing lazy imports to the additional properties. Make model quoting optional --- .../parser/properties/__init__.py | 13 +++++---- .../parser/properties/model_property.py | 27 ++++++++++++------- .../parser/properties/property.py | 11 +++++--- .../templates/model.py.jinja | 14 ++++++++-- 4 files changed, 45 insertions(+), 20 deletions(-) diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index 6326a09ba..2ada40e4b 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -224,8 +224,8 @@ class UnionProperty(Property): inner_properties: List[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} + def _get_inner_type_strings(self, json: bool = False, quoted: bool = True) -> Set[str]: + return {p.get_type_string(no_optional=True, json=json, quoted=quoted) for p in self.inner_properties} @staticmethod def _get_type_string_from_inner_type_strings(inner_types: Set[str]) -> str: @@ -239,7 +239,9 @@ def get_base_type_string(self) -> str: def get_base_json_type_string(self) -> 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]: + def get_type_strings_in_union( + self, no_optional: bool = False, json: bool = False, *, quoted: bool = True + ) -> Set[str]: """ Get the set of all the types that should appear within the `Union` representing this property. @@ -252,7 +254,7 @@ def get_type_strings_in_union(self, no_optional: bool = False, json: bool = Fals Returns: A set of strings containing the types that should appear within `Union`. """ - type_strings = self._get_inner_type_strings(json=json) + type_strings = self._get_inner_type_strings(json=json, quoted=quoted) if no_optional: return type_strings if self.nullable: @@ -267,13 +269,14 @@ def get_type_string( json: bool = False, *, model_parent: Optional[ModelProperty] = None, # pylint:disable=unused-argument + quoted: bool = True, ) -> 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 nested union types. """ - type_strings_in_union = self.get_type_strings_in_union(no_optional=no_optional, json=json) + type_strings_in_union = self.get_type_strings_in_union(no_optional=no_optional, json=json, quoted=quoted) return self._get_type_string_from_inner_type_strings(type_strings_in_union) 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 57d49adb6..8faeb2869 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -90,7 +90,12 @@ def set_lazy_imports(self, lazy_imports: Set[str]) -> None: 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, *, model_parent: Optional[ModelProperty] = None + self, + no_optional: bool = False, + json: bool = False, + *, + model_parent: Optional[ModelProperty] = None, + quoted: bool = True, ) -> str: """ Get a string representation of type that should be used when declaring this property @@ -104,16 +109,17 @@ def get_type_string( else: type_string = self.get_base_type_string() - if model_parent: - if type_string == model_parent.class_info.name: - type_string = f"'{type_string}'" - if type_string == f"List[{model_parent.class_info.name}]": - type_string = f"List['{model_parent.class_info.name}']" + if quoted: + if model_parent: + if type_string == model_parent.class_info.name: + type_string = f"'{type_string}'" + if type_string == f"List[{model_parent.class_info.name}]": + type_string = f"List['{model_parent.class_info.name}']" - if type_string == self.class_info.name: - type_string = f"'{type_string}'" - if type_string == f"List[{self.class_info.name}]": - type_string = f"List['{self.class_info.name}']" + if type_string == self.class_info.name: + type_string = f"'{type_string}'" + if type_string == f"List[{self.class_info.name}]": + type_string = f"List['{self.class_info.name}']" if no_optional or (self.required and not self.nullable): return type_string @@ -334,6 +340,7 @@ def _process_property_data( ) 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 diff --git a/openapi_python_client/parser/properties/property.py b/openapi_python_client/parser/properties/property.py index 4945ddda3..c552c6efc 100644 --- a/openapi_python_client/parser/properties/property.py +++ b/openapi_python_client/parser/properties/property.py @@ -74,7 +74,12 @@ def get_base_json_type_string(self) -> str: return self._json_type_string def get_type_string( - self, no_optional: bool = False, json: bool = False, *, model_parent: Optional[ModelProperty] = None + self, + no_optional: bool = False, + json: bool = False, + *, + model_parent: Optional[ModelProperty] = None, + quoted: bool = True, ) -> str: """ Get a string representation of type that should be used when declaring this property @@ -88,7 +93,7 @@ def get_type_string( else: type_string = self.get_base_type_string() - if model_parent: + if quoted and model_parent: if type_string == model_parent.class_info.name: type_string = f"'{type_string}'" if type_string == f"List[{model_parent.class_info.name}]": @@ -105,7 +110,7 @@ def get_type_string( 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]: diff --git a/openapi_python_client/templates/model.py.jinja b/openapi_python_client/templates/model.py.jinja index 065c2424b..c2f785d15 100644 --- a/openapi_python_client/templates/model.py.jinja +++ b/openapi_python_client/templates/model.py.jinja @@ -113,6 +113,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,9 +125,9 @@ return field_dict @classmethod def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: - {% for lazy_import in model.lazy_imports %} + {% for lazy_import in model.lazy_imports %} {{ lazy_import }} - {% endfor %} + {% endfor %} d = src_dict.copy() {% for property in model.required_properties + model.optional_properties %} @@ -150,6 +153,13 @@ 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 %} + +# {{ model.additional_properties }} +{% 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 %} From 8e6a9dc624239e5da1597402ec90f4021dd737b1 Mon Sep 17 00:00:00 2001 From: Matvey Ovtsin Date: Tue, 13 Sep 2022 19:12:50 +0300 Subject: [PATCH 09/27] Fix: remove extra lazy imports in list property --- openapi_python_client/templates/model.py.jinja | 1 - .../templates/property_templates/list_property.py.jinja | 7 ------- 2 files changed, 8 deletions(-) diff --git a/openapi_python_client/templates/model.py.jinja b/openapi_python_client/templates/model.py.jinja index c2f785d15..4f46f2f86 100644 --- a/openapi_python_client/templates/model.py.jinja +++ b/openapi_python_client/templates/model.py.jinja @@ -154,7 +154,6 @@ return field_dict {% if model.additional_properties.template %}{# Can be a bool instead of an object #} {% import "property_templates/" + model.additional_properties.template as prop_template %} -# {{ model.additional_properties }} {% if model.additional_properties.lazy_imports %} {% for lazy_import in model.additional_properties.lazy_imports %} {{ lazy_import }} diff --git a/openapi_python_client/templates/property_templates/list_property.py.jinja b/openapi_python_client/templates/property_templates/list_property.py.jinja index 0e49c7128..9686f6930 100644 --- a/openapi_python_client/templates/property_templates/list_property.py.jinja +++ b/openapi_python_client/templates/property_templates/list_property.py.jinja @@ -3,13 +3,6 @@ {% import "property_templates/" + inner_property.template as inner_template %} {% if inner_template.construct %} {% set inner_source = inner_property.python_name + "_data" %} - -{% if inner_property.get_lazy_imports(prefix="..") %} -{% for lazy_import in inner_property.get_lazy_imports(prefix="..") %} -{{ lazy_import }} -{% endfor %} -{% endif %} - {{ property.python_name }} = {{ initial_value }} _{{ property.python_name }} = {{ source }} {% if property.required and not property.nullable %} From d8c60efdbc21804007ade336f26f4483556587cf Mon Sep 17 00:00:00 2001 From: Matvey Ovtsin Date: Tue, 13 Sep 2022 19:14:02 +0300 Subject: [PATCH 10/27] Add lazy imports to the relatives in `Endpoint`s --- openapi_python_client/parser/openapi.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/openapi_python_client/parser/openapi.py b/openapi_python_client/parser/openapi.py index 8ebf02a39..0512b8846 100644 --- a/openapi_python_client/parser/openapi.py +++ b/openapi_python_client/parser/openapi.py @@ -98,6 +98,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: @@ -238,15 +241,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 @@ -285,7 +292,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 @@ -422,7 +432,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 From 252450936d850bde21ec90c01aa1937c1462ce9d Mon Sep 17 00:00:00 2001 From: Matvey Ovtsin Date: Wed, 14 Sep 2022 00:22:43 +0300 Subject: [PATCH 11/27] Use `quoted` arg and base type checking. Enum properties are not quoting now. --- openapi_python_client/parser/openapi.py | 4 +-- .../parser/properties/__init__.py | 28 +++++++++--------- .../parser/properties/enum_property.py | 5 ++-- .../parser/properties/model_property.py | 6 ++-- .../parser/properties/property.py | 29 +++++++++++++------ .../templates/model.py.jinja | 3 +- 6 files changed, 43 insertions(+), 32 deletions(-) diff --git a/openapi_python_client/parser/openapi.py b/openapi_python_client/parser/openapi.py index 0512b8846..24654efe4 100644 --- a/openapi_python_client/parser/openapi.py +++ b/openapi_python_client/parser/openapi.py @@ -508,11 +508,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 2ada40e4b..a7f8922e9 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -188,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=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=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""" @@ -224,8 +225,8 @@ class UnionProperty(Property): inner_properties: List[Property] template: ClassVar[str] = "union_property.py.jinja" - def _get_inner_type_strings(self, json: bool = False, quoted: bool = True) -> Set[str]: - return {p.get_type_string(no_optional=True, json=json, quoted=quoted) for p in self.inner_properties} + def _get_inner_type_strings(self, json: bool = False) -> Set[str]: + return {p.get_type_string(no_optional=True, json=json, quoted=p.is_base_type) for p in self.inner_properties} @staticmethod def _get_type_string_from_inner_type_strings(inner_types: Set[str]) -> str: @@ -233,15 +234,14 @@ 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 = True) -> 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, *, quoted: bool = True - ) -> Set[str]: + def get_type_strings_in_union(self, no_optional: bool = False, json: bool = False) -> Set[str]: """ Get the set of all the types that should appear within the `Union` representing this property. @@ -254,7 +254,7 @@ def get_type_strings_in_union( Returns: A set of strings containing the types that should appear within `Union`. """ - type_strings = self._get_inner_type_strings(json=json, quoted=quoted) + type_strings = self._get_inner_type_strings(json=json) if no_optional: return type_strings if self.nullable: @@ -269,14 +269,14 @@ def get_type_string( json: bool = False, *, model_parent: Optional[ModelProperty] = None, # pylint:disable=unused-argument - quoted: bool = True, + 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 nested union types. """ - type_strings_in_union = self.get_type_strings_in_union(no_optional=no_optional, json=json, quoted=quoted) + type_strings_in_union = self.get_type_strings_in_union(no_optional=no_optional, json=json) return self._get_type_string_from_inner_type_strings(type_strings_in_union) def get_imports(self, *, prefix: str) -> Set[str]: 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 8faeb2869..7569b734a 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -44,8 +44,8 @@ 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) -> str: - return 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]: """ @@ -95,7 +95,7 @@ def get_type_string( json: bool = False, *, model_parent: Optional[ModelProperty] = None, - quoted: bool = True, + quoted: bool = False, ) -> str: """ Get a string representation of type that should be used when declaring this property diff --git a/openapi_python_client/parser/properties/property.py b/openapi_python_client/parser/properties/property.py index c552c6efc..30702a21b 100644 --- a/openapi_python_client/parser/properties/property.py +++ b/openapi_python_client/parser/properties/property.py @@ -65,13 +65,13 @@ 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: + def get_base_type_string(self, *, quoted: bool = False) -> str: """Get the string describing the Python type of this property.""" - return self._type_string + return f'"{self._type_string}"' if not self.is_base_type and quoted else self._type_string - def get_base_json_type_string(self) -> str: + def get_base_json_type_string(self, *, quoted: bool = False) -> str: """Get the string describing the JSON type of this property.""" - return self._json_type_string + return f'"{self._json_type_string}"' if not self.is_base_type and quoted else self._json_type_string def get_type_string( self, @@ -79,7 +79,7 @@ def get_type_string( json: bool = False, *, model_parent: Optional[ModelProperty] = None, - quoted: bool = True, + quoted: bool = False, ) -> str: """ Get a string representation of type that should be used when declaring this property @@ -89,9 +89,9 @@ def get_type_string( 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 quoted and model_parent: if type_string == model_parent.class_info.name: @@ -154,8 +154,8 @@ def to_string(self, *, model_parent: Optional[ModelProperty] = None) -> str: default = None if default is not None: - return f"{self.python_name}: {self.get_type_string(model_parent=model_parent)} = {default}" - return f"{self.python_name}: {self.get_type_string(model_parent=model_parent)}" + return f"{self.python_name}: {self.get_type_string(model_parent=model_parent, quoted=True)} = {default}" + return f"{self.python_name}: {self.get_type_string(model_parent=model_parent, quoted=True)}" def to_docstring(self) -> str: """Returns property docstring""" @@ -165,3 +165,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.__class__.__name__, + ListProperty.__class__.__name__, + UnionProperty.__class__.__name__, + } diff --git a/openapi_python_client/templates/model.py.jinja b/openapi_python_client/templates/model.py.jinja index 4f46f2f86..6407bb551 100644 --- a/openapi_python_client/templates/model.py.jinja +++ b/openapi_python_client/templates/model.py.jinja @@ -18,7 +18,7 @@ from ..types import UNSET, Unset {% if model.additional_properties %} -{% set additional_property_type = 'Any' if model.additional_properties == True else model.additional_properties.get_type_string(model_parent=model) %} +{% set additional_property_type = 'Any' if model.additional_properties == True else model.additional_properties.get_type_string(model_parent=model, quoted=model.additional_properties.is_base_type) %} {% endif %} {% set class_name = model.class_info.name %} @@ -128,7 +128,6 @@ return field_dict {% 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 %} From 93ee14dbdc99f99f0552d7894e4c0ab8dfd6f4cc Mon Sep 17 00:00:00 2001 From: Matvey Ovtsin Date: Wed, 14 Sep 2022 00:23:05 +0300 Subject: [PATCH 12/27] Revert changed test --- .../test_properties/test_model_property.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_parser/test_properties/test_model_property.py b/tests/test_parser/test_properties/test_model_property.py index fd53f9c03..f4e12b5e0 100644 --- a/tests/test_parser/test_properties/test_model_property.py +++ b/tests/test_parser/test_properties/test_model_property.py @@ -13,14 +13,14 @@ @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, 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]"), ], ) From cadad67e3a5f63ef4b26e10c346065410033471f Mon Sep 17 00:00:00 2001 From: Matvey Ovtsin Date: Wed, 14 Sep 2022 00:51:49 +0300 Subject: [PATCH 13/27] Update e2e: models --- .../api/tests/defaults_tests_defaults_post.py | 20 ++++----- .../api/tests/get_user_list.py | 20 ++++----- .../my_test_api_client/models/a_model.py | 44 ++++++++++--------- ...h_a_circular_ref_in_items_object_a_item.py | 9 ++-- ...ems_object_additional_properties_a_item.py | 17 ++++--- ...ems_object_additional_properties_b_item.py | 17 ++++--- ...h_a_circular_ref_in_items_object_b_item.py | 9 ++-- ...th_a_recursive_ref_in_items_object_item.py | 2 +- .../body_upload_file_tests_upload_post.py | 33 +++++++------- .../models/http_validation_error.py | 7 +-- ...odel_with_additional_properties_inlined.py | 13 +++--- .../models/model_with_any_json_properties.py | 21 +++++---- .../models/model_with_circular_ref_a.py | 5 ++- .../models/model_with_circular_ref_b.py | 5 ++- ...circular_ref_in_additional_properties_a.py | 13 +++--- ...circular_ref_in_additional_properties_b.py | 13 +++--- ...el_with_primitive_additional_properties.py | 9 ++-- .../models/model_with_property_ref.py | 5 ++- .../model_with_union_property_inlined.py | 13 +++--- ...ions_simple_before_complex_response_200.py | 20 +++++---- 20 files changed, 167 insertions(+), 128 deletions(-) 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 6c30b939d..e3521a0a4 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 @@ -23,8 +23,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) @@ -129,8 +129,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 @@ -186,8 +186,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 @@ -236,8 +236,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 @@ -291,8 +291,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 e4ed22231..69b1db04b 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 @@ -68,7 +68,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 == 200: response_200 = [] _response_200 = response.json() @@ -89,7 +89,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=response.status_code, content=response.content, @@ -105,7 +105,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 @@ -117,7 +117,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( @@ -143,7 +143,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 @@ -155,7 +155,7 @@ def sync( some_date (Union[datetime.date, datetime.datetime]): Returns: - Response[Union[HTTPValidationError, List[AModel]]] + Response[Union[HTTPValidationError, List['AModel']]] """ return sync_detailed( @@ -174,7 +174,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 @@ -186,7 +186,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( @@ -210,7 +210,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 @@ -222,7 +222,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/a_model.py b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py index 0cf302e56..dd6d47d4f 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 @@ -7,8 +7,6 @@ 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 T = TypeVar("T", bound="AModel") @@ -24,7 +22,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 +33,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 +45,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 +59,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 +219,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 +248,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 +265,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 +314,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 +333,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 +364,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 +399,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 index ee802e42d..9bcff7d1b 100644 --- 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 @@ -2,7 +2,6 @@ import attr -from ..models.an_array_with_a_circular_ref_in_items_object_b_item import AnArrayWithACircularRefInItemsObjectBItem from ..types import UNSET, Unset T = TypeVar("T", bound="AnArrayWithACircularRefInItemsObjectAItem") @@ -12,10 +11,10 @@ class AnArrayWithACircularRefInItemsObjectAItem: """ Attributes: - circular (Union[Unset, List[AnArrayWithACircularRefInItemsObjectBItem]]): + circular (Union[Unset, List['AnArrayWithACircularRefInItemsObjectBItem']]): """ - circular: Union[Unset, List[AnArrayWithACircularRefInItemsObjectBItem]] = UNSET + circular: Union[Unset, List["AnArrayWithACircularRefInItemsObjectBItem"]] = UNSET additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) def to_dict(self) -> Dict[str, Any]: @@ -39,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.an_array_with_a_circular_ref_in_items_object_b_item import ( + AnArrayWithACircularRefInItemsObjectBItem, + ) + d = src_dict.copy() circular = [] _circular = d.pop("circular", UNSET) 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 index 125e2a8be..4db4f6c19 100644 --- 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 @@ -2,10 +2,6 @@ import attr -from ..models.an_array_with_a_circular_ref_in_items_object_additional_properties_b_item import ( - AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem, -) - T = TypeVar("T", bound="AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem") @@ -13,11 +9,12 @@ class AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem: """ """ - additional_properties: Dict[str, List[AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem]] = attr.ib( + 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(): @@ -39,6 +36,10 @@ def to_dict(self) -> Dict[str, Any]: @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() @@ -70,10 +71,12 @@ 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) -> List[AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem]: + def __getitem__(self, key: str) -> List["AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem"]: return self.additional_properties[key] - def __setitem__(self, key: str, value: List[AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem]) -> None: + def __setitem__( + self, key: str, value: List["AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem"] + ) -> 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/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 index b984f2d12..c535a3051 100644 --- 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 @@ -2,10 +2,6 @@ import attr -from ..models.an_array_with_a_circular_ref_in_items_object_additional_properties_a_item import ( - AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem, -) - T = TypeVar("T", bound="AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem") @@ -13,11 +9,12 @@ class AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem: """ """ - additional_properties: Dict[str, List[AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem]] = attr.ib( + 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(): @@ -39,6 +36,10 @@ def to_dict(self) -> Dict[str, Any]: @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() @@ -70,10 +71,12 @@ 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) -> List[AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem]: + def __getitem__(self, key: str) -> List["AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem"]: return self.additional_properties[key] - def __setitem__(self, key: str, value: List[AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem]) -> None: + def __setitem__( + self, key: str, value: List["AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem"] + ) -> 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/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 index 6224bbc1e..f6b5242fa 100644 --- 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 @@ -2,7 +2,6 @@ import attr -from ..models.an_array_with_a_circular_ref_in_items_object_a_item import AnArrayWithACircularRefInItemsObjectAItem from ..types import UNSET, Unset T = TypeVar("T", bound="AnArrayWithACircularRefInItemsObjectBItem") @@ -12,10 +11,10 @@ class AnArrayWithACircularRefInItemsObjectBItem: """ Attributes: - circular (Union[Unset, List[AnArrayWithACircularRefInItemsObjectAItem]]): + circular (Union[Unset, List['AnArrayWithACircularRefInItemsObjectAItem']]): """ - circular: Union[Unset, List[AnArrayWithACircularRefInItemsObjectAItem]] = UNSET + circular: Union[Unset, List["AnArrayWithACircularRefInItemsObjectAItem"]] = UNSET additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) def to_dict(self) -> Dict[str, Any]: @@ -39,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.an_array_with_a_circular_ref_in_items_object_a_item import ( + AnArrayWithACircularRefInItemsObjectAItem, + ) + d = src_dict.copy() circular = [] _circular = d.pop("circular", UNSET) 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 index 7f6641985..c14ee07c2 100644 --- 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 @@ -11,7 +11,7 @@ class AnArrayWithARecursiveRefInItemsObjectItem: """ Attributes: - recursive (Union[Unset, List[AnArrayWithARecursiveRefInItemsObjectItem]]): + recursive (Union[Unset, List['AnArrayWithARecursiveRefInItemsObjectItem']]): """ recursive: Union[Unset, List["AnArrayWithARecursiveRefInItemsObjectItem"]] = UNSET 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..fdeb42b4a 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 @@ -6,16 +6,6 @@ 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 @@ -40,17 +30,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 +185,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 +276,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..d4ab9a95e 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 @@ -2,7 +2,6 @@ import attr -from ..models.validation_error import ValidationError from ..types import UNSET, Unset T = TypeVar("T", bound="HTTPValidationError") @@ -12,10 +11,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 +34,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..d242e64c2 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 @@ -2,9 +2,6 @@ import attr -from ..models.model_with_additional_properties_inlined_additional_property import ( - ModelWithAdditionalPropertiesInlinedAdditionalProperty, -) from ..types import UNSET, Unset T = TypeVar("T", bound="ModelWithAdditionalPropertiesInlined") @@ -18,7 +15,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 +34,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 +58,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..4a4365d9e 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 @@ -2,10 +2,6 @@ import attr -from ..models.model_with_any_json_properties_additional_property_type_0 import ( - ModelWithAnyJsonPropertiesAdditionalPropertyType0, -) - T = TypeVar("T", bound="ModelWithAnyJsonProperties") @@ -14,10 +10,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 +36,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 +48,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 +66,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 +82,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 index f9adb4bb1..6da3ed502 100644 --- 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 @@ -2,7 +2,6 @@ import attr -from ..models.model_with_circular_ref_b import ModelWithCircularRefB from ..types import UNSET, Unset T = TypeVar("T", bound="ModelWithCircularRefA") @@ -15,7 +14,7 @@ class ModelWithCircularRefA: circular (Union[Unset, ModelWithCircularRefB]): """ - circular: Union[Unset, ModelWithCircularRefB] = UNSET + circular: Union[Unset, "ModelWithCircularRefB"] = UNSET additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) def to_dict(self) -> Dict[str, Any]: @@ -33,6 +32,8 @@ def to_dict(self) -> Dict[str, Any]: @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] 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 index 25fa39b5f..966a91b02 100644 --- 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 @@ -2,7 +2,6 @@ import attr -from ..models.model_with_circular_ref_a import ModelWithCircularRefA from ..types import UNSET, Unset T = TypeVar("T", bound="ModelWithCircularRefB") @@ -15,7 +14,7 @@ class ModelWithCircularRefB: circular (Union[Unset, ModelWithCircularRefA]): """ - circular: Union[Unset, ModelWithCircularRefA] = UNSET + circular: Union[Unset, "ModelWithCircularRefA"] = UNSET additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) def to_dict(self) -> Dict[str, Any]: @@ -33,6 +32,8 @@ def to_dict(self) -> Dict[str, Any]: @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] 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 index 3349d1429..de56af2be 100644 --- 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 @@ -2,8 +2,6 @@ import attr -from ..models.model_with_circular_ref_in_additional_properties_b import ModelWithCircularRefInAdditionalPropertiesB - T = TypeVar("T", bound="ModelWithCircularRefInAdditionalPropertiesA") @@ -11,9 +9,10 @@ class ModelWithCircularRefInAdditionalPropertiesA: """ """ - additional_properties: Dict[str, ModelWithCircularRefInAdditionalPropertiesB] = attr.ib(init=False, factory=dict) + 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(): @@ -25,6 +24,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_circular_ref_in_additional_properties_b import ( + ModelWithCircularRefInAdditionalPropertiesB, + ) + d = src_dict.copy() model_with_circular_ref_in_additional_properties_a = cls() @@ -41,10 +44,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) -> ModelWithCircularRefInAdditionalPropertiesB: + def __getitem__(self, key: str) -> "ModelWithCircularRefInAdditionalPropertiesB": return self.additional_properties[key] - def __setitem__(self, key: str, value: ModelWithCircularRefInAdditionalPropertiesB) -> None: + def __setitem__(self, key: str, value: "ModelWithCircularRefInAdditionalPropertiesB") -> 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_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 index 99a9c5ed2..9ae0ee960 100644 --- 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 @@ -2,8 +2,6 @@ import attr -from ..models.model_with_circular_ref_in_additional_properties_a import ModelWithCircularRefInAdditionalPropertiesA - T = TypeVar("T", bound="ModelWithCircularRefInAdditionalPropertiesB") @@ -11,9 +9,10 @@ class ModelWithCircularRefInAdditionalPropertiesB: """ """ - additional_properties: Dict[str, ModelWithCircularRefInAdditionalPropertiesA] = attr.ib(init=False, factory=dict) + 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(): @@ -25,6 +24,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_circular_ref_in_additional_properties_a import ( + ModelWithCircularRefInAdditionalPropertiesA, + ) + d = src_dict.copy() model_with_circular_ref_in_additional_properties_b = cls() @@ -41,10 +44,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) -> ModelWithCircularRefInAdditionalPropertiesA: + def __getitem__(self, key: str) -> "ModelWithCircularRefInAdditionalPropertiesA": return self.additional_properties[key] - def __setitem__(self, key: str, value: ModelWithCircularRefInAdditionalPropertiesA) -> None: + def __setitem__(self, key: str, value: "ModelWithCircularRefInAdditionalPropertiesA") -> 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_primitive_additional_properties.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_primitive_additional_properties.py index 40d384759..4a97ab347 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 @@ -2,9 +2,6 @@ import attr -from ..models.model_with_primitive_additional_properties_a_date_holder import ( - ModelWithPrimitiveAdditionalPropertiesADateHolder, -) from ..types import UNSET, Unset T = TypeVar("T", bound="ModelWithPrimitiveAdditionalProperties") @@ -17,7 +14,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 +32,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..636e29d41 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 @@ -2,7 +2,6 @@ import attr -from ..models.model_name import ModelName from ..types import UNSET, Unset T = TypeVar("T", bound="ModelWithPropertyRef") @@ -15,7 +14,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 +32,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_union_property_inlined.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property_inlined.py index e89861520..2ec95801a 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 @@ -2,8 +2,6 @@ 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 T = TypeVar("T", bound="ModelWithUnionPropertyInlined") @@ -13,12 +11,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 +42,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..ea335d0d8 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 @@ -2,10 +2,6 @@ import attr -from ..models.post_responses_unions_simple_before_complex_response_200a_type_1 import ( - PostResponsesUnionsSimpleBeforeComplexResponse200AType1, -) - T = TypeVar("T", bound="PostResponsesUnionsSimpleBeforeComplexResponse200") @@ -13,13 +9,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 +40,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 +55,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")) From 11fb3dd8006376089d7e118706a61d9602524178 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sun, 18 Sep 2022 11:03:49 -0600 Subject: [PATCH 14/27] fix: type checking for lazy imports --- .../my_test_api_client/models/a_model.py | 7 ++++++- ..._with_a_circular_ref_in_items_object_a_item.py | 6 +++++- ...n_items_object_additional_properties_a_item.py | 8 +++++++- ...n_items_object_additional_properties_b_item.py | 8 +++++++- ..._with_a_circular_ref_in_items_object_b_item.py | 6 +++++- .../models/body_upload_file_tests_upload_post.py | 15 ++++++++++++++- .../models/http_validation_error.py | 6 +++++- .../model_with_additional_properties_inlined.py | 8 +++++++- .../models/model_with_any_json_properties.py | 8 +++++++- .../models/model_with_circular_ref_a.py | 6 +++++- .../models/model_with_circular_ref_b.py | 6 +++++- ...ith_circular_ref_in_additional_properties_a.py | 6 +++++- ...ith_circular_ref_in_additional_properties_b.py | 6 +++++- .../model_with_primitive_additional_properties.py | 8 +++++++- .../models/model_with_property_ref.py | 6 +++++- .../models/model_with_union_property_inlined.py | 7 ++++++- ...s_unions_simple_before_complex_response_200.py | 8 +++++++- .../integration_tests/models/public_error.py | 13 +++++++++---- openapi_python_client/templates/model.py.jinja | 9 ++++++++- 19 files changed, 125 insertions(+), 22 deletions(-) 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 dd6d47d4f..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 @@ -9,6 +9,11 @@ from ..models.different_enum import DifferentEnum from ..types import UNSET, Unset +if TYPE_CHECKING: + from ..models.free_form_model import FreeFormModel + from ..models.model_with_union_property import ModelWithUnionProperty + + T = TypeVar("T", bound="AModel") 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 index 9bcff7d1b..16fcbb88d 100644 --- 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 @@ -1,9 +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 ..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") 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 index 4db4f6c19..f03a87604 100644 --- 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 @@ -1,7 +1,13 @@ -from typing import Any, Dict, List, Type, TypeVar +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") 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 index c535a3051..cdff09b4b 100644 --- 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 @@ -1,7 +1,13 @@ -from typing import Any, Dict, List, Type, TypeVar +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") 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 index f6b5242fa..b15bb0f7b 100644 --- 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 @@ -1,9 +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 ..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") 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 fdeb42b4a..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,7 +1,7 @@ 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 @@ -9,6 +9,19 @@ 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") 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 d4ab9a95e..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,9 +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 ..types import UNSET, Unset +if TYPE_CHECKING: + from ..models.validation_error import ValidationError + + T = TypeVar("T", bound="HTTPValidationError") 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 d242e64c2..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,9 +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 ..types import UNSET, Unset +if TYPE_CHECKING: + from ..models.model_with_additional_properties_inlined_additional_property import ( + ModelWithAdditionalPropertiesInlinedAdditionalProperty, + ) + + T = TypeVar("T", bound="ModelWithAdditionalPropertiesInlined") 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 4a4365d9e..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,7 +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 +if TYPE_CHECKING: + from ..models.model_with_any_json_properties_additional_property_type_0 import ( + ModelWithAnyJsonPropertiesAdditionalPropertyType0, + ) + + T = TypeVar("T", bound="ModelWithAnyJsonProperties") 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 index 6da3ed502..11e31983d 100644 --- 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 @@ -1,9 +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 ..types import UNSET, Unset +if TYPE_CHECKING: + from ..models.model_with_circular_ref_b import ModelWithCircularRefB + + T = TypeVar("T", bound="ModelWithCircularRefA") 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 index 966a91b02..5fb34f4f4 100644 --- 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 @@ -1,9 +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 ..types import UNSET, Unset +if TYPE_CHECKING: + from ..models.model_with_circular_ref_a import ModelWithCircularRefA + + T = TypeVar("T", bound="ModelWithCircularRefB") 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 index de56af2be..0d9e0155c 100644 --- 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 @@ -1,7 +1,11 @@ -from typing import Any, Dict, List, Type, TypeVar +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") 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 index 9ae0ee960..0583b40f8 100644 --- 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 @@ -1,7 +1,11 @@ -from typing import Any, Dict, List, Type, TypeVar +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") 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 4a97ab347..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,9 +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 ..types import UNSET, Unset +if TYPE_CHECKING: + from ..models.model_with_primitive_additional_properties_a_date_holder import ( + ModelWithPrimitiveAdditionalPropertiesADateHolder, + ) + + T = TypeVar("T", bound="ModelWithPrimitiveAdditionalProperties") 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 636e29d41..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,9 +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 ..types import UNSET, Unset +if TYPE_CHECKING: + from ..models.model_name import ModelName + + T = TypeVar("T", bound="ModelWithPropertyRef") 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 2ec95801a..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,9 +1,14 @@ -from typing import Any, Dict, Type, TypeVar, Union +from typing import TYPE_CHECKING, Any, Dict, Type, TypeVar, Union import attr 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") 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 ea335d0d8..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,7 +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 +if TYPE_CHECKING: + from ..models.post_responses_unions_simple_before_complex_response_200a_type_1 import ( + PostResponsesUnionsSimpleBeforeComplexResponse200AType1, + ) + + T = TypeVar("T", bound="PostResponsesUnionsSimpleBeforeComplexResponse200") 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/templates/model.py.jinja b/openapi_python_client/templates/model.py.jinja index 6407bb551..17cd06460 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,6 +16,13 @@ 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(model_parent=model, quoted=model.additional_properties.is_base_type) %} From 1577f678b425d704d312617280173d529aad7c81 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Mon, 26 Sep 2022 17:37:28 -0600 Subject: [PATCH 15/27] test: Regen e2e --- .../my_test_api_client/models/__init__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 192c54687..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 @@ -76,6 +76,12 @@ "AModel", "AModelWithPropertiesReferenceThatAreNotObject", "AnAllOfEnum", + "AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem", + "AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem", + "AnArrayWithACircularRefInItemsObjectAItem", + "AnArrayWithACircularRefInItemsObjectBItem", + "AnArrayWithARecursiveRefInItemsObjectAdditionalPropertiesItem", + "AnArrayWithARecursiveRefInItemsObjectItem", "AnEnum", "AnEnumWithNull", "AnIntEnum", @@ -101,10 +107,16 @@ "ModelWithAdditionalPropertiesRefed", "ModelWithAnyJsonProperties", "ModelWithAnyJsonPropertiesAdditionalPropertyType0", + "ModelWithCircularRefA", + "ModelWithCircularRefB", + "ModelWithCircularRefInAdditionalPropertiesA", + "ModelWithCircularRefInAdditionalPropertiesB", "ModelWithDateTimeProperty", "ModelWithPrimitiveAdditionalProperties", "ModelWithPrimitiveAdditionalPropertiesADateHolder", "ModelWithPropertyRef", + "ModelWithRecursiveRef", + "ModelWithRecursiveRefInAdditionalProperties", "ModelWithUnionProperty", "ModelWithUnionPropertyInlined", "ModelWithUnionPropertyInlinedFruitType0", From 986f152d82cc8056de9564894bd049e457915f78 Mon Sep 17 00:00:00 2001 From: Matvey Ovtsin Date: Sun, 2 Oct 2022 00:38:33 +0300 Subject: [PATCH 16/27] Fix: `Property.is_base_type` and it usage --- openapi_python_client/parser/properties/__init__.py | 8 ++++---- openapi_python_client/parser/properties/property.py | 6 +++--- openapi_python_client/templates/model.py.jinja | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index a7f8922e9..988ff7c95 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -190,10 +190,10 @@ class ListProperty(Property, Generic[InnerProp]): # pylint: disable=unused-argument def get_base_type_string(self, *, quoted: bool = False) -> str: - return f"List[{self.inner_property.get_type_string(quoted=self.inner_property.is_base_type)}]" + return f"List[{self.inner_property.get_type_string(quoted=not self.inner_property.is_base_type)}]" def get_base_json_type_string(self, *, quoted: bool = False) -> str: - return f"List[{self.inner_property.get_type_string(json=True, quoted=self.inner_property.is_base_type)}]" + 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""" @@ -226,7 +226,7 @@ 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, quoted=p.is_base_type) 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: @@ -235,7 +235,7 @@ def _get_type_string_from_inner_type_strings(inner_types: Set[str]) -> str: return f"Union[{', '.join(sorted(inner_types))}]" # pylint: disable=unused-argument - def get_base_type_string(self, *, quoted: bool = True) -> str: + 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, *, quoted: bool = False) -> str: diff --git a/openapi_python_client/parser/properties/property.py b/openapi_python_client/parser/properties/property.py index 30702a21b..d84e84ca7 100644 --- a/openapi_python_client/parser/properties/property.py +++ b/openapi_python_client/parser/properties/property.py @@ -172,7 +172,7 @@ def is_base_type(self) -> bool: from . import ListProperty, ModelProperty, UnionProperty return self.__class__.__name__ not in { - ModelProperty.__class__.__name__, - ListProperty.__class__.__name__, - UnionProperty.__class__.__name__, + ModelProperty.__name__, + ListProperty.__name__, + UnionProperty.__name__, } diff --git a/openapi_python_client/templates/model.py.jinja b/openapi_python_client/templates/model.py.jinja index 17cd06460..05e0d5ff5 100644 --- a/openapi_python_client/templates/model.py.jinja +++ b/openapi_python_client/templates/model.py.jinja @@ -25,7 +25,7 @@ if TYPE_CHECKING: {% if model.additional_properties %} -{% set additional_property_type = 'Any' if model.additional_properties == True else model.additional_properties.get_type_string(model_parent=model, quoted=model.additional_properties.is_base_type) %} +{% set additional_property_type = 'Any' if model.additional_properties == True else model.additional_properties.get_type_string(model_parent=model, quoted=not model.additional_properties.is_base_type) %} {% endif %} {% set class_name = model.class_info.name %} From fbcfd65c4e69487811c724dc4ee1b476510672b3 Mon Sep 17 00:00:00 2001 From: Matvey Ovtsin Date: Sun, 2 Oct 2022 04:58:33 +0300 Subject: [PATCH 17/27] update docstring for `get_base_*type_string` --- openapi_python_client/parser/properties/__init__.py | 4 +++- openapi_python_client/parser/properties/property.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index 988ff7c95..03e1d1c48 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -226,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, quoted=not p.is_base_type) 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: diff --git a/openapi_python_client/parser/properties/property.py b/openapi_python_client/parser/properties/property.py index d84e84ca7..b5b4e48f4 100644 --- a/openapi_python_client/parser/properties/property.py +++ b/openapi_python_client/parser/properties/property.py @@ -66,11 +66,11 @@ 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, *, quoted: bool = False) -> str: - """Get the string describing the Python type of this property.""" + """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.""" + """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( From 5fe28f5d803e8e6de43d58d76ece0aee2eafcb37 Mon Sep 17 00:00:00 2001 From: Matvey Ovtsin Date: Sun, 2 Oct 2022 04:59:10 +0300 Subject: [PATCH 18/27] Add test related to `quoted` argument --- tests/conftest.py | 17 ++- .../test_parser/test_properties/test_init.py | 103 +++++++++++++++++- .../test_properties/test_model_property.py | 91 ++++++++++------ .../test_properties/test_property.py | 71 ++++++++---- 4 files changed, 225 insertions(+), 57 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e99ee2e45..7f8442ab7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,7 @@ from openapi_python_client import schema as oai from openapi_python_client.parser.properties import ( AnyProperty, + BooleanProperty, DateProperty, DateTimeProperty, EnumProperty, @@ -43,7 +44,6 @@ def _factory(**kwargs): "lazy_imports": None, "additional_properties": None, "python_name": "", - "description": "", "example": "", **kwargs, } @@ -149,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 afd46caec..5adf8d882 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,30 @@ 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( "required, nullable, expected", ( @@ -103,11 +138,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 +206,9 @@ 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 + @pytest.mark.parametrize( "nullable,required,no_optional,json,expected", [ @@ -173,18 +251,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 +310,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", ( diff --git a/tests/test_parser/test_properties/test_model_property.py b/tests/test_parser/test_properties/test_model_property.py index f4e12b5e0..294ff246c 100644 --- a/tests/test_parser/test_properties/test_model_property.py +++ b/tests/test_parser/test_properties/test_model_property.py @@ -10,48 +10,71 @@ 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) == expected + assert prop.get_type_string(no_optional=no_optional, json=json, quoted=quoted) == expected + def test_get_imports(self, model_property_factory): + prop = model_property_factory(required=False, nullable=True) -def test_get_imports(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_imports(prefix="..") == { - "from typing import Optional", - "from typing import Union", - "from ..types import UNSET, Unset", - "from typing import Dict", - "from typing import cast", - } + 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_lazy_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_lazy_imports(prefix="..") == { - "from ..models.my_module import MyClass", - } + m = model_property_factory() + assert m.get_base_type_string(quoted=quoted) == expected class TestBuildModelProperty: 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 From 843ef7fee5827e21a7e534781132e05a11cee1ab Mon Sep 17 00:00:00 2001 From: Matvey Ovtsin Date: Sun, 2 Oct 2022 05:26:57 +0300 Subject: [PATCH 19/27] remove unused lazy import statement in `ModelProperty.__attrs_post_init__` --- openapi_python_client/parser/properties/model_property.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index 7569b734a..a476dd13d 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -36,8 +36,6 @@ class ModelProperty(Property): def __attrs_post_init__(self) -> None: if self.relative_imports: self.set_relative_imports(self.relative_imports) - if self.lazy_imports: - self.set_lazy_imports(self.lazy_imports) @property def self_import(self) -> str: From 8978e6c85494c92b9308b964620e44c5208ce015 Mon Sep 17 00:00:00 2001 From: Matvey Ovtsin Date: Sun, 2 Oct 2022 05:51:51 +0300 Subject: [PATCH 20/27] remove unused type_string check in `ModelProperty` --- openapi_python_client/parser/properties/model_property.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index a476dd13d..7eb363936 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -116,8 +116,6 @@ def get_type_string( if type_string == self.class_info.name: type_string = f"'{type_string}'" - if type_string == f"List[{self.class_info.name}]": - type_string = f"List['{self.class_info.name}']" if no_optional or (self.required and not self.nullable): return type_string From f1ca6831e46dbabcfd9bcf92aab4d53c22ab14ae Mon Sep 17 00:00:00 2001 From: Matvey Ovtsin Date: Sun, 2 Oct 2022 05:53:58 +0300 Subject: [PATCH 21/27] remove second unused type_string check in `ModelProperty` --- openapi_python_client/parser/properties/model_property.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index 7eb363936..c86236148 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -111,8 +111,6 @@ def get_type_string( if model_parent: if type_string == model_parent.class_info.name: type_string = f"'{type_string}'" - if type_string == f"List[{model_parent.class_info.name}]": - type_string = f"List['{model_parent.class_info.name}']" if type_string == self.class_info.name: type_string = f"'{type_string}'" From 36b00ed889bfc5a25112beb55b083033cb6f7d41 Mon Sep 17 00:00:00 2001 From: Matvey Ovtsin Date: Sun, 2 Oct 2022 06:13:15 +0300 Subject: [PATCH 22/27] remove unused type_string checks in `Property` --- openapi_python_client/parser/properties/property.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/openapi_python_client/parser/properties/property.py b/openapi_python_client/parser/properties/property.py index b5b4e48f4..bc7c14b2e 100644 --- a/openapi_python_client/parser/properties/property.py +++ b/openapi_python_client/parser/properties/property.py @@ -93,12 +93,6 @@ def get_type_string( else: type_string = self.get_base_type_string(quoted=quoted) - if quoted and model_parent: - if type_string == model_parent.class_info.name: - type_string = f"'{type_string}'" - if type_string == f"List[{model_parent.class_info.name}]": - type_string = f"List['{model_parent.class_info.name}']" - if no_optional or (self.required and not self.nullable): return type_string if self.required and self.nullable: From f3e8251ae4abd12126fc1e984dc7e361bf30ab94 Mon Sep 17 00:00:00 2001 From: Matvey Ovtsin Date: Sun, 2 Oct 2022 06:13:47 +0300 Subject: [PATCH 23/27] Add missed tests related to `quoted` and `lazy_import` --- .../test_parser/test_properties/test_init.py | 29 +++++++++++++++++++ .../test_properties/test_model_property.py | 12 ++++++++ 2 files changed, 41 insertions(+) diff --git a/tests/test_parser/test_properties/test_init.py b/tests/test_parser/test_properties/test_init.py index 5adf8d882..0b50a6f65 100644 --- a/tests/test_parser/test_properties/test_init.py +++ b/tests/test_parser/test_properties/test_init.py @@ -129,6 +129,26 @@ 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", ( @@ -209,6 +229,15 @@ 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", [ diff --git a/tests/test_parser/test_properties/test_model_property.py b/tests/test_parser/test_properties/test_model_property.py index 294ff246c..5de4931d4 100644 --- a/tests/test_parser/test_properties/test_model_property.py +++ b/tests/test_parser/test_properties/test_model_property.py @@ -54,6 +54,18 @@ def test_get_imports(self, model_property_factory): "from typing import cast", } + @pytest.mark.parametrize( + "quoted,expected", + [ + (False, "MyClass"), + (True, "'MyClass'"), + ] + ) + def test_get_type_string_parent(self, model_property_factory, quoted, expected): + parent = model_property_factory() + m = model_property_factory() + assert m.get_type_string(model_parent=parent, quoted=quoted) == expected + def test_get_lazy_imports(self, model_property_factory): prop = model_property_factory(required=False, nullable=True) From aaf3c7086e0e2c835a60d0f1ce3ed0bc3f967c5d Mon Sep 17 00:00:00 2001 From: Matvey Ovtsin Date: Sun, 2 Oct 2022 06:19:56 +0300 Subject: [PATCH 24/27] minor: reformat --- tests/test_parser/test_properties/test_init.py | 4 ++-- tests/test_parser/test_properties/test_model_property.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_parser/test_properties/test_init.py b/tests/test_parser/test_properties/test_init.py index 0b50a6f65..85ffe8570 100644 --- a/tests/test_parser/test_properties/test_init.py +++ b/tests/test_parser/test_properties/test_init.py @@ -147,7 +147,7 @@ def test_get_lazy_import_base_inner(self, list_property_factory): 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'} + assert p.get_lazy_imports(prefix="..") == {"from ..models.my_module import MyClass"} @pytest.mark.parametrize( "required, nullable, expected", @@ -236,7 +236,7 @@ def test_get_lazy_import_base_inner(self, union_property_factory): 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'} + assert p.get_lazy_imports(prefix="..") == {"from ..models.my_module import MyClass"} @pytest.mark.parametrize( "nullable,required,no_optional,json,expected", diff --git a/tests/test_parser/test_properties/test_model_property.py b/tests/test_parser/test_properties/test_model_property.py index 5de4931d4..5f0e3d137 100644 --- a/tests/test_parser/test_properties/test_model_property.py +++ b/tests/test_parser/test_properties/test_model_property.py @@ -59,7 +59,7 @@ def test_get_imports(self, model_property_factory): [ (False, "MyClass"), (True, "'MyClass'"), - ] + ], ) def test_get_type_string_parent(self, model_property_factory, quoted, expected): parent = model_property_factory() From ba13956bfc28b47a35718fa2f21f59f5630bba0b Mon Sep 17 00:00:00 2001 From: Matvey Ovtsin Date: Sun, 2 Oct 2022 06:42:08 +0300 Subject: [PATCH 25/27] Remove wrong `model_parent` argument in `Property.get_type_string` --- openapi_python_client/parser/properties/__init__.py | 1 - .../parser/properties/model_property.py | 5 ----- openapi_python_client/parser/properties/property.py | 13 ++++--------- openapi_python_client/templates/model.py.jinja | 4 ++-- .../test_properties/test_model_property.py | 12 ------------ 5 files changed, 6 insertions(+), 29 deletions(-) diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index 03e1d1c48..bc22528b3 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -270,7 +270,6 @@ def get_type_string( no_optional: bool = False, json: bool = False, *, - model_parent: Optional[ModelProperty] = None, # pylint:disable=unused-argument quoted: bool = False, ) -> str: """ diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index c86236148..18e4c4c43 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -92,7 +92,6 @@ def get_type_string( no_optional: bool = False, json: bool = False, *, - model_parent: Optional[ModelProperty] = None, quoted: bool = False, ) -> str: """ @@ -108,10 +107,6 @@ def get_type_string( type_string = self.get_base_type_string() if quoted: - if model_parent: - if type_string == model_parent.class_info.name: - type_string = f"'{type_string}'" - if type_string == self.class_info.name: type_string = f"'{type_string}'" diff --git a/openapi_python_client/parser/properties/property.py b/openapi_python_client/parser/properties/property.py index bc7c14b2e..5f82df224 100644 --- a/openapi_python_client/parser/properties/property.py +++ b/openapi_python_client/parser/properties/property.py @@ -78,7 +78,6 @@ def get_type_string( no_optional: bool = False, json: bool = False, *, - model_parent: Optional[ModelProperty] = None, quoted: bool = False, ) -> str: """ @@ -133,12 +132,8 @@ def get_lazy_imports(self, *, prefix: str) -> Set[str]: """ return set() - def to_string(self, *, model_parent: Optional[ModelProperty] = None) -> str: - """How this should be declared in a dataclass - - Args: - model_parent: The ModelProperty which contains this Property (used for template type annotations) - """ + def to_string(self) -> str: + """How this should be declared in a dataclass""" default: Optional[str] if self.default is not None: default = self.default @@ -148,8 +143,8 @@ def to_string(self, *, model_parent: Optional[ModelProperty] = None) -> str: default = None if default is not None: - return f"{self.python_name}: {self.get_type_string(model_parent=model_parent, quoted=True)} = {default}" - return f"{self.python_name}: {self.get_type_string(model_parent=model_parent, quoted=True)}" + 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""" diff --git a/openapi_python_client/templates/model.py.jinja b/openapi_python_client/templates/model.py.jinja index 05e0d5ff5..98f2d2682 100644 --- a/openapi_python_client/templates/model.py.jinja +++ b/openapi_python_client/templates/model.py.jinja @@ -25,7 +25,7 @@ if TYPE_CHECKING: {% if model.additional_properties %} -{% set additional_property_type = 'Any' if model.additional_properties == True else model.additional_properties.get_type_string(model_parent=model, quoted=not model.additional_properties.is_base_type) %} +{% 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 %} @@ -64,7 +64,7 @@ class {{ class_name }}: {% endfor %} {% for property in model.required_properties + model.optional_properties %} {% if property.default is not none or not property.required %} - {{ property.to_string(model_parent=model) }} + {{ property.to_string() }} {% endif %} {% endfor %} {% if model.additional_properties %} diff --git a/tests/test_parser/test_properties/test_model_property.py b/tests/test_parser/test_properties/test_model_property.py index 5f0e3d137..294ff246c 100644 --- a/tests/test_parser/test_properties/test_model_property.py +++ b/tests/test_parser/test_properties/test_model_property.py @@ -54,18 +54,6 @@ def test_get_imports(self, model_property_factory): "from typing import cast", } - @pytest.mark.parametrize( - "quoted,expected", - [ - (False, "MyClass"), - (True, "'MyClass'"), - ], - ) - def test_get_type_string_parent(self, model_property_factory, quoted, expected): - parent = model_property_factory() - m = model_property_factory() - assert m.get_type_string(model_parent=parent, quoted=quoted) == expected - def test_get_lazy_imports(self, model_property_factory): prop = model_property_factory(required=False, nullable=True) From 8fe2c750361644456bbdb530d29ba2137c4bc922 Mon Sep 17 00:00:00 2001 From: Matvey Ovtsin Date: Sun, 2 Oct 2022 07:49:47 +0300 Subject: [PATCH 26/27] update locked `pydantic` --- poetry.lock | 71 +++++++++++------------------------------------------ 1 file changed, 14 insertions(+), 57 deletions(-) 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"}, From 42a56da84fa00a16aa02d5161d7818cd7983a21c Mon Sep 17 00:00:00 2001 From: Matvey Ovtsin Date: Sun, 2 Oct 2022 08:11:19 +0300 Subject: [PATCH 27/27] Add ModelProperty test `test_process_properties_all_of_reference_not_exist` --- .../test_parser/test_properties/test_model_property.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_parser/test_properties/test_model_property.py b/tests/test_parser/test_properties/test_model_property.py index 294ff246c..7f8a92270 100644 --- a/tests/test_parser/test_properties/test_model_property.py +++ b/tests/test_parser/test_properties/test_model_property.py @@ -333,6 +333,16 @@ def test_process_properties_reference_not_exist(self): 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_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