Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit c7da89e

Browse files
committedMay 8, 2021
parser / property / add recursive reference resoltion
1 parent 5696079 commit c7da89e

File tree

5 files changed

+143
-24
lines changed

5 files changed

+143
-24
lines changed
 

‎openapi_python_client/parser/errors.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from dataclasses import dataclass
22
from enum import Enum
3-
from typing import Optional
3+
from typing import Any, Optional
44

55
__all__ = ["ErrorLevel", "GeneratorError", "ParseError", "PropertyError", "ValidationError"]
66

@@ -39,5 +39,12 @@ class PropertyError(ParseError):
3939
header = "Problem creating a Property: "
4040

4141

42+
@dataclass
43+
class RecursiveReferenceInterupt(PropertyError):
44+
"""Error raised when a property have an recursive reference to itself"""
45+
46+
schemas: Optional[Any] = None # TODO: shall not use Any here, shall be Schemas, to fix later
47+
48+
4249
class ValidationError(Exception):
4350
pass
Binary file not shown.

‎openapi_python_client/parser/properties/__init__.py

+85-9
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@
1717
from ... import Config
1818
from ... import schema as oai
1919
from ... import utils
20-
from ..errors import ParseError, PropertyError, ValidationError
20+
from ..errors import ParseError, PropertyError, RecursiveReferenceInterupt, ValidationError
2121
from .converter import convert, convert_chain
2222
from .enum_property import EnumProperty
2323
from .model_property import ModelProperty, build_model_property
2424
from .property import Property
25-
from .schemas import Class, Schemas, parse_reference_path, update_schemas_with
25+
from .schemas import Class, Schemas, _Holder, _ReferencePath, parse_reference_path, update_schemas_with
2626

2727

2828
@attr.s(auto_attribs=True, frozen=True)
@@ -34,6 +34,59 @@ class NoneProperty(Property):
3434
template: ClassVar[Optional[str]] = "none_property.py.jinja"
3535

3636

