Skip to content

Commit 5cd2a18

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

File tree

1 file changed

+140
-23
lines changed

1 file changed

+140
-23
lines changed

openapi_python_client/parser/properties/__init__.py

Lines changed: 140 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,41 @@ 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+
owner_class_name = Reference.from_ref(f"#{parent_name}").class_name
577+
reference_name = _reference_name(data)
578+
if lookup_is_reference_to_itself(reference_name, owner_class_name, lazy_references):
527579
return cast(Property, LazyReferencePropertyProxy.create(name, required, data, parent_name)), schemas
528580
else:
529581
return PropertyError(data=data, detail="Could not find reference in parsed models or enums."), schemas
530582

531583
if data.enum:
532584
return build_enum_property(
533-
data=data, name=name, required=required, schemas=schemas, enum=data.enum, parent_name=parent_name
585+
data=data,
586+
name=name,
587+
required=required,
588+
schemas=schemas,
589+
enum=data.enum,
590+
parent_name=parent_name,
534591
)
535592
if data.anyOf or data.oneOf:
536-
return build_union_property(data=data, name=name, required=required, schemas=schemas, parent_name=parent_name)
593+
return build_union_property(
594+
data=data,
595+
name=name,
596+
required=required,
597+
schemas=schemas,
598+
parent_name=parent_name,
599+
lazy_references=lazy_references,
600+
)
537601
if not data.type:
538602
return NoneProperty(name=name, required=required, nullable=False, default=None), schemas
539603

@@ -570,9 +634,23 @@ def _property_from_data(
570634
schemas,
571635
)
572636
elif data.type == "array":
573-
return build_list_property(data=data, name=name, required=required, schemas=schemas, parent_name=parent_name)
637+
return build_list_property(
638+
data=data,
639+
name=name,
640+
required=required,
641+
schemas=schemas,
642+
parent_name=parent_name,
643+
lazy_references=lazy_references,
644+
)
574645
elif data.type == "object":
575-
return build_model_property(data=data, name=name, schemas=schemas, required=required, parent_name=parent_name)
646+
return build_model_property(
647+
data=data,
648+
name=name,
649+
schemas=schemas,
650+
required=required,
651+
parent_name=parent_name,
652+
lazy_references=lazy_references,
653+
)
576654
return PropertyError(data=data, detail=f"unknown type {data.type}"), schemas
577655

578656

@@ -583,21 +661,41 @@ def property_from_data(
583661
data: Union[oai.Reference, oai.Schema],
584662
schemas: Schemas,
585663
parent_name: str,
664+
lazy_references: Optional[Dict[str, oai.Reference]] = None,
586665
) -> Tuple[Union[Property, PropertyError], Schemas]:
666+
if lazy_references is None:
667+
lazy_references = dict()
668+
587669
try:
588-
return _property_from_data(name=name, required=required, data=data, schemas=schemas, parent_name=parent_name)
670+
return _property_from_data(
671+
name=name,
672+
required=required,
673+
data=data,
674+
schemas=schemas,
675+
parent_name=parent_name,
676+
lazy_references=lazy_references,
677+
)
589678
except ValidationError:
590679
return PropertyError(detail="Failed to validate default value", data=data), schemas
591680

592681

593-
def update_schemas_with_data(name: str, data: oai.Schema, schemas: Schemas) -> Union[Schemas, PropertyError]:
682+
def update_schemas_with_data(
683+
name: str, data: oai.Schema, schemas: Schemas, lazy_references: Dict[str, oai.Reference]
684+
) -> Union[Schemas, PropertyError]:
594685
prop: Union[PropertyError, ModelProperty, EnumProperty]
595686
if data.enum is not None:
596687
prop, schemas = build_enum_property(
597-
data=data, name=name, required=True, schemas=schemas, enum=data.enum, parent_name=None
688+
data=data,
689+
name=name,
690+
required=True,
691+
schemas=schemas,
692+
enum=data.enum,
693+
parent_name=None,
598694
)
599695
else:
600-
prop, schemas = build_model_property(data=data, name=name, schemas=schemas, required=True, parent_name=None)
696+
prop, schemas = build_model_property(
697+
data=data, name=name, schemas=schemas, required=True, parent_name=None, lazy_references=lazy_references
698+
)
601699

602700
if isinstance(prop, PropertyError):
603701
return prop
@@ -680,23 +778,31 @@ def build_schemas(*, components: Dict[str, Union[oai.Reference, oai.Schema]]) ->
680778
to_process: Iterable[Tuple[str, Union[oai.Reference, oai.Schema]]] = components.items()
681779
processing = True
682780
errors: List[PropertyError] = []
683-
references_by_name: Dict[str, oai.Reference] = dict()
684-
references_to_process: List[Tuple[str, oai.Reference]] = list()
685781
LazyReferencePropertyProxy.flush_internal_references() # Cleanup side effects
782+
lazy_self_references: Dict[str, oai.Reference] = dict()
783+
visited: List[str] = []
686784

687785
# References could have forward References so keep going as long as we are making progress
688786
while processing:
787+
references_by_name: Dict[str, oai.Reference] = dict()
788+
references_to_process: List[Tuple[str, oai.Reference]] = list()
689789
processing = False
690790
errors = []
691791
next_round = []
792+
692793
# Only accumulate errors from the last round, since we might fix some along the way
693794
for name, data in to_process:
795+
visited.append(name)
796+
694797
if isinstance(data, oai.Reference):
695-
references_by_name[name] = data
696-
references_to_process.append((name, data))
798+
class_name = _reference_model_name(data)
799+
800+
if not schemas.models.get(class_name) and not schemas.enums.get(class_name):
801+
references_by_name[name] = data
802+
references_to_process.append((name, data))
697803
continue
698804

699-
schemas_or_err = update_schemas_with_data(name, data, schemas)
805+
schemas_or_err = update_schemas_with_data(name, data, schemas, lazy_self_references)
700806

701807
if isinstance(schemas_or_err, PropertyError):
702808
next_round.append((name, data))
@@ -711,7 +817,18 @@ def build_schemas(*, components: Dict[str, Union[oai.Reference, oai.Schema]]) ->
711817
schemas_or_err = resolve_reference_and_update_schemas(name, reference, schemas, references_by_name)
712818

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

716833
schemas.errors.extend(errors)
717834
LazyReferencePropertyProxy.update_schemas(schemas)

0 commit comments

Comments
 (0)