Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion openapi_python_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
36 changes: 22 additions & 14 deletions openapi_python_client/openapi_parser/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Comment on lines +169 to +180
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a personal preference, I try to avoid multiple layers as much as possible. Something about "cognitive complexity" I read in a book 🧠🤓. Feel free to disagree and leave as is.

Suggested change
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))
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:
required_properties.append(p)
else:
optional_properties.append(p)
if (
isinstance(p, (ReferenceListProperty, EnumListProperty, RefProperty, EnumProperty))
and p.reference
and p.reference.class_name != ref.class_name # don't add an import for self-referencing schemas
):
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 @@ -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

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 @@ -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]}"
Expand Down Expand Up @@ -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:
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 = 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),)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will conflict with #29 most likely, so whichever one gets merged first will trigger an update in the other. Just to be aware.

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
Comment on lines -280 to +284
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this change was because it was difficult to read. Maybe I should be using itemgetter for this, would that be better / more Pythonic you think?

Pretty sure I do this in several tests.

project.package_dir.__truediv__.return_value = models_dir
model_render_1 = mocker.MagicMock()
model_render_2 = mocker.MagicMock()
Expand Down
14 changes: 8 additions & 6 deletions tests/test_openapi_parser/test_openapi.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import pytest

from openapi_python_client.openapi_parser.reference import Reference

MODULE_NAME = "openapi_python_client.openapi_parser.openapi"


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

Expand All @@ -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,
Expand All @@ -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]
Expand Down Expand Up @@ -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()
Expand Down
22 changes: 11 additions & 11 deletions tests/test_openapi_parser/test_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -216,14 +215,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 @@ -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):
Expand Down