Skip to content

Commit 4dc6e4c

Browse files
committed
Do not inline recursive references at all
1 parent a05f0aa commit 4dc6e4c

File tree

3 files changed

+48
-33
lines changed

3 files changed

+48
-33
lines changed

src/hypothesis_jsonschema/_canonicalise.py

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -594,10 +594,13 @@ def is_recursive_reference(reference: str, resolver: LocalResolver) -> bool:
594594

595595
def resolve_all_refs(
596596
schema: Union[bool, Schema], *, resolver: LocalResolver = None
597-
) -> Schema:
598-
"""Resolve all non-recursive references in the given schema."""
597+
) -> Tuple[Schema, bool]:
598+
"""Resolve all non-recursive references in the given schema.
599+
600+
When a recursive reference is detected, it stops traversing the currently resolving branch and leaves it as is.
601+
"""
599602
if isinstance(schema, bool):
600-
return canonicalish(schema)
603+
return canonicalish(schema), False
601604
assert isinstance(schema, dict), schema
602605
if resolver is None:
603606
resolver = LocalResolver.from_schema(deepcopy(schema))
@@ -620,32 +623,52 @@ def resolve_all_refs(
620623
raise HypothesisRefResolutionError(msg)
621624
# `deepcopy` is not needed, because, the schemas are copied inside the `merged` call above
622625
return resolve_all_refs(m, resolver=resolver)
626+
else:
627+
return schema, True
623628

624629
for key in SCHEMA_KEYS:
625630
val = schema.get(key, False)
626631
if isinstance(val, list):
627-
schema[key] = [
628-
resolve_all_refs(deepcopy(v), resolver=resolver)
629-
if isinstance(v, dict)
630-
else v
631-
for v in val
632-
]
632+
value = []
633+
for v in val:
634+
if isinstance(v, dict):
635+
resolved, is_recursive = resolve_all_refs(
636+
deepcopy(v), resolver=resolver
637+
)
638+
if is_recursive:
639+
return schema, True
640+
else:
641+
value.append(resolved)
642+
else:
643+
value.append(v)
644+
schema[key] = value
633645
elif isinstance(val, dict):
634-
schema[key] = resolve_all_refs(deepcopy(val), resolver=resolver)
646+
resolved, is_recursive = resolve_all_refs(deepcopy(val), resolver=resolver)
647+
if is_recursive:
648+
return schema, True
649+
else:
650+
schema[key] = resolved
635651
else:
636652
assert isinstance(val, bool)
637653
for key in SCHEMA_OBJECT_KEYS: # values are keys-to-schema-dicts, not schemas
638654
if key in schema:
639655
subschema = schema[key]
640656
assert isinstance(subschema, dict)
641-
schema[key] = {
642-
k: resolve_all_refs(deepcopy(v), resolver=resolver)
643-
if isinstance(v, dict)
644-
else v
645-
for k, v in subschema.items()
646-
}
657+
value = {}
658+
for k, v in subschema.items():
659+
if isinstance(v, dict):
660+
resolved, is_recursive = resolve_all_refs(
661+
deepcopy(v), resolver=resolver
662+
)
663+
if is_recursive:
664+
return schema, True
665+
else:
666+
value[k] = resolved
667+
else:
668+
value[k] = v
669+
schema[key] = value
647670
assert isinstance(schema, dict)
648-
return schema
671+
return schema, False
649672

650673

651674
def merged(schemas: List[Any]) -> Optional[Schema]:

src/hypothesis_jsonschema/_from_schema.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ def __from_schema(
114114
custom_formats: Dict[str, st.SearchStrategy[str]] = None,
115115
) -> st.SearchStrategy[JSONType]:
116116
try:
117-
schema = resolve_all_refs(schema)
117+
schema, _ = resolve_all_refs(schema)
118118
except RecursionError:
119119
raise HypothesisRefResolutionError(
120120
f"Could not resolve recursive references in schema={schema!r}"

tests/test_canonicalise.py

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -578,12 +578,12 @@ def test_validators_use_proper_draft():
578578
(ROOT_REFERENCE, ROOT_REFERENCE),
579579
(NESTED, NESTED),
580580
(NESTED_WITH_ID, NESTED_WITH_ID),
581-
# "foo" content should be inlined as is, because "#" is recursive (special case)
581+
# "foo" content should not be inlined, because "#" is recursive (special case)
582582
(
583583
{"foo": {"$ref": "#"}, "not": {"$ref": "#foo"}},
584-
{"foo": {"$ref": "#"}, "not": {"$ref": "#"}},
584+
{"foo": {"$ref": "#"}, "not": {"$ref": "#foo"}},
585585
),
586-
# "foo" content should be inlined as is, because it points to itself
586+
# "foo" content should not be inlined, because it points to itself
587587
(
588588
SELF_REFERENTIAL,
589589
SELF_REFERENTIAL,
@@ -594,22 +594,21 @@ def test_validators_use_proper_draft():
594594
# 1. We start from resolving "$ref" in "not"
595595
# 2. at this point we don't know this path is recursive, so we follow to "foo"
596596
# 3. inside "foo" we found a reference to "foo", which means it is recursive
597-
{"foo": {"not": {"$ref": "#foo"}}, "not": {"not": {"$ref": "#foo"}}},
597+
{"foo": {"not": {"$ref": "#foo"}}, "not": {"$ref": "#foo"}},
598598
),
599599
# Circular reference between two schemas
600600
(
601601
{"foo": {"$ref": "#bar"}, "bar": {"$ref": "#foo"}, "not": {"$ref": "#foo"}},
602602
# 1. We start in "not" and follow to "foo"
603603
# 2. In "foo" we follow to "bar"
604604
# 3. Here we see a reference to previously seen scope, which means it is a recursive path
605-
# We take the schema where we stop and inline it to the starting point (therefore it is `{"$ref": "#foo"}`)
606605
{"foo": {"$ref": "#bar"}, "bar": {"$ref": "#foo"}, "not": {"$ref": "#foo"}},
607606
),
608607
),
609608
)
610609
def test_skip_recursive_references_simple_schemas(schema, expected):
611610
# When there is a recursive reference, it should not be resolved
612-
assert resolve_all_refs(schema) == expected
611+
assert resolve_all_refs(schema)[0] == expected
613612

614613

615614
@pytest.mark.parametrize(
@@ -673,18 +672,11 @@ def test_skip_recursive_references_simple_schemas(schema, expected):
673672
"properties": {"foo": {"$ref": "#/definitions/foo"}},
674673
},
675674
{
676-
"properties": {
677-
"foo": {
678-
"properties": {
679-
"bar": {"$ref": "#/definitions/foo"},
680-
"baz": {"$ref": "#/definitions/foo"},
681-
}
682-
}
683-
},
675+
"properties": {"foo": {"$ref": "#/definitions/foo"}},
684676
},
685677
),
686678
),
687679
)
688680
def test_skip_recursive_references_complex_schemas(schema, resolved):
689681
resolved["definitions"] = schema["definitions"]
690-
assert resolve_all_refs(schema) == resolved
682+
assert resolve_all_refs(schema)[0] == resolved

0 commit comments

Comments
 (0)