Skip to content

Improve Enum and Schema naming #38

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
2 changes: 1 addition & 1 deletion openapi_python_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
22 changes: 15 additions & 7 deletions openapi_python_client/openapi_parser/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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,
Expand All @@ -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

Expand Down
4 changes: 2 additions & 2 deletions openapi_python_client/openapi_parser/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]}"
Expand Down Expand Up @@ -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:
Expand Down
5 changes: 3 additions & 2 deletions openapi_python_client/openapi_parser/reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
10 changes: 7 additions & 3 deletions tests/test___init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
37 changes: 27 additions & 10 deletions tests/test_openapi_parser/test_openapi.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import pytest

from openapi_python_client.openapi_parser.properties import DateProperty, DateTimeProperty

MODULE_NAME = "openapi_python_client.openapi_parser.openapi"


Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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]

Expand All @@ -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(),
Expand All @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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",
Expand All @@ -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(
Expand Down
33 changes: 21 additions & 12 deletions tests/test_openapi_parser/test_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -250,14 +254,15 @@ 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

p = property_from_dict(name=name, required=required, data=data)

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()

Expand All @@ -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):
Expand Down