From 4b7ef35ab683b173e90b6f483217634657f09d9a Mon Sep 17 00:00:00 2001 From: Adam Gray Date: Sun, 12 Apr 2020 11:14:43 +0100 Subject: [PATCH 1/6] Improvements to schema and enum naming --- openapi_python_client/__init__.py | 2 +- .../openapi_parser/openapi.py | 36 +++++++++++-------- .../openapi_parser/properties.py | 4 +-- tests/test___init__.py | 10 ++++-- tests/test_openapi_parser/test_openapi.py | 14 ++++---- tests/test_openapi_parser/test_properties.py | 22 ++++++------ 6 files changed, 51 insertions(+), 37 deletions(-) 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..d78683b4e 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 = None) -> 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..75345de91 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 = field(init=True) 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/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): From 8a76df5ba379297406385a9064ff7e7260729afc Mon Sep 17 00:00:00 2001 From: Adam Gray Date: Sun, 12 Apr 2020 12:00:15 +0100 Subject: [PATCH 2/6] fix output case for enums --- openapi_python_client/openapi_parser/reference.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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),) From d5c1c97942771688963eb975003d517de989450e Mon Sep 17 00:00:00 2001 From: Dylan Anthony <43723790+dbanty@users.noreply.github.com> Date: Sat, 25 Apr 2020 17:29:08 -0400 Subject: [PATCH 3/6] Make `Schema.from_dict` `name` param required --- openapi_python_client/openapi_parser/openapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi_python_client/openapi_parser/openapi.py b/openapi_python_client/openapi_parser/openapi.py index d78683b4e..1fdba1fdb 100644 --- a/openapi_python_client/openapi_parser/openapi.py +++ b/openapi_python_client/openapi_parser/openapi.py @@ -154,7 +154,7 @@ class Schema: relative_imports: Set[str] @staticmethod - def from_dict(d: Dict[str, Any], /, name: str = None) -> Schema: + 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. From 640d1114b8c90cedc5e60c6c69b96360d3a982c2 Mon Sep 17 00:00:00 2001 From: Dylan Anthony <43723790+dbanty@users.noreply.github.com> Date: Sat, 25 Apr 2020 17:31:26 -0400 Subject: [PATCH 4/6] Remove unnecessary `field()` in `EnumProperty` --- openapi_python_client/openapi_parser/properties.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi_python_client/openapi_parser/properties.py b/openapi_python_client/openapi_parser/properties.py index 75345de91..e37c4628d 100644 --- a/openapi_python_client/openapi_parser/properties.py +++ b/openapi_python_client/openapi_parser/properties.py @@ -146,7 +146,7 @@ class EnumProperty(Property): """ A property that should use an enum """ values: Dict[str, str] - reference: Reference = field(init=True) + reference: Reference def __post_init__(self) -> None: inverse_values = {v: k for k, v in self.values.items()} From 855a964b1902a26d0dd9a6a74965a98b6833d0f7 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sat, 25 Apr 2020 17:46:12 -0400 Subject: [PATCH 5/6] Fix a couple broken tests --- tests/test_openapi_parser/test_openapi.py | 2 +- tests/test_openapi_parser/test_properties.py | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/test_openapi_parser/test_openapi.py b/tests/test_openapi_parser/test_openapi.py index 28a2072d1..04a5e199e 100644 --- a/tests/test_openapi_parser/test_openapi.py +++ b/tests/test_openapi_parser/test_openapi.py @@ -189,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( diff --git a/tests/test_openapi_parser/test_properties.py b/tests/test_openapi_parser/test_properties.py index 2d3b4c7ad..148818c58 100644 --- a/tests/test_openapi_parser/test_properties.py +++ b/tests/test_openapi_parser/test_properties.py @@ -174,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={}, reference=mocker.MagicMock(class_name="MyTestEnum")) + 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 @@ -199,7 +201,9 @@ def test_constructor_from_dict(self, mocker): 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") @@ -269,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"], reference=from_ref() + name=name, + required=required, + values=EnumProperty.values_from_list(), + default=data["default"], + reference=from_ref(), ) def test_property_from_dict_ref(self, mocker): From 0398a1d373c9135cddcde994c0bad1e7796e72df Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sat, 25 Apr 2020 17:48:22 -0400 Subject: [PATCH 6/6] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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!