Skip to content

Commit b719316

Browse files
authored
fix!: Change reference resolution to use reference path instead of class name (fixes #342) (#366)
* refactorWIP refactor reference / class name handling to fix resolution errors * refactorRevert field_prefix refactor, continue class name refactor [WIP] * test: Fix all the type errors and start fixing unit tests * test: Fix more broken tests * test: Fix more unit tests * test: Finish fixing broken unit tests * test: Fix errors found by E2E test * docs: Update usage.md * test: Add more unit test coverage * fix: nullable passthrough in single union refs * docs: Improve `properties.Class` docstring
1 parent bf575fb commit b719316

37 files changed

+1233
-856
lines changed

Diff for: end_to_end_tests/golden-record/my_test_api_client/models/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from .free_form_model import FreeFormModel
1111
from .http_validation_error import HTTPValidationError
1212
from .model_from_all_of import ModelFromAllOf
13+
from .model_name import ModelName
1314
from .model_with_additional_properties_inlined import ModelWithAdditionalPropertiesInlined
1415
from .model_with_additional_properties_inlined_additional_property import (
1516
ModelWithAdditionalPropertiesInlinedAdditionalProperty,
@@ -19,6 +20,7 @@
1920
from .model_with_any_json_properties_additional_property_type0 import ModelWithAnyJsonPropertiesAdditionalPropertyType0
2021
from .model_with_primitive_additional_properties import ModelWithPrimitiveAdditionalProperties
2122
from .model_with_primitive_additional_properties_a_date_holder import ModelWithPrimitiveAdditionalPropertiesADateHolder
23+
from .model_with_property_ref import ModelWithPropertyRef
2224
from .model_with_union_property import ModelWithUnionProperty
2325
from .model_with_union_property_inlined import ModelWithUnionPropertyInlined
2426
from .model_with_union_property_inlined_fruit_type0 import ModelWithUnionPropertyInlinedFruitType0
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from typing import Any, Dict, List, Type, TypeVar
2+
3+
import attr
4+
5+
T = TypeVar("T", bound="ModelName")
6+
7+
8+
@attr.s(auto_attribs=True)
9+
class ModelName:
10+
""" """
11+
12+
additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict)
13+
14+
def to_dict(self) -> Dict[str, Any]:
15+
16+
field_dict: Dict[str, Any] = {}
17+
field_dict.update(self.additional_properties)
18+
field_dict.update({})
19+
20+
return field_dict
21+
22+
@classmethod
23+
def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
24+
d = src_dict.copy()
25+
model_name = cls()
26+
27+
model_name.additional_properties = d
28+
return model_name
29+
30+
@property
31+
def additional_keys(self) -> List[str]:
32+
return list(self.additional_properties.keys())
33+
34+
def __getitem__(self, key: str) -> Any:
35+
return self.additional_properties[key]
36+
37+
def __setitem__(self, key: str, value: Any) -> None:
38+
self.additional_properties[key] = value
39+
40+
def __delitem__(self, key: str) -> None:
41+
del self.additional_properties[key]
42+
43+
def __contains__(self, key: str) -> bool:
44+
return key in self.additional_properties
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from typing import Any, Dict, List, Type, TypeVar, Union
2+
3+
import attr
4+
5+
from ..models.model_name import ModelName
6+
from ..types import UNSET, Unset
7+
8+
T = TypeVar("T", bound="ModelWithPropertyRef")
9+
10+
11+
@attr.s(auto_attribs=True)
12+
class ModelWithPropertyRef:
13+
""" """
14+
15+
inner: Union[Unset, ModelName] = UNSET
16+
additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict)
17+
18+
def to_dict(self) -> Dict[str, Any]:
19+
inner: Union[Unset, Dict[str, Any]] = UNSET
20+
if not isinstance(self.inner, Unset):
21+
inner = self.inner.to_dict()
22+
23+
field_dict: Dict[str, Any] = {}
24+
field_dict.update(self.additional_properties)
25+
field_dict.update({})
26+
if inner is not UNSET:
27+
field_dict["inner"] = inner
28+
29+
return field_dict
30+
31+
@classmethod
32+
def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
33+
d = src_dict.copy()
34+
inner: Union[Unset, ModelName] = UNSET
35+
_inner = d.pop("inner", UNSET)
36+
if not isinstance(_inner, Unset):
37+
inner = ModelName.from_dict(_inner)
38+
39+
model_with_property_ref = cls(
40+
inner=inner,
41+
)
42+
43+
model_with_property_ref.additional_properties = d
44+
return model_with_property_ref
45+
46+
@property
47+
def additional_keys(self) -> List[str]:
48+
return list(self.additional_properties.keys())
49+
50+
def __getitem__(self, key: str) -> Any:
51+
return self.additional_properties[key]
52+
53+
def __setitem__(self, key: str, value: Any) -> None:
54+
self.additional_properties[key] = value
55+
56+
def __delitem__(self, key: str) -> None:
57+
del self.additional_properties[key]
58+
59+
def __contains__(self, key: str) -> bool:
60+
return key in self.additional_properties

