Skip to content

Commit 5980501

Browse files
committed
parser / correct recursive {direct,indirect} ref detection logic | todo: cleanup
1 parent 47adea9 commit 5980501

File tree

5 files changed

+91
-8
lines changed

5 files changed

+91
-8
lines changed

end_to_end_tests/golden-record/my_test_api_client/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
""" Contains all the data models used in inputs/outputs """
22

33
from .a_model import AModel
4+
from .a_model_with_direct_self_reference_property import AModelWithDirectSelfReferenceProperty
45
from .a_model_with_indirect_reference_property import AModelWithIndirectReferenceProperty
56
from .a_model_with_indirect_self_reference_property import AModelWithIndirectSelfReferenceProperty
67
from .all_of_sub_model import AllOfSubModel
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from typing import Any, Dict, List, Type, TypeVar, Union
2+
3+
import attr
4+
5+
from ..types import UNSET, Unset
6+
7+
T = TypeVar("T", bound="AModelWithDirectSelfReferenceProperty")
8+
9+
10+
@attr.s(auto_attribs=True)
11+
class AModelWithDirectSelfReferenceProperty:
12+
""" """
13+
14+
required_self_ref: AModelWithDirectSelfReferenceProperty
15+
optional_self_ref: Union[Unset, AModelWithDirectSelfReferenceProperty] = UNSET
16+
additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict)
17+
18+
def to_dict(self) -> Dict[str, Any]:
19+
required_self_ref = self.required_self_ref
20+
optional_self_ref = self.optional_self_ref
21+
22+
field_dict: Dict[str, Any] = {}
23+
field_dict.update(self.additional_properties)
24+
field_dict.update(
25+
{
26+
"required_self_ref": required_self_ref,
27+
}
28+
)
29+
if optional_self_ref is not UNSET:
30+
field_dict["optional_self_ref"] = optional_self_ref
31+
32+
return field_dict
33+
34+
@classmethod
35+
def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
36+
d = src_dict.copy()
37+
required_self_ref = d.pop("required_self_ref")
38+
39+
optional_self_ref = d.pop("optional_self_ref", UNSET)
40+
41+
a_model_with_direct_self_reference_property = cls(
42+
required_self_ref=required_self_ref,
43+
optional_self_ref=optional_self_ref,
44+
)
45+
46+
a_model_with_direct_self_reference_property.additional_properties = d
47+
return a_model_with_direct_self_reference_property
48+
49+
@property
50+
def additional_keys(self) -> List[str]:
51+
return list(self.additional_properties.keys())
52+
53+
def __getitem__(self, key: str) -> Any:
54+
return self.additional_properties[key]
55+
56+
def __setitem__(self, key: str, value: Any) -> None:
57+
self.additional_properties[key] = value
58+
59+
def __delitem__(self, key: str) -> None:
60+
del self.additional_properties[key]
61+
62+
def __contains__(self, key: str) -> bool:
63+
return key in self.additional_properties

