diff --git a/openapi_python_client/__init__.py b/openapi_python_client/__init__.py index 2985af7b7..1053e59dd 100644 --- a/openapi_python_client/__init__.py +++ b/openapi_python_client/__init__.py @@ -150,7 +150,7 @@ def _build_models(self) -> None: # Generate enums enum_template = self.env.get_template("enum.pyi") for enum in self.openapi.enums.values(): - module_path = models_dir / f"{enum.name}.py" + module_path = models_dir / f"{enum.reference.module_name}.py" module_path.write_text(enum_template.render(enum=enum)) imports.append(import_string_from_reference(enum.reference)) diff --git a/openapi_python_client/openapi_parser/openapi.py b/openapi_python_client/openapi_parser/openapi.py index 23ffb0c5a..1fdba1fdb 100644 --- a/openapi_python_client/openapi_parser/openapi.py +++ b/openapi_python_client/openapi_parser/openapi.py @@ -154,24 +154,32 @@ class Schema: relative_imports: Set[str] @staticmethod - def from_dict(d: Dict[str, Any], /) -> Schema: - """ A single Schema from its dict representation """ + def from_dict(d: Dict[str, Any], /, name: str) -> Schema: + """ A single Schema from its dict representation + :param d: Dict representation of the schema + :param name: Name by which the schema is referenced, such as a model name. Used to infer the type name if a `title` property is not available. + """ required_set = set(d.get("required", [])) required_properties: List[Property] = [] optional_properties: List[Property] = [] relative_imports: Set[str] = set() - for key, value in d["properties"].items(): - required = key in required_set - p = property_from_dict(name=key, required=required, data=value) - if required: - required_properties.append(p) - else: - optional_properties.append(p) - if isinstance(p, (ReferenceListProperty, EnumListProperty, RefProperty, EnumProperty)) and p.reference: - relative_imports.add(import_string_from_reference(p.reference)) + ref = Reference.from_ref(d.get("title", name)) + + if "properties" in d: + for key, value in d["properties"].items(): + required = key in required_set + p = property_from_dict(name=key, required=required, data=value) + if required: + required_properties.append(p) + else: + optional_properties.append(p) + if isinstance(p, (ReferenceListProperty, EnumListProperty, RefProperty, EnumProperty)) and p.reference: + # don't add an import for self-referencing schemas + if p.reference.class_name != ref.class_name: + relative_imports.add(import_string_from_reference(p.reference)) schema = Schema( - reference=Reference.from_ref(d["title"]), + reference=ref, required_properties=required_properties, optional_properties=optional_properties, relative_imports=relative_imports, @@ -183,8 +191,8 @@ def from_dict(d: Dict[str, Any], /) -> Schema: def dict(d: Dict[str, Dict[str, Any]], /) -> Dict[str, Schema]: """ Get a list of Schemas from an OpenAPI dict """ result = {} - for data in d.values(): - s = Schema.from_dict(data) + for name, data in d.items(): + s = Schema.from_dict(data, name=name) result[s.reference.class_name] = s return result diff --git a/openapi_python_client/openapi_parser/properties.py b/openapi_python_client/openapi_parser/properties.py index fc092b1a6..e37c4628d 100644 --- a/openapi_python_client/openapi_parser/properties.py +++ b/openapi_python_client/openapi_parser/properties.py @@ -146,10 +146,9 @@ class EnumProperty(Property): """ A property that should use an enum """ values: Dict[str, str] - reference: Reference = field(init=False) + reference: Reference def __post_init__(self) -> None: - self.reference = Reference.from_ref(self.name) inverse_values = {v: k for k, v in self.values.items()} if self.default is not None: self.default = f"{self.reference.class_name}.{inverse_values[self.default]}" @@ -227,6 +226,7 @@ def property_from_dict(name: str, required: bool, data: Dict[str, Any]) -> Prope name=name, required=required, values=EnumProperty.values_from_list(data["enum"]), + reference=Reference.from_ref(data.get("title", name)), default=data.get("default"), ) if "$ref" in data: diff --git a/openapi_python_client/openapi_parser/reference.py b/openapi_python_client/openapi_parser/reference.py index 8174dc071..9a6638bdc 100644 --- a/openapi_python_client/openapi_parser/reference.py +++ b/openapi_python_client/openapi_parser/reference.py @@ -21,9 +21,10 @@ class Reference: def from_ref(ref: str) -> Reference: """ Get a Reference from the openapi #/schemas/blahblah string """ ref_value = ref.split("/")[-1] - class_name = stringcase.pascalcase(ref_value) + # ugly hack to avoid stringcase ugly pascalcase output when ref_value isn't snake case + class_name = stringcase.pascalcase(ref_value.replace(" ", "")) if class_name in class_overrides: return class_overrides[class_name] - return Reference(class_name=class_name, module_name=stringcase.snakecase(ref_value),) + return Reference(class_name=class_name, module_name=stringcase.snakecase(class_name),) diff --git a/tests/test___init__.py b/tests/test___init__.py index b84416e3f..a687c20b4 100644 --- a/tests/test___init__.py +++ b/tests/test___init__.py @@ -274,10 +274,14 @@ def test__build_models(self, mocker): "__init__.py": models_init, f"{schema_1.reference.module_name}.py": schema_1_module_path, f"{schema_2.reference.module_name}.py": schema_2_module_path, - f"{enum_1.name}.py": enum_1_module_path, - f"{enum_2.name}.py": enum_2_module_path, + f"{enum_1.reference.module_name}.py": enum_1_module_path, + f"{enum_2.reference.module_name}.py": enum_2_module_path, } - models_dir.__truediv__.side_effect = lambda x: module_paths[x] + + def models_dir_get(x): + return module_paths[x] + + models_dir.__truediv__.side_effect = models_dir_get project.package_dir.__truediv__.return_value = models_dir model_render_1 = mocker.MagicMock() model_render_2 = mocker.MagicMock() diff --git a/tests/test_openapi_parser/test_openapi.py b/tests/test_openapi_parser/test_openapi.py index 537193562..4527de54f 100644 --- a/tests/test_openapi_parser/test_openapi.py +++ b/tests/test_openapi_parser/test_openapi.py @@ -1,5 +1,7 @@ import pytest +from openapi_python_client.openapi_parser.reference import Reference + MODULE_NAME = "openapi_python_client.openapi_parser.openapi" @@ -43,7 +45,7 @@ def test__check_enums(self, mocker): from openapi_python_client.openapi_parser.properties import EnumProperty, StringProperty def _make_enum(): - return EnumProperty(name=mocker.MagicMock(), required=True, default=None, values=mocker.MagicMock(),) + return EnumProperty(name=mocker.MagicMock(), required=True, default=None, values=mocker.MagicMock(), reference=mocker.MagicMock()) # Multiple schemas with both required and optional properties for making sure iteration works correctly schema_1 = mocker.MagicMock() @@ -119,7 +121,7 @@ def test__check_enums_bad_duplicate(self, mocker): schema = mocker.MagicMock() - enum_1 = EnumProperty(name=mocker.MagicMock(), required=True, default=None, values=mocker.MagicMock(),) + enum_1 = EnumProperty(name=mocker.MagicMock(), required=True, default=None, values=mocker.MagicMock(), reference=mocker.MagicMock()) enum_2 = replace(enum_1, values=mocker.MagicMock()) schema.required_properties = [enum_1, enum_2] @@ -139,7 +141,7 @@ def test_dict(self, mocker): result = Schema.dict(in_data) - from_dict.assert_has_calls([mocker.call(value) for value in in_data.values()]) + from_dict.assert_has_calls([mocker.call(value, name=name) for (name, value) in in_data.items()]) assert result == { schema_1.reference.class_name: schema_1, schema_2.reference.class_name: schema_2, @@ -154,7 +156,7 @@ def test_from_dict(self, mocker): "required": ["RequiredEnum"], "properties": {"RequiredEnum": mocker.MagicMock(), "OptionalString": mocker.MagicMock(),}, } - required_property = EnumProperty(name="RequiredEnum", required=True, default=None, values={},) + required_property = EnumProperty(name="RequiredEnum", required=True, default=None, values={}, reference=Reference.from_ref("RequiredEnum")) optional_property = StringProperty(name="OptionalString", required=False, default=None) property_from_dict = mocker.patch( f"{MODULE_NAME}.property_from_dict", side_effect=[required_property, optional_property] @@ -347,8 +349,8 @@ def test__add_parameters_happy(self, mocker): tag="tag", relative_imports={"import_3"}, ) - path_prop = EnumProperty(name="path_enum", required=True, default=None, values={}) - query_prop = EnumProperty(name="query_enum", required=False, default=None, values={}) + path_prop = EnumProperty(name="path_enum", required=True, default=None, values={}, reference=mocker.MagicMock()) + query_prop = EnumProperty(name="query_enum", required=False, default=None, values={}, reference=mocker.MagicMock()) propety_from_dict = mocker.patch(f"{MODULE_NAME}.property_from_dict", side_effect=[path_prop, query_prop]) path_schema = mocker.MagicMock() query_schema = mocker.MagicMock() diff --git a/tests/test_openapi_parser/test_properties.py b/tests/test_openapi_parser/test_properties.py index ead21aed0..d1411101c 100644 --- a/tests/test_openapi_parser/test_properties.py +++ b/tests/test_openapi_parser/test_properties.py @@ -125,16 +125,16 @@ def test_get_type_string(self, mocker): class TestEnumProperty: def test___post_init__(self, mocker): name = mocker.MagicMock() - fake_reference = mocker.MagicMock(class_name="MyTestEnum") - from_ref = mocker.patch(f"{MODULE_NAME}.Reference.from_ref", return_value=fake_reference) - from openapi_python_client.openapi_parser.properties import EnumProperty enum_property = EnumProperty( - name=name, required=True, default="second", values={"FIRST": "first", "SECOND": "second"} + name=name, + required=True, + default="second", + values={"FIRST": "first", "SECOND": "second"}, + reference=(mocker.MagicMock(class_name="MyTestEnum")), ) - from_ref.assert_called_once_with(name) assert enum_property.default == "MyTestEnum.SECOND" def test_get_type_string(self, mocker): @@ -143,7 +143,7 @@ def test_get_type_string(self, mocker): from openapi_python_client.openapi_parser.properties import EnumProperty - enum_property = EnumProperty(name="test", required=True, default=None, values={}) + enum_property = EnumProperty(name="test", required=True, default=None, values={}, reference=mocker.MagicMock(class_name="MyTestEnum")) assert enum_property.get_type_string() == "MyTestEnum" enum_property.required = False @@ -155,17 +155,16 @@ def test_transform(self, mocker): from openapi_python_client.openapi_parser.properties import EnumProperty - enum_property = EnumProperty(name=name, required=True, default=None, values={}) + enum_property = EnumProperty(name=name, required=True, default=None, values={}, reference=mocker.MagicMock()) assert enum_property.transform() == f"{name}.value" def test_constructor_from_dict(self, mocker): fake_reference = mocker.MagicMock(class_name="MyTestEnum") - mocker.patch(f"{MODULE_NAME}.Reference.from_ref", return_value=fake_reference) from openapi_python_client.openapi_parser.properties import EnumProperty - enum_property = EnumProperty(name="test_enum", required=True, default=None, values={}) + enum_property = EnumProperty(name="test_enum", required=True, default=None, values={}, reference=fake_reference) assert ( enum_property.constructor_from_dict("my_dict") @@ -216,6 +215,7 @@ def test_property_from_dict_enum(self, mocker): "enum": mocker.MagicMock(), } EnumProperty = mocker.patch(f"{MODULE_NAME}.EnumProperty") + from_ref = mocker.patch(f"{MODULE_NAME}.Reference.from_ref") from openapi_python_client.openapi_parser.properties import property_from_dict @@ -223,7 +223,7 @@ def test_property_from_dict_enum(self, mocker): EnumProperty.values_from_list.assert_called_once_with(data["enum"]) EnumProperty.assert_called_once_with( - name=name, required=required, values=EnumProperty.values_from_list(), default=None + name=name, required=required, values=EnumProperty.values_from_list(), default=None, reference=from_ref() ) assert p == EnumProperty() @@ -234,7 +234,7 @@ def test_property_from_dict_enum(self, mocker): name=name, required=required, data=data, ) EnumProperty.assert_called_once_with( - name=name, required=required, values=EnumProperty.values_from_list(), default=data["default"] + name=name, required=required, values=EnumProperty.values_from_list(), default=data["default"], reference=from_ref() ) def test_property_from_dict_ref(self, mocker):