diff options
Diffstat (limited to 'jsonschema')
-rw-r--r-- | jsonschema/_legacy_validators.py | 44 | ||||
-rw-r--r-- | jsonschema/_utils.py | 40 | ||||
-rw-r--r-- | jsonschema/protocols.py | 7 | ||||
-rw-r--r-- | jsonschema/tests/_suite.py | 17 | ||||
-rw-r--r-- | jsonschema/tests/test_jsonschema_test_suite.py | 306 | ||||
-rw-r--r-- | jsonschema/tests/test_validators.py | 1 | ||||
-rw-r--r-- | jsonschema/validators.py | 98 |
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: |