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"""
@@ -411,11 +464,18 @@ def _property_from_ref(
411464 ref_path = parse_reference_path (data .ref )
412465 if isinstance (ref_path , ParseError ):
413466 return PropertyError (data = data , detail = ref_path .detail ), schemas
467+
414468 existing = schemas .classes_by_reference .get (ref_path )
415- if not existing :
469+ if not existing or not existing . data :
416470 return PropertyError (data = data , detail = "Could not find reference in parsed models or enums" ), schemas
417471
418- prop = attr .evolve (existing , required = required , name = name )
472+ if isinstance (existing .data , RecursiveReferenceInterupt ):
473+ return (
474+ LazySelfReferenceProperty (required = required , name = name , nullable = False , default = None , owner = existing ),
475+ schemas ,
476+ )
477+
478+ prop = attr .evolve (existing .data , required = required , name = name )
419479 if parent :
420480 prop = attr .evolve (prop , nullable = parent .nullable )
421481 if isinstance (prop , EnumProperty ):
@@ -551,28 +611,44 @@ def build_schemas(
551611 to_process : Iterable [Tuple [str , Union [oai .Reference , oai .Schema ]]] = components .items ()
552612 still_making_progress = True
553613 errors : List [PropertyError ] = []
554-
614+ recursive_references_waiting_reprocess : Dict [str , Union [oai .Reference , oai .Schema ]] = dict ()
615+ visited : Set [_ReferencePath ] = set ()
616+ depth = 0
555617 # References could have forward References so keep going as long as we are making progress
556618 while still_making_progress :
557619 still_making_progress = False
558620 errors = []
559621 next_round = []
622+
560623 # Only accumulate errors from the last round, since we might fix some along the way
561624 for name , data in to_process :
562625 ref_path = parse_reference_path (f"#/components/schemas/{ name } " )
563626 if isinstance (ref_path , ParseError ):
564627 schemas .errors .append (PropertyError (detail = ref_path .detail , data = data ))
565628 continue
566629
567- schemas_or_err = update_schemas_with (ref_path = ref_path , data = data , schemas = schemas , config = config )
630+ visited .add (ref_path )
631+ schemas_or_err = update_schemas_with (
632+ ref_path = ref_path , data = data , schemas = schemas , visited = visited , config = config
633+ )
568634 if isinstance (schemas_or_err , PropertyError ):
569- next_round .append ((name , data ))
570- errors .append (schemas_or_err )
571- continue
635+ if isinstance (schemas_or_err , RecursiveReferenceInterupt ):
636+ up_schemas = schemas_or_err .schemas
637+ assert isinstance (up_schemas , Schemas ) # TODO fix typedef in RecursiveReferenceInterupt
638+ schemas_or_err = up_schemas
639+ recursive_references_waiting_reprocess [name ] = data
640+ else :
641+ next_round .append ((name , data ))
642+ errors .append (schemas_or_err )
643+ continue
572644
573645 schemas = schemas_or_err
574646 still_making_progress = True
647+ depth += 1
575648 to_process = next_round
576649
650+ if len (recursive_references_waiting_reprocess .keys ()):
651+ schemas = build_schemas (components = recursive_references_waiting_reprocess , schemas = schemas , config = config )
652+
577653 schemas .errors .extend (errors )
578654 return schemas
0 commit comments