Diff for: end_to_end_tests/openapi.json

+18-8
Original file line numberDiff line numberDiff line change
@@ -829,43 +829,43 @@
829829
"one_of_models": {
830830
"oneOf": [
831831
{
832-
"ref": "#components/schemas/FreeFormModel"
832+
"ref": "#/components/schemas/FreeFormModel"
833833
},
834834
{
835-
"ref": "#components/schemas/ModelWithUnionProperty"
835+
"ref": "#/components/schemas/ModelWithUnionProperty"
836836
}
837837
],
838838
"nullable": false
839839
},
840840
"nullable_one_of_models": {
841841
"oneOf": [
842842
{
843-
"ref": "#components/schemas/FreeFormModel"
843+
"ref": "#/components/schemas/FreeFormModel"
844844
},
845845
{
846-
"ref": "#components/schemas/ModelWithUnionProperty"
846+
"ref": "#/components/schemas/ModelWithUnionProperty"
847847
}
848848
],
849849
"nullable": true
850850
},
851851
"not_required_one_of_models": {
852852
"oneOf": [
853853
{
854-
"ref": "#components/schemas/FreeFormModel"
854+
"ref": "#/components/schemas/FreeFormModel"
855855
},
856856
{
857-
"ref": "#components/schemas/ModelWithUnionProperty"
857+
"ref": "#/components/schemas/ModelWithUnionProperty"
858858
}
859859
],
860860
"nullable": false
861861
},
862862
"not_required_nullable_one_of_models": {
863863
"oneOf": [
864864
{
865-
"ref": "#components/schemas/FreeFormModel"
865+
"ref": "#/components/schemas/FreeFormModel"
866866
},
867867
{
868-
"ref": "#components/schemas/ModelWithUnionProperty"
868+
"ref": "#/components/schemas/ModelWithUnionProperty"
869869
},
870870
{
871871
"type": "string"
@@ -1120,6 +1120,16 @@
11201120
"type": "string"
11211121
}
11221122
}
1123+
},
1124+
"model_reference_doesnt_match": {
1125+
"title": "ModelName",
1126+
"type": "object"
1127+
},
1128+
"ModelWithPropertyRef": {
1129+
"type": "object",
1130+
"properties": {
1131+
"inner": {"$ref": "#/components/schemas/model_reference_doesnt_match"}
1132+
}
11231133
}
11241134
}
11251135
}

Diff for: end_to_end_tests/regen_golden_record.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
shutil.rmtree(gr_path, ignore_errors=True)
1919
shutil.rmtree(output_path, ignore_errors=True)
2020

21-
result = runner.invoke(app, [f"--config={config_path}", "generate", f"--path={openapi_path}"])
21+
result = runner.invoke(app, ["generate", f"--config={config_path}", f"--path={openapi_path}"])
2222

2323
if result.stdout:
2424
print(result.stdout)

Diff for: end_to_end_tests/test_end_to_end.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def run_e2e_test(extra_args=None, expected_differences=None):
5353
output_path = Path.cwd() / "my-test-api-client"
5454
shutil.rmtree(output_path, ignore_errors=True)
5555

56-
args = [f"--config={config_path}", "generate", f"--path={openapi_path}"]
56+
args = ["generate", f"--config={config_path}", f"--path={openapi_path}"]
5757
if extra_args:
5858
args.extend(extra_args)
5959
result = runner.invoke(app, args)

Diff for: openapi_python_client/__init__.py

+35-18
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414

1515
from openapi_python_client import utils
1616

17-
from .parser import GeneratorData, import_string_from_reference
17+
from .config import Config
18+
from .parser import GeneratorData, import_string_from_class
1819
from .parser.errors import GeneratorError
1920
from .utils import snake_case
2021

@@ -41,15 +42,12 @@ class MetaType(str, Enum):
4142

4243

