Skip to content

Commit 737d88d

Browse files
committed
parser / propertie / add support for indirect reference to itself resolution
1 parent 78fc5b7 commit 737d88d

File tree

1 file changed

+139
-23
lines changed

1 file changed

+139
-23
lines changed

openapi_python_client/parser/properties/__init__.py

Lines changed: 139 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,13 @@ def __deepcopy__(self, memo: Any) -> Property:
7272
return copy.deepcopy(resolved, memo)
7373

7474
def __getattr__(self, name: str) -> Any:
75-
resolved = self.resolve(False)
76-
return resolved.__getattribute__(name)
75+
if name == "nullable":
76+
return not self._required
77+
elif name == "required":
78+
return self._required
79+
else:
80+
resolved = self.resolve(False)
81+
return resolved.__getattribute__(name)
7782

7883
def resolve(self, allow_lazyness: bool = True) -> Union[Property, None]:
7984
if not self._resolved:
@@ -312,7 +317,13 @@ def _string_based_property(
312317

313318

314319
def build_model_property(
315-
*, data: oai.Schema, name: str, schemas: Schemas, required: bool, parent_name: Optional[str]
320+
*,
321+
data: oai.Schema,
322+
name: str,
323+
schemas: Schemas,
324+
required: bool,
325+
parent_name: Optional[str],
326+
lazy_references: Dict[str, oai.Reference],
316327
) -> Tuple[Union[ModelProperty, PropertyError], Schemas]:
317328
"""
318329
A single ModelProperty from its OAI data
@@ -336,7 +347,12 @@ def build_model_property(
336347
for key, value in (data.properties or {}).items():
337348
prop_required = key in required_set
338349
prop, schemas = property_from_data(
339-
name=key, required=prop_required, data=value, schemas=schemas, parent_name=class_name
350+
name=key,
351+
required=prop_required,
352+
data=value,
353+
schemas=schemas,
354+
parent_name=class_name,
355+
lazy_references=lazy_references,
340356
)
341357
if isinstance(prop, PropertyError):
342358
return prop, schemas
@@ -362,6 +378,7 @@ def build_model_property(
362378
data=data.additionalProperties,
363379
schemas=schemas,
364380
parent_name=class_name,
381+
lazy_references=lazy_references,
365382
)
366383
if isinstance(additional_properties, PropertyError):
367384
return additional_properties, schemas
@@ -456,12 +473,23 @@ def build_enum_property(
456473

457474

458475
def build_union_property(
459-
*, data: oai.Schema, name: str, required: bool, schemas: Schemas, parent_name: str
476+
*,
477+
data: oai.Schema,
478+
name: str,
479+
required: bool,
480+
schemas: Schemas,
481+
parent_name: str,
482+
lazy_references: Dict[str, oai.Reference],
460483
) -> Tuple[Union[UnionProperty, PropertyError], Schemas]:
461484
sub_properties: List[Property] = []
462485
for sub_prop_data in chain(data.anyOf, data.oneOf):
463486
sub_prop, schemas = property_from_data(
464-
name=name, required=required, data=sub_prop_data, schemas=schemas, parent_name=parent_name
487+
name=name,
488+
required=required,
489+
data=sub_prop_data,
490+
schemas=schemas,
491+
parent_name=parent_name,
492+
lazy_references=lazy_references,
465493
)
466494
if isinstance(sub_prop, PropertyError):
467495
return PropertyError(detail=f"Invalid property in union {name}", data=sub_prop_data), schemas
@@ -481,12 +509,23 @@ def build_union_property(
481509

482510

483511
def build_list_property(
484-
*, data: oai.Schema, name: str, required: bool, schemas: Schemas, parent_name: str
512+
*,
513+
data: oai.Schema,
514+
name: str,
515+
required: bool,
516+
schemas: Schemas,
517+
parent_name: str,
518+
lazy_references: Dict[str, oai.Reference],
485519
) -> Tuple[Union[ListProperty[Any], PropertyError], Schemas]:
486520
if data.items is None:
487521
return PropertyError(data=data, detail="type array must have items defined"), schemas
488522
inner_prop, schemas = property_from_data(
489-
name=f"{name}_item", required=True, data=data.items, schemas=schemas, parent_name=parent_name
523+
name=f"{name}_item",
524+
required=True,
525+
data=data.items,
526+
schemas=schemas,
527+
parent_name=parent_name,
528+
lazy_references=lazy_references,
490529
)
491530
if isinstance(inner_prop, PropertyError):
492531
return PropertyError(data=inner_prop.data, detail=f"invalid data in items of array {name}"), schemas
@@ -508,6 +547,7 @@ def _property_from_data(
508547
data: Union[oai.Reference, oai.Schema],
509548
schemas: Schemas,
510549
parent_name: str,
550+
lazy_references: Dict[str, oai.Reference],
511551
) -> Tuple[Union[Property, PropertyError], Schemas]:
512552
""" Generate a Property from the OpenAPI dictionary representation of it """
513553
name = utils.remove_string_escapes(name)
@@ -523,17 +563,40 @@ def _property_from_data(
523563
schemas,
524564
)
525565
else:
526-
if Reference.from_ref(f"#{parent_name}").class_name == reference.class_name:
566+
567+
def lookup_is_reference_to_itself(
568+
ref_name: str, owner_class_name: str, lazy_references: Dict[str, oai.Reference]
569+
) -> bool:
570+
if ref_name in lazy_references:
571+
next_ref_name = _reference_name(lazy_references[ref_name])
572+
return lookup_is_reference_to_itself(next_ref_name, owner_class_name, lazy_references)
573+
574+
return ref_name.casefold() == owner_class_name.casefold()
575+
576+
reference_name = _reference_name(data)
577+
if lookup_is_reference_to_itself(reference_name, parent_name, lazy_references):
527578
return cast(Property, LazyReferencePropertyProxy.create(name, required, data, parent_name)), schemas
528579
else:
529580
return PropertyError(data=data, detail="Could not find reference in parsed models or enums."), schemas
530581

531582
if data.enum:
532583
return build_enum_property(
533-
data=data, name=name, required=required, schemas=schemas, enum=data.enum, parent_name=parent_name
584+
data=data,
585+
name=name,
586+
required=required,
587+
schemas=schemas,
588+
enum=data.enum,
589+
parent_name=parent_name,
534590
)
535591
if data.anyOf or data.oneOf:
536-
return build_union_property(data=data, name=name, required=required, schemas=schemas, parent_name=parent_name)
592+
return build_union_property(
593+
data=data,
594+
name=name,
595+
required=required,
596+
schemas=schemas,
597+
parent_name=parent_name,
598+
lazy_references=lazy_references,
599+
)
537600
if not data.type:
538601
return NoneProperty(name=name, required=required, nullable=False, default=None), schemas
539602

@@ -570,9 +633,23 @@ def _property_from_data(
570633
schemas,
571634
)
572635
elif data.type == "array":
573-
return build_list_property(data=data, name=name, required=required, schemas=schemas, parent_name=parent_name)
636+
return build_list_property(
637+
data=data,
638+
name=name,
639+
required=required,
640+
schemas=schemas,
641+
parent_name=parent_name,
642+
lazy_references=lazy_references,
643+
)
574644
elif data.type == "object":
575-
return build_model_property(data=data, name=name, schemas=schemas, required=required, parent_name=parent_name)
645+
return build_model_property(
646+
data=data,
647+
name=name,
648+
schemas=schemas,
649+
required=required,
650+
parent_name=parent_name,
651+
lazy_references=lazy_references,
652+
)
576653
return PropertyError(data=data, detail=f"unknown type {data.type}"), schemas
577654

578655

@@ -583,21 +660,41 @@ def property_from_data(
583660
data: Union[oai.Reference, oai.Schema],
584661
schemas: Schemas,
585662
parent_name: str,
663+
lazy_references: Optional[Dict[str, oai.Reference]] = None,
586664
) -> Tuple[Union[Property, PropertyError], Schemas]:
665+
if lazy_references is None:
666+
lazy_references = dict()
667+
587668
try:
588-
return _property_from_data(name=name, required=required, data=data, schemas=schemas, parent_name=parent_name)
669+
return _property_from_data(
670+
name=name,
671+
required=required,
672+
data=data,
673+
schemas=schemas,
674+
parent_name=parent_name,
675+
lazy_references=lazy_references,
676+
)
589677
except ValidationError:
590678
return PropertyError(detail="Failed to validate default value", data=data), schemas
591679

592680

593-
def update_schemas_with_data(name: str, data: oai.Schema, schemas: Schemas) -> Union[Schemas, PropertyError]:
681+
def update_schemas_with_data(
682+
name: str, data: oai.Schema, schemas: Schemas, lazy_references: Dict[str, oai.Reference]
683+
) -> Union[Schemas, PropertyError]:
594684
prop: Union[PropertyError, ModelProperty, EnumProperty]
595685
if data.enum is not None:
596686
prop, schemas = build_enum_property(
597-
data=data, name=name, required=True, schemas=schemas, enum=data.enum, parent_name=None
687+
data=data,
688+
name=name,
689+
required=True,
690+
schemas=schemas,
691+
enum=data.enum,
692+
parent_name=None,
598693
)
599694
else:
600-
prop, schemas = build_model_property(data=data, name=name, schemas=schemas, required=True, parent_name=None)
695+
prop, schemas = build_model_property(
696+
data=data, name=name, schemas=schemas, required=True, parent_name=None, lazy_references=lazy_references
697+
)
601698

602699
if isinstance(prop, PropertyError):
603700
return prop
@@ -680,23 +777,31 @@ def build_schemas(*, components: Dict[str, Union[oai.Reference, oai.Schema]]) ->
680777
to_process: Iterable[Tuple[str, Union[oai.Reference, oai.Schema]]] = components.items()
681778
processing = True
682779
errors: List[PropertyError] = []
683-
references_by_name: Dict[str, oai.Reference] = dict()
684-
references_to_process: List[Tuple[str, oai.Reference]] = list()
685780
LazyReferencePropertyProxy.flush_internal_references() # Cleanup side effects
781+
lazy_self_references: Dict[str, oai.Reference] = dict()
782+
visited: List[str] = []
686783

687784
# References could have forward References so keep going as long as we are making progress
688785
while processing:
786+
references_by_name: Dict[str, oai.Reference] = dict()
787+
references_to_process: List[Tuple[str, oai.Reference]] = list()
689788
processing = False
690789
errors = []
691790
next_round = []
791+
692792
# Only accumulate errors from the last round, since we might fix some along the way
693793
for name, data in to_process:
794+
visited.append(name)
795+
694796
if isinstance(data, oai.Reference):
695-
references_by_name[name] = data
696-
references_to_process.append((name, data))
797+
class_name = _reference_model_name(data)
798+
799+
if not schemas.models.get(class_name) and not schemas.enums.get(class_name):
800+
references_by_name[name] = data
801+
references_to_process.append((name, data))
697802
continue
698803

699-
schemas_or_err = update_schemas_with_data(name, data, schemas)
804+
schemas_or_err = update_schemas_with_data(name, data, schemas, lazy_self_references)
700805

701806
if isinstance(schemas_or_err, PropertyError):
702807
next_round.append((name, data))
@@ -711,7 +816,18 @@ def build_schemas(*, components: Dict[str, Union[oai.Reference, oai.Schema]]) ->
711816
schemas_or_err = resolve_reference_and_update_schemas(name, reference, schemas, references_by_name)
712817

713818
if isinstance(schemas_or_err, PropertyError):
714-
errors.append(schemas_or_err)
819+
if _reference_name(reference) in visited:
820+
# It's a reference to an already visited Enum|Model; not yet resolved
821+
# It's an indirect reference toward this Enum|Model;
822+
# It will be lazy proxified and resolved later on
823+
lazy_self_references[name] = reference
824+
else:
825+
errors.append(schemas_or_err)
826+
827+
for name in lazy_self_references.keys():
828+
schemas_or_err = resolve_reference_and_update_schemas(
829+
name, lazy_self_references[name], schemas, references_by_name
830+
)
715831

716832
schemas.errors.extend(errors)
717833
LazyReferencePropertyProxy.update_schemas(schemas)

0 commit comments

Comments
 (0)