end_to_end_tests/openapi.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,6 +1210,20 @@
12101210
"AnEnumIndirectReference": {
12111211
"$ref": "#/components/schemas/AnEnum"
12121212
},
1213+
"AModelWithDirectSelfReferenceProperty": {
1214+
"type": "object",
1215+
"properties": {
1216+
"required_self_ref": {
1217+
"$ref": "#/components/schemas/AModelWithDirectSelfReferenceProperty",
1218+
},
1219+
"optional_self_ref": {
1220+
"$ref": "#/components/schemas/AModelWithDirectSelfReferenceProperty",
1221+
}
1222+
},
1223+
"required": [
1224+
"required_self_ref"
1225+
]
1226+
},
12131227
"AModelWithIndirectSelfReferenceProperty": {
12141228
"type": "object",
12151229
"properties": {

openapi_python_client/parser/properties/__init__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ def _property_from_ref(
466466

467467
existing = schemas.classes_by_reference.get(ref_path)
468468
if not existing or not existing.data:
469-
return PropertyError(data=data, detail="Could not find reference in parsed models or enums"), schemas
469+
return PropertyError(data=data, detail=f"Could not find reference {ref_path} in parsed models or enums"), schemas
470470

471471
if isinstance(existing.data, RecursiveReferenceInterupt):
472472
return (
@@ -613,6 +613,7 @@ def build_schemas(
613613
recursive_references_waiting_reprocess: Dict[str, Union[oai.Reference, oai.Schema]] = dict()
614614
visited: Set[_ReferencePath] = set()
615615
depth = 0
616+
616617
# References could have forward References so keep going as long as we are making progress
617618
while still_making_progress:
618619
still_making_progress = False
@@ -626,17 +627,18 @@ def build_schemas(
626627
schemas.errors.append(PropertyError(detail=ref_path.detail, data=data))
627628
continue
628629

629-
visited.add(ref_path)
630630
schemas_or_err = update_schemas_with(
631-
ref_path=ref_path, data=data, schemas=schemas, visited=visited, config=config
631+
ref_path=ref_path, data=data, schemas=schemas, visited=(ref_path, visited), config=config
632632
)
633+
visited.add(ref_path)
633634
if isinstance(schemas_or_err, PropertyError):
634635
if isinstance(schemas_or_err, RecursiveReferenceInterupt):
635636
up_schemas = schemas_or_err.schemas
636637
assert isinstance(up_schemas, Schemas) # TODO fix typedef in RecursiveReferenceInterupt
637638
schemas_or_err = up_schemas
638639
recursive_references_waiting_reprocess[name] = data
639640
else:
641+
visited.remove(ref_path)
640642
next_round.append((name, data))
641643
errors.append(schemas_or_err)
642644
continue

openapi_python_client/parser/properties/schemas.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
__all__ = ["Class", "Schemas", "parse_reference_path", "update_schemas_with", "_ReferencePath"]
22

3-
from typing import TYPE_CHECKING, Dict, Generic, List, NewType, Optional, Set, TypeVar, Union, cast
3+
from typing import TYPE_CHECKING, Dict, Generic, List, NewType, Optional, Set, Tuple, TypeVar, Union, cast
44
from urllib.parse import urlparse
55

66
import attr
@@ -77,7 +77,7 @@ def update_schemas_with(
7777
ref_path: _ReferencePath,
7878
data: Union[oai.Reference, oai.Schema],
7979
schemas: Schemas,
80-
visited: Set[_ReferencePath],
80+
visited: Tuple[_ReferencePath, Set[_ReferencePath]],
8181
config: Config,
8282
) -> Union[Schemas, PropertyError]:
8383
if isinstance(data, oai.Reference):
@@ -89,12 +89,14 @@ def update_schemas_with(
8989

9090

9191
def _update_schemas_with_reference(
92-
*, ref_path: _ReferencePath, data: oai.Reference, schemas: Schemas, visited: Set[_ReferencePath], config: Config
92+
*, ref_path: _ReferencePath, data: oai.Reference, schemas: Schemas, visited: Tuple[_ReferencePath, Set[_ReferencePath]], config: Config
9393
) -> Union[Schemas, PropertyError]:
9494
reference_pointer = parse_reference_path(data.ref)
9595
if isinstance(reference_pointer, ParseError):
9696
return PropertyError(detail=reference_pointer.detail, data=data)
9797

98+
curr, previous = visited
99+
previous.add(reference_pointer)
98100
resolved_reference = schemas.classes_by_reference.get(reference_pointer)
99101
if resolved_reference:
100102
return attr.evolve(schemas, classes_by_reference={ref_path: resolved_reference, **schemas.classes_by_reference})
@@ -103,7 +105,7 @@ def _update_schemas_with_reference(
103105

104106

105107
def _update_schemas_with_data(
106-
*, ref_path: _ReferencePath, data: oai.Schema, schemas: Schemas, visited: Set[_ReferencePath], config: Config
108+
*, ref_path: _ReferencePath, data: oai.Schema, schemas: Schemas, visited: Tuple[_ReferencePath, Set[_ReferencePath]], config: Config
107109
) -> Union[Schemas, PropertyError]:
108110
from . import build_enum_property, build_model_property
109111

@@ -119,7 +121,8 @@ def _update_schemas_with_data(
119121

120122
holder = schemas.classes_by_reference.get(ref_path)
121123
if isinstance(prop, PropertyError):
122-
if ref_path in visited and not holder:
124+
curr, previous = visited
125+
if (ref_path in previous or curr in f"{prop.data}") and not holder:
123126
holder = _Holder(data=RecursiveReferenceInterupt())
124127
schemas = attr.evolve(schemas, classes_by_reference={ref_path: holder, **schemas.classes_by_reference})
125128
return RecursiveReferenceInterupt(schemas=schemas)

0 commit comments

Comments
 (0)