1717from ... import Config
1818from ... import schema as oai
1919from ... import utils
20- from ..errors import ParseError , PropertyError , ValidationError
20+ from ..errors import ParseError , PropertyError , RecursiveReferenceInterupt , ValidationError
2121from .converter import convert , convert_chain
2222from .enum_property import EnumProperty
2323from .model_property import ModelProperty , build_model_property
2424from .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 )
3891class 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
0 commit comments