Skip to content

Commit 4382760

Browse files
authored
Merge pull request #38 from triaxtec/pr/31
Improve Enum and Schema naming
2 parents f626147 + 0398a1d commit 4382760

File tree

8 files changed

+77
-37
lines changed

8 files changed

+77
-37
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
### Additions
99
- Link to the GitHub repository from PyPI (#26). Thanks @theY4Kman!
1010
- Support for date properties (#30, #37). Thanks @acgray!
11+
- Allow naming schemas by property name and Enums by title (#21, #31, #38). Thanks @acgray!
1112

1213
### Fixes
1314
- Fixed some typing issues in generated clients and incorporate mypy into end to end tests (#32). Thanks @acgray!

openapi_python_client/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ def _build_models(self) -> None:
156156
# Generate enums
157157
enum_template = self.env.get_template("enum.pyi")
158158
for enum in self.openapi.enums.values():
159-
module_path = models_dir / f"{enum.name}.py"
159+
module_path = models_dir / f"{enum.reference.module_name}.py"
160160
module_path.write_text(enum_template.render(enum=enum))
161161
imports.append(import_string_from_reference(enum.reference))
162162

openapi_python_client/openapi_parser/openapi.py

+15-7
Original file line numberDiff line numberDiff line change
@@ -168,14 +168,19 @@ class Schema:
168168
relative_imports: Set[str]
169169

170170
@staticmethod
171-
def from_dict(d: Dict[str, Any], /) -> Schema:
172-
""" A single Schema from its dict representation """
171+
def from_dict(d: Dict[str, Any], /, name: str) -> Schema:
172+
""" A single Schema from its dict representation
173+
:param d: Dict representation of the schema
174+
: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.
175+
"""
173176
required_set = set(d.get("required", []))
174177
required_properties: List[Property] = []
175178
optional_properties: List[Property] = []
176179
relative_imports: Set[str] = set()
177180

178-
for key, value in d["properties"].items():
181+
ref = Reference.from_ref(d.get("title", name))
182+
183+
for key, value in d.get("properties", {}).items():
179184
required = key in required_set
180185
p = property_from_dict(name=key, required=required, data=value)
181186
if required:
@@ -187,9 +192,12 @@ def from_dict(d: Dict[str, Any], /) -> Schema:
187192
elif isinstance(p, DateProperty):
188193
relative_imports.add("from datetime import date")
189194
elif isinstance(p, (ReferenceListProperty, EnumListProperty, RefProperty, EnumProperty)) and p.reference:
190-
relative_imports.add(import_string_from_reference(p.reference))
195+
# don't add an import for self-referencing schemas
196+
if p.reference.class_name != ref.class_name:
197+
relative_imports.add(import_string_from_reference(p.reference))
198+
191199
schema = Schema(
192-
reference=Reference.from_ref(d["title"]),
200+
reference=ref,
193201
required_properties=required_properties,
194202
optional_properties=optional_properties,
195203
relative_imports=relative_imports,
@@ -201,8 +209,8 @@ def from_dict(d: Dict[str, Any], /) -> Schema:
201209
def dict(d: Dict[str, Dict[str, Any]], /) -> Dict[str, Schema]:
202210
""" Get a list of Schemas from an OpenAPI dict """
203211
result = {}
204-
for data in d.values():
205-
s = Schema.from_dict(data)
212+
for name, data in d.items():
213+
s = Schema.from_dict(data, name=name)
206214
result[s.reference.class_name] = s
207215
return result
208216

openapi_python_client/openapi_parser/properties.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -169,11 +169,10 @@ class EnumProperty(Property):
169169
""" A property that should use an enum """
170170

171171
values: Dict[str, str]
172-
reference: Reference = field(init=False)
172+
reference: Reference
173173

174174
def __post_init__(self) -> None:
175175
super().__post_init__()
176-
self.reference = Reference.from_ref(self.name)
177176
inverse_values = {v: k for k, v in self.values.items()}
178177
if self.default is not None:
179178
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
254253
name=name,
255254
required=required,
256255
values=EnumProperty.values_from_list(data["enum"]),
256+
reference=Reference.from_ref(data.get("title", name)),
257257
default=data.get("default"),
258258
)
259259
if "$ref" in data:

openapi_python_client/openapi_parser/reference.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ class Reference:
2121
def from_ref(ref: str) -> Reference:
2222
""" Get a Reference from the openapi #/schemas/blahblah string """
2323
ref_value = ref.split("/")[-1]
24-
class_name = utils.pascal_case(ref_value)
24+
# ugly hack to avoid stringcase ugly pascalcase output when ref_value isn't snake case
25+
class_name = utils.pascal_case(ref_value.replace(" ", ""))
2526

2627
if class_name in class_overrides:
2728
return class_overrides[class_name]
2829

29-
return Reference(class_name=class_name, module_name=utils.snake_case(ref_value),)
30+
return Reference(class_name=class_name, module_name=utils.snake_case(class_name))

tests/test___init__.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -274,10 +274,14 @@ def test__build_models(self, mocker):
274274
"__init__.py": models_init,
275275
f"{schema_1.reference.module_name}.py": schema_1_module_path,
276276
f"{schema_2.reference.module_name}.py": schema_2_module_path,
277-
f"{enum_1.name}.py": enum_1_module_path,
278-
f"{enum_2.name}.py": enum_2_module_path,
277+
f"{enum_1.reference.module_name}.py": enum_1_module_path,
278+
f"{enum_2.reference.module_name}.py": enum_2_module_path,
279279
}
280-
models_dir.__truediv__.side_effect = lambda x: module_paths[x]
280+
281+
def models_dir_get(x):
282+
return module_paths[x]
283+
284+
models_dir.__truediv__.side_effect = models_dir_get
281285
project.package_dir.__truediv__.return_value = models_dir
282286
model_render_1 = mocker.MagicMock()
283287
model_render_2 = mocker.MagicMock()

tests/test_openapi_parser/test_openapi.py

+27-10
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import pytest
22

3-
from openapi_python_client.openapi_parser.properties import DateProperty, DateTimeProperty
4-
53
MODULE_NAME = "openapi_python_client.openapi_parser.openapi"
64

75

@@ -45,7 +43,13 @@ def test__check_enums(self, mocker):
4543
from openapi_python_client.openapi_parser.properties import EnumProperty, StringProperty
4644

4745
def _make_enum():
48-
return EnumProperty(name=str(mocker.MagicMock()), required=True, default=None, values=mocker.MagicMock(),)
46+
return EnumProperty(
47+
name=str(mocker.MagicMock()),
48+
required=True,
49+
default=None,
50+
values=mocker.MagicMock(),
51+
reference=mocker.MagicMock(),
52+
)
4953

5054
# Multiple schemas with both required and optional properties for making sure iteration works correctly
5155
schema_1 = mocker.MagicMock()
@@ -121,7 +125,13 @@ def test__check_enums_bad_duplicate(self, mocker):
121125

122126
schema = mocker.MagicMock()
123127

124-
enum_1 = EnumProperty(name=str(mocker.MagicMock()), required=True, default=None, values=mocker.MagicMock(),)
128+
enum_1 = EnumProperty(
129+
name=str(mocker.MagicMock()),
130+
required=True,
131+
default=None,
132+
values=mocker.MagicMock(),
133+
reference=mocker.MagicMock(),
134+
)
125135
enum_2 = replace(enum_1, values=mocker.MagicMock())
126136
schema.required_properties = [enum_1, enum_2]
127137

@@ -141,14 +151,19 @@ def test_dict(self, mocker):
141151

142152
result = Schema.dict(in_data)
143153

144-
from_dict.assert_has_calls([mocker.call(value) for value in in_data.values()])
154+
from_dict.assert_has_calls([mocker.call(value, name=name) for (name, value) in in_data.items()])
145155
assert result == {
146156
schema_1.reference.class_name: schema_1,
147157
schema_2.reference.class_name: schema_2,
148158
}
149159

150160
def test_from_dict(self, mocker):
151-
from openapi_python_client.openapi_parser.properties import EnumProperty, StringProperty
161+
from openapi_python_client.openapi_parser.properties import (
162+
EnumProperty,
163+
DateProperty,
164+
DateTimeProperty,
165+
Reference,
166+
)
152167

153168
in_data = {
154169
"title": mocker.MagicMock(),
@@ -160,7 +175,9 @@ def test_from_dict(self, mocker):
160175
"OptionalDate": mocker.MagicMock(),
161176
},
162177
}
163-
required_property = EnumProperty(name="RequiredEnum", required=True, default=None, values={},)
178+
required_property = EnumProperty(
179+
name="RequiredEnum", required=True, default=None, values={}, reference=Reference.from_ref("RequiredEnum")
180+
)
164181
optional_property = DateTimeProperty(name="OptionalDateTime", required=False, default=None)
165182
optional_date_property = DateProperty(name="OptionalDate", required=False, default=None)
166183
property_from_dict = mocker.patch(
@@ -172,7 +189,7 @@ def test_from_dict(self, mocker):
172189

173190
from openapi_python_client.openapi_parser.openapi import Schema
174191

175-
result = Schema.from_dict(in_data)
192+
result = Schema.from_dict(in_data, name=mocker.MagicMock())
176193

177194
from_ref.assert_called_once_with(in_data["title"])
178195
property_from_dict.assert_has_calls(
@@ -349,7 +366,7 @@ def test__add_parameters_fail_loudly_when_location_not_supported(self, mocker):
349366
)
350367

351368
def test__add_parameters_happy(self, mocker):
352-
from openapi_python_client.openapi_parser.openapi import Endpoint, EnumProperty
369+
from openapi_python_client.openapi_parser.openapi import Endpoint, EnumProperty, DateTimeProperty, DateProperty
353370

354371
endpoint = Endpoint(
355372
path="path",
@@ -360,7 +377,7 @@ def test__add_parameters_happy(self, mocker):
360377
tag="tag",
361378
relative_imports={"import_3"},
362379
)
363-
path_prop = EnumProperty(name="path_enum", required=True, default=None, values={})
380+
path_prop = EnumProperty(name="path_enum", required=True, default=None, values={}, reference=mocker.MagicMock())
364381
query_prop_datetime = DateTimeProperty(name="query_datetime", required=False, default=None)
365382
query_prop_date = DateProperty(name="query_date", required=False, default=None)
366383
propety_from_dict = mocker.patch(

tests/test_openapi_parser/test_properties.py

+21-12
Original file line numberDiff line numberDiff line change
@@ -153,17 +153,18 @@ def test_get_type_string(self, mocker):
153153
class TestEnumProperty:
154154
def test___post_init__(self, mocker):
155155
name = mocker.MagicMock()
156-
snake_case = mocker.patch(f"openapi_python_client.utils.snake_case")
157-
fake_reference = mocker.MagicMock(class_name="MyTestEnum")
158-
from_ref = mocker.patch(f"{MODULE_NAME}.Reference.from_ref", return_value=fake_reference)
159156

157+
snake_case = mocker.patch(f"openapi_python_client.utils.snake_case")
160158
from openapi_python_client.openapi_parser.properties import EnumProperty
161159

162160
enum_property = EnumProperty(
163-
name=name, required=True, default="second", values={"FIRST": "first", "SECOND": "second"}
161+
name=name,
162+
required=True,
163+
default="second",
164+
values={"FIRST": "first", "SECOND": "second"},
165+
reference=(mocker.MagicMock(class_name="MyTestEnum")),
164166
)
165167

166-
from_ref.assert_called_once_with(name)
167168
assert enum_property.default == "MyTestEnum.SECOND"
168169
assert enum_property.python_name == snake_case(name)
169170

@@ -173,7 +174,9 @@ def test_get_type_string(self, mocker):
173174

174175
from openapi_python_client.openapi_parser.properties import EnumProperty
175176

176-
enum_property = EnumProperty(name="test", required=True, default=None, values={})
177+
enum_property = EnumProperty(
178+
name="test", required=True, default=None, values={}, reference=mocker.MagicMock(class_name="MyTestEnum")
179+
)
177180

178181
assert enum_property.get_type_string() == "MyTestEnum"
179182
enum_property.required = False
@@ -185,21 +188,22 @@ def test_transform(self, mocker):
185188

186189
from openapi_python_client.openapi_parser.properties import EnumProperty
187190

188-
enum_property = EnumProperty(name=name, required=True, default=None, values={})
191+
enum_property = EnumProperty(name=name, required=True, default=None, values={}, reference=mocker.MagicMock())
189192

190193
assert enum_property.transform() == f"the_property_name.value"
191194

192195
def test_constructor_from_dict(self, mocker):
193196
fake_reference = mocker.MagicMock(class_name="MyTestEnum")
194-
mocker.patch(f"{MODULE_NAME}.Reference.from_ref", return_value=fake_reference)
195197

196198
from openapi_python_client.openapi_parser.properties import EnumProperty
197199

198-
enum_property = EnumProperty(name="test_enum", required=True, default=None, values={})
200+
enum_property = EnumProperty(name="test_enum", required=True, default=None, values={}, reference=fake_reference)
199201

200202
assert enum_property.constructor_from_dict("my_dict") == 'MyTestEnum(my_dict["test_enum"])'
201203

202-
enum_property = EnumProperty(name="test_enum", required=False, default=None, values={})
204+
enum_property = EnumProperty(
205+
name="test_enum", required=False, default=None, values={}, reference=fake_reference
206+
)
203207

204208
assert (
205209
enum_property.constructor_from_dict("my_dict")
@@ -250,14 +254,15 @@ def test_property_from_dict_enum(self, mocker):
250254
"enum": mocker.MagicMock(),
251255
}
252256
EnumProperty = mocker.patch(f"{MODULE_NAME}.EnumProperty")
257+
from_ref = mocker.patch(f"{MODULE_NAME}.Reference.from_ref")
253258

254259
from openapi_python_client.openapi_parser.properties import property_from_dict
255260

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

258263
EnumProperty.values_from_list.assert_called_once_with(data["enum"])
259264
EnumProperty.assert_called_once_with(
260-
name=name, required=required, values=EnumProperty.values_from_list(), default=None
265+
name=name, required=required, values=EnumProperty.values_from_list(), default=None, reference=from_ref()
261266
)
262267
assert p == EnumProperty()
263268

@@ -268,7 +273,11 @@ def test_property_from_dict_enum(self, mocker):
268273
name=name, required=required, data=data,
269274
)
270275
EnumProperty.assert_called_once_with(
271-
name=name, required=required, values=EnumProperty.values_from_list(), default=data["default"]
276+
name=name,
277+
required=required,
278+
values=EnumProperty.values_from_list(),
279+
default=data["default"],
280+
reference=from_ref(),
272281
)
273282

274283
def test_property_from_dict_ref(self, mocker):

0 commit comments

Comments
 (0)