diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f0da24d4..711e5a258 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Additions - Link to the GitHub repository from PyPI (#26). Thanks @theY4Kman! - Support for date properties (#30, #37). Thanks @acgray! +- Allow naming schemas by property name and Enums by title (#21, #31, #38). Thanks @acgray! ### Fixes - Fixed some typing issues in generated clients and incorporate mypy into end to end tests (#32). Thanks @acgray! diff --git a/openapi_python_client/__init__.py b/openapi_python_client/__init__.py index bc0e0ae03..ea68d8086 100644 --- a/openapi_python_client/__init__.py +++ b/openapi_python_client/__init__.py @@ -156,7 +156,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 97d424c46..433bf8edb 100644 --- a/openapi_python_client/openapi_parser/openapi.py +++ b/openapi_python_client/openapi_parser/openapi.py @@ -168,14 +168,19 @@ 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(): + ref = Reference.from_ref(d.get("title", name)) + + for key, value in d.get("properties", {}).items(): required = key in required_set p = property_from_dict(name=key, required=required, data=value) if required: @@ -187,9 +192,12 @@ def from_dict(d: Dict[str, Any], /) -> Schema: elif isinstance(p, DateProperty): relative_imports.add("from datetime import date") elif isinstance(p, (ReferenceListProperty, EnumListProperty, RefProperty, EnumProperty)) and p.reference: - relative_imports.add(import_string_from_reference(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, @@ -201,8 +209,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 eb7f14262..9af277e63 100644 --- a/openapi_python_client/openapi_parser/properties.py +++ b/openapi_python_client/openapi_parser/properties.py @@ -169,11 +169,10 @@ 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: super().__post_init__() - 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]}" @@ -254,6 +253,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 1b04402aa..7283201e5 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 = utils.pascal_case(ref_value) + # ugly hack to avoid stringcase ugly pascalcase output when ref_value isn't snake case + class_name = utils.pascal_case(ref_value.replace(" ", "")) if class_name in class_overrides: return class_overrides[class_name] - return Reference(class_name=class_name, module_name=utils.snake_case(ref_value),) + return Reference(class_name=class_name, module_name=utils.snake_case(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 2d5f4a6b2..04a5e199e 100644 --- a/tests/test_openapi_parser/test_openapi.py +++ b/tests/test_openapi_parser/test_openapi.py @@ -1,7 +1,5 @@ import pytest -from openapi_python_client.openapi_parser.properties import DateProperty, DateTimeProperty - MODULE_NAME = "openapi_python_client.openapi_parser.openapi" @@ -45,7 +43,13 @@ def test__check_enums(self, mocker): from openapi_python_client.openapi_parser.properties import EnumProperty, StringProperty def _make_enum(): - return EnumProperty(name=str(mocker.MagicMock()), required=True, default=None, values=mocker.MagicMock(),) + return EnumProperty( + name=str(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() @@ -121,7 +125,13 @@ def test__check_enums_bad_duplicate(self, mocker): schema = mocker.MagicMock() - enum_1 = EnumProperty(name=str(mocker.MagicMock()), required=True, default=None, values=mocker.MagicMock(),) + enum_1 = EnumProperty( + name=str(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] @@ -141,14 +151,19 @@ 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, } def test_from_dict(self, mocker): - from openapi_python_client.openapi_parser.properties import EnumProperty, StringProperty + from openapi_python_client.openapi_parser.properties import ( + EnumProperty, + DateProperty, + DateTimeProperty, + Reference, + ) in_data = { "title": mocker.MagicMock(), @@ -160,7 +175,9 @@ def test_from_dict(self, mocker): "OptionalDate": 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 = DateTimeProperty(name="OptionalDateTime", required=False, default=None) optional_date_property = DateProperty(name="OptionalDate", required=False, default=None) property_from_dict = mocker.patch( @@ -172,7 +189,7 @@ def test_from_dict(self, mocker): from openapi_python_client.openapi_parser.openapi import Schema - result = Schema.from_dict(in_data) + result = Schema.from_dict(in_data, name=mocker.MagicMock()) from_ref.assert_called_once_with(in_data["title"]) property_from_dict.assert_has_calls( @@ -349,7 +366,7 @@ def test__add_parameters_fail_loudly_when_location_not_supported(self, mocker): ) def test__add_parameters_happy(self, mocker): - from openapi_python_client.openapi_parser.openapi import Endpoint, EnumProperty + from openapi_python_client.openapi_parser.openapi import Endpoint, EnumProperty, DateTimeProperty, DateProperty endpoint = Endpoint( path="path", @@ -360,7 +377,7 @@ def test__add_parameters_happy(self, mocker): tag="tag", relative_imports={"import_3"}, ) - path_prop = EnumProperty(name="path_enum", required=True, default=None, values={}) + path_prop = EnumProperty(name="path_enum", required=True, default=None, values={}, reference=mocker.MagicMock()) query_prop_datetime = DateTimeProperty(name="query_datetime", required=False, default=None) query_prop_date = DateProperty(name="query_date", required=False, default=None) propety_from_dict = mocker.patch( diff --git a/tests/test_openapi_parser/test_properties.py b/tests/test_openapi_parser/test_properties.py index d04de014e..148818c58 100644 --- a/tests/test_openapi_parser/test_properties.py +++ b/tests/test_openapi_parser/test_properties.py @@ -153,17 +153,18 @@ def test_get_type_string(self, mocker): class TestEnumProperty: def test___post_init__(self, mocker): name = mocker.MagicMock() - snake_case = mocker.patch(f"openapi_python_client.utils.snake_case") - fake_reference = mocker.MagicMock(class_name="MyTestEnum") - from_ref = mocker.patch(f"{MODULE_NAME}.Reference.from_ref", return_value=fake_reference) + snake_case = mocker.patch(f"openapi_python_client.utils.snake_case") 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" assert enum_property.python_name == snake_case(name) @@ -173,7 +174,9 @@ 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 @@ -185,21 +188,22 @@ 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"the_property_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") == 'MyTestEnum(my_dict["test_enum"])' - enum_property = EnumProperty(name="test_enum", required=False, default=None, values={}) + enum_property = EnumProperty( + name="test_enum", required=False, default=None, values={}, reference=fake_reference + ) assert ( enum_property.constructor_from_dict("my_dict") @@ -250,6 +254,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 @@ -257,7 +262,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() @@ -268,7 +273,11 @@ 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):