Skip to content

Commit 12c791e

Browse files
committed
Extend dynamicRef keyword
1 parent eed6d8b commit 12c791e

File tree

4 files changed

+98
-34
lines changed

4 files changed

+98
-34
lines changed

jsonschema/_utils.py

+82
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections import deque
12
from collections.abc import Mapping, MutableMapping, Sequence
23
from urllib.parse import urlsplit
34
import itertools
@@ -346,3 +347,84 @@ def find_evaluated_property_keys_by_schema(validator, instance, schema):
346347
)
347348

348349
return evaluated_keys
350+
351+
352+
def _schema_is_referenced(schema, parent_schema):
353+
"""
354+
Checks if a schema is referenced by another schema
355+
"""
356+
return ("$id" in schema and "$ref" in parent_schema
357+
and parent_schema["$ref"] == schema["$id"])
358+
359+
360+
def _find_dynamic_anchor_extender(validator, scopes, fragment, schema):
361+
"""
362+
Find a schema that extends the dynamic anchor
363+
"""
364+
for url in scopes:
365+
with validator.resolver.resolving(url) as parent_schema:
366+
if _schema_is_referenced(schema, parent_schema):
367+
return validator.resolver.resolve_fragment(
368+
parent_schema,
369+
fragment,
370+
)
371+
372+
373+
def _find_dynamic_anchor_intermediate(validator, scopes, fragment):
374+
"""
375+
Find a schema that extends the dynamic anchor by an intermediate schema
376+
"""
377+
for url in scopes:
378+
with validator.resolver.resolving(url) as schema:
379+
if "$id" in schema:
380+
for intermediate_url in scopes:
381+
with validator.resolver.resolving(
382+
intermediate_url) as intermediate_schema:
383+
for subschema in search_schema(
384+
intermediate_schema, match_keyword("$ref")):
385+
if _schema_is_referenced(subschema, schema):
386+
return _find_dynamic_anchor_extender(
387+
validator,
388+
scopes,
389+
fragment,
390+
subschema,
391+
)
392+
393+
394+
def dynamic_anchor_extender(validator, scopes, fragment, schema, subschema):
395+
extender_schema = _find_dynamic_anchor_extender(
396+
validator, scopes, fragment, schema,
397+
)
398+
if not extender_schema:
399+
extender_schema = _find_dynamic_anchor_extender(
400+
validator, scopes, fragment, subschema,
401+
)
402+
if not extender_schema:
403+
extender_schema = _find_dynamic_anchor_intermediate(
404+
validator, scopes, fragment,
405+
)
406+
407+
return extender_schema
408+
409+
410+
def match_keyword(keyword):
411+
412+
def matcher(value):
413+
if keyword in value:
414+
yield value
415+
416+
return matcher
417+
418+
419+
def search_schema(schema, matcher):
420+
"""Breadth-first search routine."""
421+
values = deque([schema])
422+
while values:
423+
value = values.pop()
424+
if isinstance(value, list):
425+
values.extendleft(value)
426+
continue
427+
if not isinstance(value, dict):
428+
continue
429+
yield from matcher(value)
430+
values.extendleft(value.values())

jsonschema/_validators.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import re
44

55
from jsonschema._utils import (
6+
dynamic_anchor_extender,
67
ensure_list,
78
equal,
89
extras_msg,
@@ -302,14 +303,21 @@ def ref(validator, ref, instance, schema):
302303

303304
def dynamicRef(validator, dynamicRef, instance, schema):
304305
_, fragment = urldefrag(dynamicRef)
305-
306306
for url in validator.resolver._scopes_stack:
307307
lookup_url = urljoin(url, dynamicRef)
308308
with validator.resolver.resolving(lookup_url) as subschema:
309309
if ("$dynamicAnchor" in subschema
310310
and fragment == subschema["$dynamicAnchor"]):
311+
scope_stack = list(validator.resolver._scopes_stack)
312+
scope_stack.reverse()
313+
extended_schema = dynamic_anchor_extender(
314+
validator, scope_stack, fragment, schema, subschema,
315+
)
316+
if extended_schema:
317+
yield from validator.descend(instance, extended_schema)
318+
break
319+
311320
yield from validator.descend(instance, subschema)
312-
break
313321
else:
314322
with validator.resolver.resolving(dynamicRef) as subschema:
315323
yield from validator.descend(instance, subschema)

jsonschema/tests/test_jsonschema_test_suite.py

+1-9
Original file line numberDiff line numberDiff line change
@@ -405,15 +405,7 @@ def leap_second(test):
405405
skip=lambda test: (
406406
narrow_unicode_build(test)
407407
or skip(
408-
message="dynamicRef support isn't working yet.",
409-
subject="dynamicRef",
410-
)(test)
411-
or skip(
412-
message="These tests depends on dynamicRef working.",
413-
subject="defs",
414-
)(test)
415-
or skip(
416-
message="These tests depends on dynamicRef working.",
408+
message="These tests require an extension or the url resolver.",
417409
subject="anchor",
418410
case_description="same $anchor with different base uri",
419411
)(test)

jsonschema/validators.py

+5-23
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
"""
44
from __future__ import annotations
55

6-
from collections import deque
76
from collections.abc import Sequence
87
from functools import lru_cache
98
from urllib.parse import unquote, urldefrag, urljoin, urlsplit
@@ -770,7 +769,7 @@ def _find_in_referrer(self, key):
770769
@lru_cache()
771770
def _get_subschemas_cache(self):
772771
cache = {key: [] for key in _SUBSCHEMAS_KEYWORDS}
773-
for keyword, subschema in _search_schema(
772+
for keyword, subschema in _utils.search_schema(
774773
self.referrer, _match_subschema_keywords,
775774
):
776775
cache[keyword].append(subschema)
@@ -844,7 +843,10 @@ def resolve_fragment(self, document, fragment):
844843
else:
845844

846845
def find(key):
847-
yield from _search_schema(document, _match_keyword(key))
846+
yield from _utils.search_schema(
847+
document,
848+
_utils.match_keyword(key),
849+
)
848850

849851
for keyword in ["$anchor", "$dynamicAnchor"]:
850852
for subschema in find(keyword):
@@ -930,32 +932,12 @@ def resolve_remote(self, uri):
930932
_SUBSCHEMAS_KEYWORDS = ("$id", "id", "$anchor", "$dynamicAnchor")
931933

932934

933-
def _match_keyword(keyword):
934-
935-
def matcher(value):
936-
if keyword in value:
937-
yield value
938-
939-
return matcher
940-
941-
942935
def _match_subschema_keywords(value):
943936
for keyword in _SUBSCHEMAS_KEYWORDS:
944937
if keyword in value:
945938
yield keyword, value
946939

947940

948-
def _search_schema(schema, matcher):
949-
"""Breadth-first search routine."""
950-
values = deque([schema])
951-
while values:
952-
value = values.pop()
953-
if not isinstance(value, dict):
954-
continue
955-
yield from matcher(value)
956-
values.extendleft(value.values())
957-
958-
959941
def validate(instance, schema, cls=None, *args, **kwargs):
960942
"""
961943
Validate an instance under the given schema.

0 commit comments

Comments
 (0)