17
17
import math
18
18
import re
19
19
from copy import deepcopy
20
- from typing import Any , Dict , List , NoReturn , Optional , Tuple , Union
20
+ from typing import Any , Dict , List , NoReturn , Optional , Set , Tuple , Union
21
+ from urllib .parse import urljoin
21
22
22
23
import jsonschema
23
24
from hypothesis .errors import InvalidArgument
@@ -567,15 +568,14 @@ def canonicalish(
567
568
568
569
569
570
def resolve_all_refs (
570
- schema : Union [bool , Schema ], * , resolver : LocalResolver = None
571
+ schema : Union [bool , Schema ],
572
+ * ,
573
+ resolver : LocalResolver = None ,
574
+ seen_map : Dict [str , Set [str ]] = None ,
571
575
) -> Schema :
572
- """
573
- Resolve all references in the given schema.
574
-
575
- This handles nested definitions, but not recursive definitions.
576
- The latter require special handling to convert to strategies and are much
577
- less common, so we just ignore them (and error out) for now.
578
- """
576
+ """Resolve all non-recursive references in the given schema."""
577
+ if seen_map is None :
578
+ seen_map = {}
579
579
if isinstance (schema , bool ):
580
580
return canonicalish (schema )
581
581
assert isinstance (schema , dict ), schema
@@ -587,41 +587,49 @@ def resolve_all_refs(
587
587
)
588
588
589
589
def is_recursive (reference : str ) -> bool :
590
- return reference == "#" or reference in resolver ._scopes_stack # type: ignore
590
+ full_ref = urljoin (resolver .base_uri , reference ) # type: ignore
591
+ return reference == "#" or reference in resolver ._scopes_stack or full_ref in resolver ._scopes_stack # type: ignore
591
592
592
593
# To avoid infinite recursion, we skip all recursive definitions, and such references will be processed later
593
594
# A definition is recursive if it contains a reference to itself or one of its ancestors.
594
- if "$ref" in schema and not is_recursive (schema ["$ref" ]): # type: ignore
595
- s = dict (schema )
596
- ref = s .pop ("$ref" )
597
- with resolver .resolving (ref ) as got :
598
- if s == {}:
599
- return resolve_all_refs (got , resolver = resolver )
600
- m = merged ([s , got ], resolver = resolver )
601
- if m is None : # pragma: no cover
602
- msg = f"$ref:{ ref !r} had incompatible base schema { s !r} "
603
- raise HypothesisRefResolutionError (msg )
604
- return resolve_all_refs (m , resolver = resolver )
595
+ if "$ref" in schema :
596
+ path = "-" .join (resolver ._scopes_stack )
597
+ seen_paths = seen_map .setdefault (path , set ())
598
+ if schema ["$ref" ] not in seen_paths and not is_recursive (schema ["$ref" ]): # type: ignore
599
+ seen_paths .add (schema ["$ref" ]) # type: ignore
600
+ s = dict (schema )
601
+ ref = s .pop ("$ref" )
602
+ with resolver .resolving (ref ) as got :
603
+ if s == {}:
604
+ return resolve_all_refs (got , resolver = resolver , seen_map = seen_map )
605
+ m = merged ([s , got ])
606
+ if m is None : # pragma: no cover
607
+ msg = f"$ref:{ ref !r} had incompatible base schema { s !r} "
608
+ raise HypothesisRefResolutionError (msg )
609
+
610
+ return resolve_all_refs (m , resolver = resolver , seen_map = seen_map )
605
611
606
612
for key in SCHEMA_KEYS :
607
613
val = schema .get (key , False )
608
614
if isinstance (val , list ):
609
615
schema [key ] = [
610
- resolve_all_refs (deepcopy (v ), resolver = resolver )
616
+ resolve_all_refs (deepcopy (v ), resolver = resolver , seen_map = seen_map )
611
617
if isinstance (v , dict )
612
618
else v
613
619
for v in val
614
620
]
615
621
elif isinstance (val , dict ):
616
- schema [key ] = resolve_all_refs (deepcopy (val ), resolver = resolver )
622
+ schema [key ] = resolve_all_refs (
623
+ deepcopy (val ), resolver = resolver , seen_map = seen_map
624
+ )
617
625
else :
618
626
assert isinstance (val , bool )
619
627
for key in SCHEMA_OBJECT_KEYS : # values are keys-to-schema-dicts, not schemas
620
628
if key in schema :
621
629
subschema = schema [key ]
622
630
assert isinstance (subschema , dict )
623
631
schema [key ] = {
624
- k : resolve_all_refs (deepcopy (v ), resolver = resolver )
632
+ k : resolve_all_refs (deepcopy (v ), resolver = resolver , seen_map = seen_map )
625
633
if isinstance (v , dict )
626
634
else v
627
635
for k , v in subschema .items ()
0 commit comments