37+
@attr.s(auto_attribs=True, frozen=True)
38+
class LazySelfReferenceProperty(Property):
39+
"""A property used to resolve recursive reference.
40+
It proxyfy the required method call to its binded Property owner
41+
"""
42+
43+
owner: _Holder[Union[ModelProperty, EnumProperty, RecursiveReferenceInterupt]]
44+
_resolved: bool = False
45+
46+
def get_base_type_string(self) -> str:
47+
self._ensure_resolved()
48+
49+
prop = self.owner.data
50+
assert isinstance(prop, Property)
51+
return prop.get_base_type_string()
52+
53+
def get_base_json_type_string(self) -> str:
54+
self._ensure_resolved()
55+
56+
prop = self.owner.data
57+
assert isinstance(prop, Property)
58+
return prop.get_base_json_type_string()
59+
60+
def get_type_string(self, no_optional: bool = False, json: bool = False) -> str:
61+
self._ensure_resolved()
62+
63+
prop = self.owner.data
64+
assert isinstance(prop, Property)
65+
return prop.get_type_string(no_optional, json)
66+
67+
def get_instance_type_string(self) -> str:
68+
self._ensure_resolved()
69+
return super().get_instance_type_string()
70+
71+
def to_string(self) -> str:
72+
self._ensure_resolved()
73+
74+
if not self.required:
75+
return f"{self.python_name}: Union[Unset, {self.get_type_string()}] = UNSET"
76+
else:
77+
return f"{self.python_name}: {self.get_type_string()}"
78+
79+
def _ensure_resolved(self) -> None:
80+
if self._resolved:
81+
return
82+
83+
if not isinstance(self.owner.data, Property):
84+
raise RuntimeError(f"LazySelfReferenceProperty {self.name} owner shall have been resolved.")
85+
else:
86+
object.__setattr__(self, "_resolved", True)
87+
object.__setattr__(self, "nullable", self.owner.data.nullable)
88+
89+
3790
@attr.s(auto_attribs=True, frozen=True)
3891
class StringProperty(Property):
3992
"""A property of type str"""
@@ -410,11 +463,18 @@ def _property_from_ref(
410463
ref_path = parse_reference_path(data.ref)
411464
if isinstance(ref_path, ParseError):
412465
return PropertyError(data=data, detail=ref_path.detail), schemas
466+
413467
existing = schemas.classes_by_reference.get(ref_path)
414-
if not existing:
468+
if not existing or not existing.data:
415469
return PropertyError(data=data, detail="Could not find reference in parsed models or enums"), schemas
416470

417-
prop = attr.evolve(existing, required=required, name=name)
471+
if isinstance(existing.data, RecursiveReferenceInterupt):
472+
return (
473+
LazySelfReferenceProperty(required=required, name=name, nullable=False, default=None, owner=existing),
474+
schemas,
475+
)
476+
477+
prop = attr.evolve(existing.data, required=required, name=name)
418478
if parent:
419479
prop = attr.evolve(prop, nullable=parent.nullable)
420480
if isinstance(prop, EnumProperty):
@@ -550,28 +610,44 @@ def build_schemas(
550610
to_process: Iterable[Tuple[str, Union[oai.Reference, oai.Schema]]] = components.items()
551611
still_making_progress = True
552612
errors: List[PropertyError] = []
553-
613+
recursive_references_waiting_reprocess: Dict[str, Union[oai.Reference, oai.Schema]] = dict()
614+
visited: Set[_ReferencePath] = set()
615+
depth = 0
554616
# References could have forward References so keep going as long as we are making progress
555617
while still_making_progress:
556618
still_making_progress = False
557619
errors = []
558620
next_round = []
621+
559622
# Only accumulate errors from the last round, since we might fix some along the way
560623
for name, data in to_process:
561624
ref_path = parse_reference_path(f"#/components/schemas/{name}")
562625
if isinstance(ref_path, ParseError):
563626
schemas.errors.append(PropertyError(detail=ref_path.detail, data=data))
564627
continue
565628

566-
schemas_or_err = update_schemas_with(ref_path=ref_path, data=data, schemas=schemas, config=config)
629+
visited.add(ref_path)
630+
schemas_or_err = update_schemas_with(
631+
ref_path=ref_path, data=data, schemas=schemas, visited=visited, config=config
632+
)
567633
if isinstance(schemas_or_err, PropertyError):
568-
next_round.append((name, data))
569-
errors.append(schemas_or_err)
570-
continue
634+
if isinstance(schemas_or_err, RecursiveReferenceInterupt):
635+
up_schemas = schemas_or_err.schemas
636+
assert isinstance(up_schemas, Schemas) # TODO fix typedef in RecursiveReferenceInterupt
637+
schemas_or_err = up_schemas
638+
recursive_references_waiting_reprocess[name] = data
639+
else:
640+
next_round.append((name, data))
641+
errors.append(schemas_or_err)
642+
continue
571643

572644
schemas = schemas_or_err
573645
still_making_progress = True
646+
depth += 1
574647
to_process = next_round
575648

649+
if len(recursive_references_waiting_reprocess.keys()):
650+
schemas = build_schemas(components=recursive_references_waiting_reprocess, schemas=schemas, config=config)
651+
576652
schemas.errors.extend(errors)
577653
return schemas

‎openapi_python_client/parser/properties/model_property.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111
from .schemas import Class, Schemas, parse_reference_path
1212

1313

14+
@attr.s(auto_attribs=True, frozen=True)
15+
class RecusiveProperty(Property):
16+
pass
17+
18+
1419
@attr.s(auto_attribs=True, frozen=True)
1520
class ModelProperty(Property):
1621
"""A property which refers to another Schema"""
@@ -93,9 +98,12 @@ def _check_existing(prop: Property) -> Union[Property, PropertyError]:
9398
ref_path = parse_reference_path(sub_prop.ref)
9499
if isinstance(ref_path, ParseError):
95100
return PropertyError(detail=ref_path.detail, data=sub_prop)
96-
sub_model = schemas.classes_by_reference.get(ref_path)
97-
if sub_model is None:
101+
102+
sub_model_ref = schemas.classes_by_reference.get(ref_path)
103+
if sub_model_ref is None or not isinstance(sub_model_ref.data, Property):
98104
return PropertyError(f"Reference {sub_prop.ref} not found")
105+
106+
sub_model = sub_model_ref.data
99107
if not isinstance(sub_model, ModelProperty):
100108
return PropertyError("Cannot take allOf a non-object")
101109
for prop in chain(sub_model.required_properties, sub_model.optional_properties):

‎openapi_python_client/parser/properties/schemas.py

+40-12
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
__all__ = ["Class", "Schemas", "parse_reference_path", "update_schemas_with"]
1+
__all__ = ["Class", "Schemas", "parse_reference_path", "update_schemas_with", "_ReferencePath"]
22

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

66
import attr
77

88
from ... import Config
99
from ... import schema as oai
1010
from ... import utils
11-
from ..errors import ParseError, PropertyError
11+
from ..errors import ParseError, PropertyError, RecursiveReferenceInterupt
1212

1313
if TYPE_CHECKING: # pragma: no cover
1414
from .enum_property import EnumProperty
@@ -17,7 +17,7 @@
1717
EnumProperty = "EnumProperty"
1818
ModelProperty = "ModelProperty"
1919

20-
20+
T = TypeVar("T")
2121
_ReferencePath = NewType("_ReferencePath", str)
2222
_ClassName = NewType("_ClassName", str)
2323

@@ -29,6 +29,11 @@ def parse_reference_path(ref_path_raw: str) -> Union[_ReferencePath, ParseError]
2929
return cast(_ReferencePath, parsed.fragment)
3030

3131

32+
@attr.s(auto_attribs=True)
33+
class _Holder(Generic[T]):
34+
data: Optional[T]
35+
36+
3237
@attr.s(auto_attribs=True, frozen=True)
3338
class Class:
3439
"""Represents Python class which will be generated from an OpenAPI schema"""
@@ -58,22 +63,33 @@ def from_string(*, string: str, config: Config) -> "Class":
5863
class Schemas:
5964
"""Structure for containing all defined, shareable, and reusable schemas (attr classes and Enums)"""
6065

61-
classes_by_reference: Dict[_ReferencePath, Union[EnumProperty, ModelProperty]] = attr.ib(factory=dict)
62-
classes_by_name: Dict[_ClassName, Union[EnumProperty, ModelProperty]] = attr.ib(factory=dict)
66+
classes_by_reference: Dict[
67+
_ReferencePath, _Holder[Union[EnumProperty, ModelProperty, RecursiveReferenceInterupt]]
68+
] = attr.ib(factory=dict)
69+
classes_by_name: Dict[
70+
_ClassName, _Holder[Union[EnumProperty, ModelProperty, RecursiveReferenceInterupt]]
71+
] = attr.ib(factory=dict)
6372
errors: List[ParseError] = attr.ib(factory=list)
6473

6574

6675
def update_schemas_with(
67-
*, ref_path: _ReferencePath, data: Union[oai.Reference, oai.Schema], schemas: Schemas, config: Config
76+
*,
77+
ref_path: _ReferencePath,
78+
data: Union[oai.Reference, oai.Schema],
79+
schemas: Schemas,
80+
visited: Set[_ReferencePath],
81+
config: Config,
6882
) -> Union[Schemas, PropertyError]:
6983
if isinstance(data, oai.Reference):
70-
return _update_schemas_with_reference(ref_path=ref_path, data=data, schemas=schemas, config=config)
84+
return _update_schemas_with_reference(
85+
ref_path=ref_path, data=data, schemas=schemas, visited=visited, config=config
86+
)
7187
else:
72-
return _update_schemas_with_data(ref_path=ref_path, data=data, schemas=schemas, config=config)
88+
return _update_schemas_with_data(ref_path=ref_path, data=data, schemas=schemas, visited=visited, config=config)
7389

7490

7591
def _update_schemas_with_reference(
76-
*, ref_path: _ReferencePath, data: oai.Reference, schemas: Schemas, config: Config
92+
*, ref_path: _ReferencePath, data: oai.Reference, schemas: Schemas, visited: Set[_ReferencePath], config: Config
7793
) -> Union[Schemas, PropertyError]:
7894
reference_pointer = parse_reference_path(data.ref)
7995
if isinstance(reference_pointer, ParseError):
@@ -87,7 +103,7 @@ def _update_schemas_with_reference(
87103

88104

89105
def _update_schemas_with_data(
90-
*, ref_path: _ReferencePath, data: oai.Schema, schemas: Schemas, config: Config
106+
*, ref_path: _ReferencePath, data: oai.Schema, schemas: Schemas, visited: Set[_ReferencePath], config: Config
91107
) -> Union[Schemas, PropertyError]:
92108
from . import build_enum_property, build_model_property
93109

@@ -100,7 +116,19 @@ def _update_schemas_with_data(
100116
prop, schemas = build_model_property(
101117
data=data, name=ref_path, schemas=schemas, required=True, parent_name=None, config=config
102118
)
119+
120+
holder = schemas.classes_by_reference.get(ref_path)
103121
if isinstance(prop, PropertyError):
122+
if ref_path in visited and not holder:
123+
holder = _Holder(data=RecursiveReferenceInterupt())
124+
schemas = attr.evolve(schemas, classes_by_reference={ref_path: holder, **schemas.classes_by_reference})
125+
return RecursiveReferenceInterupt(schemas=schemas)
104126
return prop
105-
schemas = attr.evolve(schemas, classes_by_reference={ref_path: prop, **schemas.classes_by_reference})
127+
128+
if holder:
129+
holder.data = prop
130+
else:
131+
schemas = attr.evolve(
132+
schemas, classes_by_reference={ref_path: _Holder(data=prop), **schemas.classes_by_reference}
133+
)
106134
return schemas

0 commit comments

Comments
 (0)
Please sign in to comment.