Skip to content

Commit ab759d5

Browse files
committed
Extend dynamicRef keyword
1 parent dfec932 commit ab759d5

File tree

4 files changed

+95
-34
lines changed

4 files changed

+95
-34
lines changed

jsonschema/_utils.py

Lines changed: 82 additions & 0 deletions
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

Lines changed: 10 additions & 2 deletions
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

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -401,15 +401,7 @@ def leap_second(test):
401401
skip=lambda test: (
402402
narrow_unicode_build(test)
403403
or skip(
404-
message="dynamicRef support isn't working yet.",
405-
subject="dynamicRef",
406-
)(test)
407-
or skip(
408-
message="These tests depends on dynamicRef working.",
409-
subject="defs",
410-
)(test)
411-
or skip(
412-
message="These tests depends on dynamicRef working.",
404+
message="These tests require an extension or the url resolver.",
413405
subject="anchor",
414406
case_description="same $anchor with different base uri",
415407
)(test)

jsonschema/validators.py

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""
22
Creation and extension of validators, with implementations for existing drafts.
33
"""
4-
from collections import deque
54
from collections.abc import Sequence
65
from functools import lru_cache
76
from urllib.parse import unquote, urldefrag, urljoin, urlsplit
@@ -761,7 +760,7 @@ def _find_in_referrer(self, key):
761760
@lru_cache()
762761
def _get_subschemas_cache(self):
763762
cache = {key: [] for key in _SUBSCHEMAS_KEYWORDS}
764-
for keyword, subschema in _search_schema(
763+
for keyword, subschema in _utils.search_schema(
765764
self.referrer, _match_subschema_keywords,
766765
):
767766
cache[keyword].append(subschema)
@@ -835,7 +834,7 @@ def resolve_fragment(self, document, fragment):
835834
else:
836835

837836
def find(key):
838-
yield from _search_schema(document, _match_keyword(key))
837+
yield from _utils.search_schema(document, _utils.match_keyword(key))
839838

840839
for keyword in ["$anchor", "$dynamicAnchor"]:
841840
for subschema in find(keyword):
@@ -921,32 +920,12 @@ def resolve_remote(self, uri):
921920
_SUBSCHEMAS_KEYWORDS = ("$id", "id", "$anchor", "$dynamicAnchor")
922921

923922

924-
def _match_keyword(keyword):
925-
926-
def matcher(value):
927-
if keyword in value:
928-
yield value
929-
930-
return matcher
931-
932-
933923
def _match_subschema_keywords(value):
934924
for keyword in _SUBSCHEMAS_KEYWORDS:
935925
if keyword in value:
936926
yield keyword, value
937927

938928

939-
def _search_schema(schema, matcher):
940-
"""Breadth-first search routine."""
941-
values = deque([schema])
942-
while values:
943-
value = values.pop()
944-
if not isinstance(value, dict):
945-
continue
946-
yield from matcher(value)
947-
values.extendleft(value.values())
948-
949-
950929
def validate(instance, schema, cls=None, *args, **kwargs):
951930
"""
952931
Validate an instance under the given schema.

0 commit comments

Comments
 (0)