4344
class Project:
44-
project_name_override: Optional[str] = None
45-
package_name_override: Optional[str] = None
46-
package_version_override: Optional[str] = None
47-
4845
def __init__(
4946
self,
5047
*,
5148
openapi: GeneratorData,
5249
meta: MetaType,
50+
config: Config,
5351
custom_template_path: Optional[Path] = None,
5452
file_encoding: str = "utf-8",
5553
) -> None:
@@ -70,17 +68,17 @@ def __init__(
7068
loader = package_loader
7169
self.env: Environment = Environment(loader=loader, trim_blocks=True, lstrip_blocks=True)
7270

73-
self.project_name: str = self.project_name_override or f"{utils.kebab_case(openapi.title).lower()}-client"
71+
self.project_name: str = config.project_name_override or f"{utils.kebab_case(openapi.title).lower()}-client"
7472
self.project_dir: Path = Path.cwd()
7573
if meta != MetaType.NONE:
7674
self.project_dir /= self.project_name
7775

78-
self.package_name: str = self.package_name_override or self.project_name.replace("-", "_")
76+
self.package_name: str = config.package_name_override or self.project_name.replace("-", "_")
7977
self.package_dir: Path = self.project_dir / self.package_name
8078
self.package_description: str = utils.remove_string_escapes(
8179
f"A client library for accessing {self.openapi.title}"
8280
)
83-
self.version: str = self.package_version_override or openapi.version
81+
self.version: str = config.package_version_override or openapi.version
8482

8583
self.env.filters.update(TEMPLATE_FILTERS)
8684

@@ -215,21 +213,21 @@ def _build_models(self) -> None:
215213
imports = []
216214

217215
model_template = self.env.get_template("model.py.jinja")
218-
for model in self.openapi.models.values():
219-
module_path = models_dir / f"{model.reference.module_name}.py"
216+
for model in self.openapi.models:
217+
module_path = models_dir / f"{model.class_info.module_name}.py"
220218
module_path.write_text(model_template.render(model=model), encoding=self.file_encoding)
221-
imports.append(import_string_from_reference(model.reference))
219+
imports.append(import_string_from_class(model.class_info))
222220

223221
# Generate enums
224222
str_enum_template = self.env.get_template("str_enum.py.jinja")
225223
int_enum_template = self.env.get_template("int_enum.py.jinja")
226-
for enum in self.openapi.enums.values():
227-
module_path = models_dir / f"{enum.reference.module_name}.py"
224+
for enum in self.openapi.enums:
225+
module_path = models_dir / f"{enum.class_info.module_name}.py"
228226
if enum.value_type is int:
229227
module_path.write_text(int_enum_template.render(enum=enum), encoding=self.file_encoding)
230228
else:
231229
module_path.write_text(str_enum_template.render(enum=enum), encoding=self.file_encoding)
232-
imports.append(import_string_from_reference(enum.reference))
230+
imports.append(import_string_from_class(enum.class_info))
233231

234232
models_init_template = self.env.get_template("models_init.py.jinja")
235233
models_init.write_text(models_init_template.render(imports=imports), encoding=self.file_encoding)
@@ -261,23 +259,31 @@ def _get_project_for_url_or_path(
261259
url: Optional[str],
262260
path: Optional[Path],
263261
meta: MetaType,
262+
config: Config,
264263
custom_template_path: Optional[Path] = None,
265264
file_encoding: str = "utf-8",
266265
) -> Union[Project, GeneratorError]:
267266
data_dict = _get_document(url=url, path=path)
268267
if isinstance(data_dict, GeneratorError):
269268
return data_dict
270-
openapi = GeneratorData.from_dict(data_dict)
269+
openapi = GeneratorData.from_dict(data_dict, config=config)
271270
if isinstance(openapi, GeneratorError):
272271
return openapi
273-
return Project(openapi=openapi, custom_template_path=custom_template_path, meta=meta, file_encoding=file_encoding)
272+
return Project(
273+
openapi=openapi,
274+
custom_template_path=custom_template_path,
275+
meta=meta,
276+
file_encoding=file_encoding,
277+
config=config,
278+
)
274279

275280

276281
def create_new_client(
277282
*,
278283
url: Optional[str],
279284
path: Optional[Path],
280285
meta: MetaType,
286+
config: Config,
281287
custom_template_path: Optional[Path] = None,
282288
file_encoding: str = "utf-8",
283289
) -> Sequence[GeneratorError]:
@@ -288,7 +294,12 @@ def create_new_client(
288294
A list containing any errors encountered when generating.
289295
"""
290296
project = _get_project_for_url_or_path(
291-
url=url, path=path, custom_template_path=custom_template_path, meta=meta, file_encoding=file_encoding
297+
url=url,
298+
path=path,
299+
custom_template_path=custom_template_path,
300+
meta=meta,
301+
file_encoding=file_encoding,
302+
config=config,
292303
)
293304
if isinstance(project, GeneratorError):
294305
return [project]
@@ -300,6 +311,7 @@ def update_existing_client(
300311
url: Optional[str],
301312
path: Optional[Path],
302313
meta: MetaType,
314+
config: Config,
303315
custom_template_path: Optional[Path] = None,
304316
file_encoding: str = "utf-8",
305317
) -> Sequence[GeneratorError]:
@@ -310,7 +322,12 @@ def update_existing_client(
310322
A list containing any errors encountered when generating.
311323
"""
312324
project = _get_project_for_url_or_path(
313-
url=url, path=path, custom_template_path=custom_template_path, meta=meta, file_encoding=file_encoding
325+
url=url,
326+
path=path,
327+
custom_template_path=custom_template_path,
328+
meta=meta,
329+
file_encoding=file_encoding,
330+
config=config,
314331
)
315332
if isinstance(project, GeneratorError):
316333
return [project]

0 commit comments

Comments
 (0)