Skip to content

Commit 2f1dbeb

Browse files
committed
Do not inline recursive references at all
1 parent 6323e9a commit 2f1dbeb

File tree

3 files changed

+48
-33
lines changed

3 files changed

+48
-33
lines changed

src/hypothesis_jsonschema/_canonicalise.py

+40-17
Original file line numberDiff line numberDiff line change
@@ -577,10 +577,13 @@ def is_recursive_reference(reference: str, resolver: LocalResolver) -> bool:
577577

578578
def resolve_all_refs(
579579
schema: Union[bool, Schema], *, resolver: LocalResolver = None
580-
) -> Schema:
581-
"""Resolve all non-recursive references in the given schema."""
580+
) -> Tuple[Schema, bool]:
581+
"""Resolve all non-recursive references in the given schema.
582+
583+
When a recursive reference is detected, it stops traversing the currently resolving branch and leaves it as is.
584+
"""
582585
if isinstance(schema, bool):
583-
return canonicalish(schema)
586+
return canonicalish(schema), False
584587
assert isinstance(schema, dict), schema
585588
if resolver is None:
586589
resolver = LocalResolver.from_schema(deepcopy(schema))
@@ -603,32 +606,52 @@ def resolve_all_refs(
603606
raise HypothesisRefResolutionError(msg)
604607
# `deepcopy` is not needed, because, the schemas are copied inside the `merged` call above
605608
return resolve_all_refs(m, resolver=resolver)
609+
else:
610+
return schema, True
606611

607612
for key in SCHEMA_KEYS:
608613
val = schema.get(key, False)
609614
if isinstance(val, list):
610-
schema[key] = [
611-
resolve_all_refs(deepcopy(v), resolver=resolver)
612-
if isinstance(v, dict)
613-
else v
614-
for v in val
615-
]
615+
value = []
616+
for v in val:
617+
if isinstance(v, dict):
618+
resolved, is_recursive = resolve_all_refs(
619+
deepcopy(v), resolver=resolver
620+
)
621+
if is_recursive:
622+
return schema, True
623+
else:
624+
value.append(resolved)
625+
else:
626+
value.append(v)
627+
schema[key] = value
616628
elif isinstance(val, dict):
617-
schema[key] = resolve_all_refs(deepcopy(val), resolver=resolver)
629+
resolved, is_recursive = resolve_all_refs(deepcopy(val), resolver=resolver)
630+
if is_recursive:
631+
return schema, True
632+
else:
633+
schema[key] = resolved
618634
else:
619635
assert isinstance(val, bool)
620636
for key in SCHEMA_OBJECT_KEYS: # values are keys-to-schema-dicts, not schemas
621637
if key in schema:
622638
subschema = schema[key]
623639
assert isinstance(subschema, dict)
624-
schema[key] = {
625-
k: resolve_all_refs(deepcopy(v), resolver=resolver)
626-
if isinstance(v, dict)
627-
else v
628-
for k, v in subschema.items()
629-
}
640+
value = {}
641+
for k, v in subschema.items():
642+
if isinstance(v, dict):
643+
resolved, is_recursive = resolve_all_refs(
644+
deepcopy(v), resolver=resolver
645+
)
646+
if is_recursive:
647+
return schema, True
648+
else:
649+
value[k] = resolved
650+
else:
651+
value[k] = v
652+
schema[key] = value
630653
assert isinstance(schema, dict)
631-
return schema
654+
return schema, False
632655

633656

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

src/hypothesis_jsonschema/_from_schema.py

+1-1
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

+7-15
Original file line numberDiff line numberDiff line change
@@ -542,12 +542,12 @@ def test_validators_use_proper_draft():
542542
(ROOT_REFERENCE, ROOT_REFERENCE),
543543
(NESTED, NESTED),
544544
(NESTED_WITH_ID, NESTED_WITH_ID),
545-
# "foo" content should be inlined as is, because "#" is recursive (special case)
545+
# "foo" content should not be inlined, because "#" is recursive (special case)
546546
(
547547
{"foo": {"$ref": "#"}, "not": {"$ref": "#foo"}},
548-
{"foo": {"$ref": "#"}, "not": {"$ref": "#"}},
548+
{"foo": {"$ref": "#"}, "not": {"$ref": "#foo"}},
549549
),
550-
# "foo" content should be inlined as is, because it points to itself
550+
# "foo" content should not be inlined, because it points to itself
551551
(
552552
SELF_REFERENTIAL,
553553
SELF_REFERENTIAL,
@@ -558,22 +558,21 @@ def test_validators_use_proper_draft():
558558
# 1. We start from resolving "$ref" in "not"
559559
# 2. at this point we don't know this path is recursive, so we follow to "foo"
560560
# 3. inside "foo" we found a reference to "foo", which means it is recursive
561-
{"foo": {"not": {"$ref": "#foo"}}, "not": {"not": {"$ref": "#foo"}}},
561+
{"foo": {"not": {"$ref": "#foo"}}, "not": {"$ref": "#foo"}},
562562
),
563563
# Circular reference between two schemas
564564
(
565565
{"foo": {"$ref": "#bar"}, "bar": {"$ref": "#foo"}, "not": {"$ref": "#foo"}},
566566
# 1. We start in "not" and follow to "foo"
567567
# 2. In "foo" we follow to "bar"
568568
# 3. Here we see a reference to previously seen scope, which means it is a recursive path
569-
# We take the schema where we stop and inline it to the starting point (therefore it is `{"$ref": "#foo"}`)
570569
{"foo": {"$ref": "#bar"}, "bar": {"$ref": "#foo"}, "not": {"$ref": "#foo"}},
571570
),
572571
),
573572
)
574573
def test_skip_recursive_references_simple_schemas(schema, expected):
575574
# When there is a recursive reference, it should not be resolved
576-
assert resolve_all_refs(schema) == expected
575+
assert resolve_all_refs(schema)[0] == expected
577576

578577

579578
@pytest.mark.parametrize(
@@ -637,18 +636,11 @@ def test_skip_recursive_references_simple_schemas(schema, expected):
637636
"properties": {"foo": {"$ref": "#/definitions/foo"}},
638637
},
639638
{
640-
"properties": {
641-
"foo": {
642-
"properties": {
643-
"bar": {"$ref": "#/definitions/foo"},
644-
"baz": {"$ref": "#/definitions/foo"},
645-
}
646-
}
647-
},
639+
"properties": {"foo": {"$ref": "#/definitions/foo"}},
648640
},
649641
),
650642
),
651643
)
652644
def test_skip_recursive_references_complex_schemas(schema, resolved):
653645
resolved["definitions"] = schema["definitions"]
654-
assert resolve_all_refs(schema) == resolved
646+
assert resolve_all_refs(schema)[0] == resolved

0 commit comments

Comments
 (0)