summaryrefslogtreecommitdiff
path: root/jsonschema
diff options
context:
space:
mode:
authorJulian Berman <Julian@GrayVines.com>2023-02-16 12:32:00 +0200
committerJulian Berman <Julian@GrayVines.com>2023-02-21 09:58:40 +0200
commite8266294408521daf38d879ba35c45a4b0ef5180 (patch)
tree0dba5b9aafc21b65d0a48b744cea021770e8e9d9 /jsonschema
parenta39e5c953a559b287c753bd604e3e11d218c29cf (diff)
downloadjsonschema-e8266294408521daf38d879ba35c45a4b0ef5180.tar.gz
Resolve $ref using the referencing library.
Passes all the remaining referencing tests across all drafts, hooray! Makes Validators take a referencing.Registry argument which users should use to customize preloaded schemas, or to configure remote reference retrieval. This fully obsoletes jsonschema.RefResolver, which has already been deprecated in a previous commit. Users should move to instead loading schemas into referencing.Registry objects. See the referencing documentation at https://referencing.rtfd.io/ for details (with more jsonschema-specific information to be added shortly). Note that the interface for resolving references on a Validator is not yet public (and hidden behind _resolver and _validate_reference attributes). One or both of these are likely to become public after some period of stabilization. Feedback is of course welcome!
Diffstat (limited to 'jsonschema')
-rw-r--r--jsonschema/_legacy_validators.py44
-rw-r--r--jsonschema/_utils.py40
-rw-r--r--jsonschema/protocols.py7
-rw-r--r--jsonschema/tests/_suite.py17
-rw-r--r--jsonschema/tests/test_jsonschema_test_suite.py306
-rw-r--r--jsonschema/tests/test_validators.py1
-rw-r--r--jsonschema/validators.py98
7 files changed, 131 insertions, 382 deletions
diff --git a/jsonschema/_legacy_validators.py b/jsonschema/_legacy_validators.py
index fa9daf4..aebf1b9 100644
--- a/jsonschema/_legacy_validators.py
+++ b/jsonschema/_legacy_validators.py
@@ -1,3 +1,5 @@
+from referencing.jsonschema import lookup_recursive_ref
+
from jsonschema import _utils
from jsonschema.exceptions import ValidationError
@@ -210,22 +212,12 @@ def contains_draft6_draft7(validator, contains, instance, schema):
def recursiveRef(validator, recursiveRef, instance, schema):
- lookup_url, target = validator.resolver.resolution_scope, validator.schema
-
- for each in reversed(validator.resolver._scopes_stack[1:]):
- lookup_url, next_target = validator.resolver.resolve(each)
- if next_target.get("$recursiveAnchor"):
- target = next_target
- else:
- break
-
- fragment = recursiveRef.lstrip("#")
- subschema = validator.resolver.resolve_fragment(target, fragment)
- # FIXME: This is gutted (and not calling .descend) because it can trigger
- # recursion errors, so there's a bug here. Re-enable the tests to
- # see it.
- subschema
- return []
+ resolved = lookup_recursive_ref(validator._resolver)
+ yield from validator.descend(
+ instance,
+ resolved.contents,
+ resolver=resolved.resolver,
+ )
def find_evaluated_item_indexes_by_schema(validator, instance, schema):
@@ -243,15 +235,17 @@ def find_evaluated_item_indexes_by_schema(validator, instance, schema):
return list(range(0, len(instance)))
if "$ref" in schema:
- scope, resolved = validator.resolver.resolve(schema["$ref"])
- validator.resolver.push_scope(scope)
-
- try:
- evaluated_indexes += find_evaluated_item_indexes_by_schema(
- validator, instance, resolved,
- )
- finally:
- validator.resolver.pop_scope()
+ resolved = validator._resolver.lookup(schema["$ref"])
+ evaluated_indexes.extend(
+ find_evaluated_item_indexes_by_schema(
+ validator.evolve(
+ schema=resolved.contents,
+ _resolver=resolved.resolver,
+ ),
+ instance,
+ resolved.contents,
+ ),
+ )
if "items" in schema:
if validator.is_type(schema["items"], "object"):
diff --git a/jsonschema/_utils.py b/jsonschema/_utils.py
index c218b18..b14b70e 100644
--- a/jsonschema/_utils.py
+++ b/jsonschema/_utils.py
@@ -201,15 +201,17 @@ def find_evaluated_item_indexes_by_schema(validator, instance, schema):
return list(range(0, len(instance)))
if "$ref" in schema:
- scope, resolved = validator.resolver.resolve(schema["$ref"])
- validator.resolver.push_scope(scope)
-
- try:
- evaluated_indexes += find_evaluated_item_indexes_by_schema(
- validator, instance, resolved,
- )
- finally:
- validator.resolver.pop_scope()
+ resolved = validator._resolver.lookup(schema["$ref"])
+ evaluated_indexes.extend(
+ find_evaluated_item_indexes_by_schema(
+ validator.evolve(
+ schema=resolved.contents,
+ _resolver=resolved.resolver,
+ ),
+ instance,
+ resolved.contents,
+ ),
+ )
if "prefixItems" in schema:
evaluated_indexes += list(range(0, len(schema["prefixItems"])))
@@ -260,15 +262,17 @@ def find_evaluated_property_keys_by_schema(validator, instance, schema):
evaluated_keys = []
if "$ref" in schema:
- scope, resolved = validator.resolver.resolve(schema["$ref"])
- validator.resolver.push_scope(scope)
-
- try:
- evaluated_keys += find_evaluated_property_keys_by_schema(
- validator, instance, resolved,
- )
- finally:
- validator.resolver.pop_scope()
+ resolved = validator._resolver.lookup(schema["$ref"])
+ evaluated_keys.extend(
+ find_evaluated_property_keys_by_schema(
+ validator.evolve(
+ schema=resolved.contents,
+ _resolver=resolved.resolver,
+ ),
+ instance,
+ resolved.contents,
+ ),
+ )
for keyword in [
"properties", "additionalProperties", "unevaluatedProperties",
diff --git a/jsonschema/protocols.py b/jsonschema/protocols.py
index eb67444..9d34f61 100644
--- a/jsonschema/protocols.py
+++ b/jsonschema/protocols.py
@@ -11,6 +11,8 @@ from collections.abc import Callable, Mapping
from typing import TYPE_CHECKING, Any, ClassVar, Iterable
import sys
+from referencing.jsonschema import SchemaRegistry
+
# doing these imports with `try ... except ImportError` doesn't pass mypy
# checking because mypy sees `typing._SpecialForm` and
# `typing_extensions._SpecialForm` as incompatible
@@ -60,6 +62,10 @@ class Validator(Protocol):
an invalid schema can lead to undefined behavior. See
`Validator.check_schema` to validate a schema first.
+ registry:
+
+ a schema registry that will be used for looking up JSON references
+
resolver:
a resolver that will be used to resolve :kw:`$ref`
@@ -113,6 +119,7 @@ class Validator(Protocol):
def __init__(
self,
schema: Mapping | bool,
+ registry: SchemaRegistry,
format_checker: jsonschema.FormatChecker | None = None,
) -> None:
...
diff --git a/jsonschema/tests/_suite.py b/jsonschema/tests/_suite.py
index 5fdae77..7f14ca5 100644
--- a/jsonschema/tests/_suite.py
+++ b/jsonschema/tests/_suite.py
@@ -21,7 +21,7 @@ import referencing.jsonschema
if TYPE_CHECKING:
import pyperf
-from jsonschema.validators import _VALIDATORS, _RefResolver
+from jsonschema.validators import _VALIDATORS
import jsonschema
_DELIMITERS = re.compile(r"[\W\- ]+")
@@ -245,20 +245,11 @@ class _Test:
def validate(self, Validator, **kwargs):
Validator.check_schema(self.schema)
- resolver = _RefResolver.from_schema(
+ validator = Validator(
schema=self.schema,
- store={k: v.contents for k, v in self._remotes.items()},
- id_of=Validator.ID_OF,
+ registry=self._remotes,
+ **kwargs,
)
-
- # XXX: #693 asks to improve the public API for this, since yeah, it's
- # bad. Figures that since it's hard for end-users, we experience
- # the pain internally here too.
- def prevent_network_access(uri):
- raise RuntimeError(f"Tried to access the network: {uri}")
- resolver.resolve_remote = prevent_network_access
-
- validator = Validator(schema=self.schema, resolver=resolver, **kwargs)
if os.environ.get("JSON_SCHEMA_DEBUG", "0") != "0":
breakpoint()
validator.validate(instance=self.data)
diff --git a/jsonschema/tests/test_jsonschema_test_suite.py b/jsonschema/tests/test_jsonschema_test_suite.py
index 3b602aa..fd2c499 100644
--- a/jsonschema/tests/test_jsonschema_test_suite.py
+++ b/jsonschema/tests/test_jsonschema_test_suite.py
@@ -8,7 +8,6 @@ See https://github.com/json-schema-org/JSON-Schema-Test-Suite for details.
import sys
-from jsonschema.tests._helpers import bug
from jsonschema.tests._suite import Suite
import jsonschema
@@ -135,13 +134,6 @@ TestDraft3 = DRAFT3.to_unittest_testcase(
skip=lambda test: (
missing_format(jsonschema.Draft3Validator)(test)
or complex_email_validation(test)
- or skip(
- message=bug(),
- subject="ref",
- case_description=(
- "$ref prevents a sibling id from changing the base uri"
- ),
- )(test)
),
)
@@ -160,49 +152,6 @@ TestDraft4 = DRAFT4.to_unittest_testcase(
or leap_second(test)
or missing_format(jsonschema.Draft4Validator)(test)
or complex_email_validation(test)
- or skip(
- message=bug(),
- subject="ref",
- case_description="Recursive references between schemas",
- )(test)
- or skip(
- message=bug(),
- subject="ref",
- case_description=(
- "Location-independent identifier with "
- "base URI change in subschema"
- ),
- )(test)
- or skip(
- message=bug(),
- subject="ref",
- case_description=(
- "$ref prevents a sibling id from changing the base uri"
- ),
- )(test)
- or skip(
- message=bug(),
- subject="id",
- description="match $ref to id",
- )(test)
- or skip(
- message=bug(),
- subject="id",
- description="no match on enum or $ref to id",
- )(test)
- or skip(
- message=bug(),
- subject="refRemote",
- case_description="base URI change - change folder in subschema",
- )(test)
- or skip(
- message=bug(),
- subject="ref",
- case_description=(
- "id must be resolved against nearest parent, "
- "not just immediate parent"
- ),
- )(test)
),
)
@@ -220,11 +169,6 @@ TestDraft6 = DRAFT6.to_unittest_testcase(
or leap_second(test)
or missing_format(jsonschema.Draft6Validator)(test)
or complex_email_validation(test)
- or skip(
- message=bug(),
- subject="refRemote",
- case_description="base URI change - change folder in subschema",
- )(test)
),
)
@@ -243,19 +187,6 @@ TestDraft7 = DRAFT7.to_unittest_testcase(
or leap_second(test)
or missing_format(jsonschema.Draft7Validator)(test)
or complex_email_validation(test)
- or skip(
- message=bug(),
- subject="refRemote",
- case_description="base URI change - change folder in subschema",
- )(test)
- or skip(
- message=bug(),
- subject="ref",
- case_description=(
- "$id must be resolved against nearest parent, "
- "not just immediate parent"
- ),
- )(test)
),
)
@@ -268,115 +199,12 @@ TestDraft201909 = DRAFT201909.to_unittest_testcase(
DRAFT201909.optional_cases_of(name="non-bmp-regex"),
DRAFT201909.optional_cases_of(name="refOfUnknownKeyword"),
Validator=jsonschema.Draft201909Validator,
- skip=lambda test: (
- skip(
- message="recursiveRef support isn't working yet.",
- subject="recursiveRef",
- case_description=(
- "$recursiveRef with no $recursiveAnchor in "
- "the initial target schema resource"
- ),
- description=(
- "leaf node does not match: recursion uses the inner schema"
- ),
- )(test)
- or skip(
- message="recursiveRef support isn't working yet.",
- subject="recursiveRef",
- description="leaf node matches: recursion uses the inner schema",
- )(test)
- or skip(
- message="recursiveRef support isn't working yet.",
- subject="recursiveRef",
- case_description=(
- "dynamic $recursiveRef destination (not predictable "
- "at schema compile time)"
- ),
- description="integer node",
- )(test)
- or skip(
- message="recursiveRef support isn't working yet.",
- subject="recursiveRef",
- case_description=(
- "multiple dynamic paths to the $recursiveRef keyword"
- ),
- description="recurse to integerNode - floats are not allowed",
- )(test)
- or skip(
- message="recursiveRef support isn't working yet.",
- subject="recursiveRef",
- description="integer does not match as a property value",
- )(test)
- or skip(
- message="recursiveRef support isn't working yet.",
- subject="recursiveRef",
- description=(
- "leaf node does not match: "
- "recursion only uses inner schema"
- ),
- )(test)
- or skip(
- message="recursiveRef support isn't working yet.",
- subject="recursiveRef",
- description=(
- "leaf node matches: "
- "recursion only uses inner schema"
- ),
- )(test)
- or skip(
- message="recursiveRef support isn't working yet.",
- subject="recursiveRef",
- description=(
- "two levels, integer does not match as a property value"
- ),
- )(test)
- or skip(
- message="recursiveRef support isn't working yet.",
- subject="recursiveRef",
- description="recursive mismatch",
- )(test)
- or skip(
- message="recursiveRef support isn't working yet.",
- subject="recursiveRef",
- description="two levels, no match",
- )(test)
- or skip(
- message="recursiveRef support isn't working yet.",
- subject="id",
- case_description=(
- "Invalid use of fragments in location-independent $id"
- ),
- )(test)
- or skip(
- message="dynamicRef support isn't fully working yet.",
- subject="defs",
- description="invalid definition schema",
- )(test)
- or skip(
- message="dynamicRef support isn't fully working yet.",
- subject="anchor",
- case_description="same $anchor with different base uri",
- )(test)
- or skip(
- message="Vocabulary support is still in-progress.",
- subject="vocabulary",
- description=(
- "no validation: invalid number, but it still validates"
- ),
- )(test)
- or skip(
- message=bug(),
- subject="ref",
- case_description=(
- "$id must be resolved against nearest parent, "
- "not just immediate parent"
- ),
- )(test)
- or skip(
- message=bug(),
- subject="refRemote",
- case_description="remote HTTP ref with nested absolute ref",
- )(test)
+ skip=skip(
+ message="Vocabulary support is still in-progress.",
+ subject="vocabulary",
+ description=(
+ "no validation: invalid number, but it still validates"
+ ),
),
)
@@ -404,122 +232,12 @@ TestDraft202012 = DRAFT202012.to_unittest_testcase(
DRAFT202012.optional_cases_of(name="non-bmp-regex"),
DRAFT202012.optional_cases_of(name="refOfUnknownKeyword"),
Validator=jsonschema.Draft202012Validator,
- skip=lambda test: (
- skip(
- message="dynamicRef support isn't fully working yet.",
- subject="dynamicRef",
- description="The recursive part is not valid against the root",
- )(test)
- or skip(
- message="dynamicRef support isn't fully working yet.",
- subject="dynamicRef",
- description="incorrect extended schema",
- case_description=(
- "$ref and $dynamicAnchor are independent of order - "
- "$defs first"
- ),
- )(test)
- or skip(
- message="dynamicRef support isn't fully working yet.",
- subject="dynamicRef",
- description="incorrect extended schema",
- case_description=(
- "$ref and $dynamicAnchor are independent of order - $ref first"
- ),
- )(test)
- or skip(
- message="dynamicRef support isn't fully working yet.",
- subject="dynamicRef",
- description=(
- "/then/$defs/thingy is the final stop for the $dynamicRef"
- ),
- )(test)
- or skip(
- message="dynamicRef support isn't fully working yet.",
- subject="dynamicRef",
- description=(
- "string matches /$defs/thingy, but the $dynamicRef "
- "does not stop here"
- ),
- )(test)
- or skip(
- message="dynamicRef support isn't fully working yet.",
- subject="dynamicRef",
- description=(
- "string matches /$defs/thingy, but the $dynamicRef "
- "does not stop here"
- ),
- )(test)
- or skip(
- message="dynamicRef support isn't fully working yet.",
- subject="dynamicRef",
- description="recurse to integerNode - floats are not allowed",
- )(test)
- or skip(
- message="dynamicRef support isn't fully working yet.",
- subject="defs",
- description="invalid definition schema",
- )(test)
- or skip(
- message="dynamicRef support isn't fully working yet.",
- subject="anchor",
- case_description="same $anchor with different base uri",
- )(test)
- or skip(
- message="dynamicRef support isn't fully working yet.",
- subject="dynamicRef",
- description="instance with misspelled field",
- case_description=(
- "strict-tree schema, guards against misspelled properties"
- ),
- )(test)
- or skip(
- message="dynamicRef support isn't fully working yet.",
- subject="dynamicRef",
- description="An array containing non-strings is invalid",
- case_description=(
- "A $dynamicRef resolves to the first $dynamicAnchor still "
- "in scope that is encountered when the schema is evaluated"
- ),
- )(test)
- or skip(
- message="dynamicRef support isn't fully working yet.",
- subject="dynamicRef",
- description="An array containing non-strings is invalid",
- case_description=(
- "A $dynamicRef with intermediate scopes that don't include a "
- "matching $dynamicAnchor does not affect dynamic scope "
- "resolution"
- ),
- )(test)
- or skip(
- message="dynamicRef support isn't fully working yet.",
- subject="dynamicRef",
- description="incorrect extended schema",
- case_description=(
- "tests for implementation dynamic anchor and reference link"
- ),
- )(test)
- or skip(
- message="Vocabulary support is still in-progress.",
- subject="vocabulary",
- description=(
- "no validation: invalid number, but it still validates"
- ),
- )(test)
- or skip(
- message=bug(),
- subject="ref",
- case_description=(
- "$id must be resolved against nearest parent, "
- "not just immediate parent"
- ),
- )(test)
- or skip(
- message=bug(),
- subject="refRemote",
- case_description="remote HTTP ref with nested absolute ref",
- )(test)
+ skip=skip(
+ message="Vocabulary support is still in-progress.",
+ subject="vocabulary",
+ description=(
+ "no validation: invalid number, but it still validates"
+ ),
),
)
diff --git a/jsonschema/tests/test_validators.py b/jsonschema/tests/test_validators.py
index 8553cef..c62498b 100644
--- a/jsonschema/tests/test_validators.py
+++ b/jsonschema/tests/test_validators.py
@@ -1553,6 +1553,7 @@ class ValidatorTestMixin(MetaSchemaTestsMixin, object):
expected = self.Validator(
{"type": "string"},
format_checker=self.Validator.FORMAT_CHECKER,
+ _resolver=new._resolver,
)
self.assertEqual(new, expected)
diff --git a/jsonschema/validators.py b/jsonschema/validators.py
index 5a0cd43..b802856 100644
--- a/jsonschema/validators.py
+++ b/jsonschema/validators.py
@@ -17,6 +17,7 @@ import warnings
from jsonschema_specifications import REGISTRY as SPECIFICATIONS
from pyrsistent import m
+from referencing import Specification
import attr
import referencing.jsonschema
@@ -170,6 +171,11 @@ def create(
# preemptively don't shadow the `Validator.format_checker` local
format_checker_arg = format_checker
+ specification = referencing.jsonschema.specification_with(
+ dialect_id=id_of(meta_schema),
+ default=Specification.OPAQUE,
+ )
+
@attr.s
class Validator:
@@ -182,6 +188,19 @@ def create(
schema = attr.ib(repr=reprlib.repr)
_ref_resolver = attr.ib(default=None, repr=False, alias="resolver")
format_checker = attr.ib(default=None)
+ # TODO: include new meta-schemas added at runtime
+ _registry = attr.ib(
+ default=SPECIFICATIONS,
+ converter=SPECIFICATIONS.combine, # type: ignore[misc]
+ kw_only=True,
+ repr=False,
+ )
+ _resolver = attr.ib(
+ alias="_resolver",
+ default=None,
+ kw_only=True,
+ repr=False,
+ )
def __init_subclass__(cls):
warnings.warn(
@@ -198,6 +217,12 @@ def create(
stacklevel=2,
)
+ def __attrs_post_init__(self):
+ if self._resolver is None:
+ self._resolver = self._registry.resolver_with_root(
+ resource=specification.create_resource(self.schema),
+ )
+
@classmethod
def check_schema(cls, schema, format_checker=_UNSET):
Validator = validator_for(cls.META_SCHEMA, default=cls)
@@ -276,38 +301,39 @@ def create(
)
return
- # Temporarily needed to eagerly create a resolver...
- with warnings.catch_warnings():
- warnings.simplefilter("ignore")
- self.resolver
- scope = id_of(_schema)
- if scope:
- self.resolver.push_scope(scope)
- try:
- for k, v in applicable_validators(_schema):
- validator = self.VALIDATORS.get(k)
- if validator is None:
- continue
-
- errors = validator(self, v, instance, _schema) or ()
- for error in errors:
- # set details if not already set by the called fn
- error._set(
- validator=k,
- validator_value=v,
- instance=instance,
- schema=_schema,
- type_checker=self.TYPE_CHECKER,
- )
- if k not in {"if", "$ref"}:
- error.schema_path.appendleft(k)
- yield error
- finally:
- if scope:
- self.resolver.pop_scope()
-
- def descend(self, instance, schema, path=None, schema_path=None):
- for error in self.evolve(schema=schema).iter_errors(instance):
+ for k, v in applicable_validators(_schema):
+ validator = self.VALIDATORS.get(k)
+ if validator is None:
+ continue
+
+ errors = validator(self, v, instance, _schema) or ()
+ for error in errors:
+ # set details if not already set by the called fn
+ error._set(
+ validator=k,
+ validator_value=v,
+ instance=instance,
+ schema=_schema,
+ type_checker=self.TYPE_CHECKER,
+ )
+ if k not in {"if", "$ref"}:
+ error.schema_path.appendleft(k)
+ yield error
+
+ def descend(
+ self,
+ instance,
+ schema,
+ path=None,
+ schema_path=None,
+ resolver=None,
+ ):
+ if resolver is None:
+ resolver = self._resolver.in_subresource(
+ specification.create_resource(schema),
+ )
+ validator = self.evolve(schema=schema, _resolver=resolver)
+ for error in validator.iter_errors(instance):
if path is not None:
error.path.appendleft(path)
if schema_path is not None:
@@ -325,6 +351,14 @@ def create(
raise exceptions.UnknownType(type, instance, self.schema)
def _validate_reference(self, ref, instance):
+ if self._ref_resolver is None:
+ resolved = self._resolver.lookup(ref)
+ return self.descend(
+ instance,
+ resolved.contents,
+ resolver=resolved.resolver,
+ )
+ else:
resolve = getattr(self._ref_resolver, "resolve", None)
if resolve is None:
with self._ref_resolver.resolving(ref) as resolved: