diff options
Diffstat (limited to 'jsonschema/validators.py')
-rw-r--r-- | jsonschema/validators.py | 314 |
1 files changed, 223 insertions, 91 deletions
diff --git a/jsonschema/validators.py b/jsonschema/validators.py index d370dc5..62e86df 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -13,11 +13,13 @@ from warnings import warn import contextlib import json import reprlib -import typing import warnings -from pyrsistent import m +from jsonschema_specifications import REGISTRY as SPECIFICATIONS +from referencing import Specification +from rpds import HashTrieMap import attr +import referencing.jsonschema from jsonschema import ( _format, @@ -33,7 +35,6 @@ _UNSET = _utils.Unset() _VALIDATORS: dict[str, Validator] = {} _META_SCHEMAS = _utils.URIDict() -_VOCABULARIES: list[tuple[str, typing.Any]] = [] def __getattr__(name): @@ -62,6 +63,13 @@ def __getattr__(name): stacklevel=2, ) return _META_SCHEMAS + elif name == "RefResolver": + warnings.warn( + _RefResolver._DEPRECATION_MESSAGE, + DeprecationWarning, + stacklevel=2, + ) + return _RefResolver raise AttributeError(f"module {__name__} has no attribute {name}") @@ -93,34 +101,13 @@ def validates(version): return _validates -def _id_of(schema): - """ - Return the ID of a schema for recent JSON Schema drafts. - """ - if schema is True or schema is False: - return "" - return schema.get("$id", "") - - -def _store_schema_list(): - if not _VOCABULARIES: - package = _utils.resources.files(__package__) - for version in package.joinpath("schemas", "vocabularies").iterdir(): - for path in version.iterdir(): - vocabulary = json.loads(path.read_text()) - _VOCABULARIES.append((vocabulary["$id"], vocabulary)) - return [ - (id, validator.META_SCHEMA) for id, validator in _META_SCHEMAS.items() - ] + _VOCABULARIES - - def create( meta_schema, validators=(), version=None, type_checker=_types.draft202012_type_checker, format_checker=_format.draft202012_format_checker, - id_of=_id_of, + id_of=referencing.jsonschema.DRAFT202012.id_of, applicable_validators=methodcaller("items"), ): """ @@ -184,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: @@ -194,8 +186,21 @@ def create( ID_OF = staticmethod(id_of) schema = attr.ib(repr=reprlib.repr) - resolver = attr.ib(default=None, repr=False) + _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( @@ -212,11 +217,27 @@ def create( stacklevel=2, ) + def evolve(self, **changes): + cls = self.__class__ + schema = changes.setdefault("schema", self.schema) + NewValidator = validator_for(schema, default=cls) + + for field in attr.fields(cls): + if not field.init: + continue + attr_name = field.name + init_name = field.alias + if init_name not in changes: + changes[init_name] = getattr(self, attr_name) + + return NewValidator(**changes) + + cls.evolve = evolve + def __attrs_post_init__(self): - if self.resolver is None: - self.resolver = RefResolver.from_schema( - self.schema, - id_of=id_of, + if self._resolver is None: + self._resolver = self._registry.resolver_with_root( + resource=specification.create_resource(self.schema), ) @classmethod @@ -231,19 +252,32 @@ def create( for error in validator.iter_errors(schema): raise exceptions.SchemaError.create_from(error) - def evolve(self, **changes): - # Essentially reproduces attr.evolve, but may involve instantiating - # a different class than this one. - cls = self.__class__ + @property + def resolver(self): + warnings.warn( + ( + f"Accessing {self.__class__.__name__}.resolver is " + "deprecated as of v4.18.0, in favor of the " + "https://github.com/python-jsonschema/referencing " + "library, which provides more compliant referencing " + "behavior as well as more flexible APIs for " + "customization." + ), + DeprecationWarning, + stacklevel=2, + ) + if self._ref_resolver is None: + self._ref_resolver = _RefResolver.from_schema( + self.schema, + id_of=id_of, + ) + return self._ref_resolver + def evolve(self, **changes): schema = changes.setdefault("schema", self.schema) - NewValidator = validator_for(schema, default=cls) + NewValidator = validator_for(schema, default=self.__class__) - for field in attr.fields(cls): - if not field.init: - continue - attr_name = field.name # To deal with private attributes. - init_name = attr_name if attr_name[0] != "_" else attr_name[1:] + for (attr_name, init_name) in evolve_fields: if init_name not in changes: changes[init_name] = getattr(self, attr_name) @@ -276,39 +310,73 @@ def create( ) return - 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 + 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 schema is True: + return + elif schema is False: + yield exceptions.ValidationError( + f"False schema does not allow {instance!r}", + validator=None, + validator_value=None, + instance=instance, + schema=schema, + ) + return - 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): - if path is not None: - error.path.appendleft(path) - if schema_path is not None: - error.schema_path.appendleft(schema_path) - yield error + if resolver is None: + resolver = self._resolver.in_subresource( + specification.create_resource(schema), + ) + evolved = self.evolve(schema=schema, _resolver=resolver) + + for k, v in applicable_validators(schema): + validator = evolved.VALIDATORS.get(k) + if validator is None: + continue + + errors = validator(evolved, 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=evolved.TYPE_CHECKER, + ) + if k not in {"if", "$ref"}: + error.schema_path.appendleft(k) + if path is not None: + error.path.appendleft(path) + if schema_path is not None: + error.schema_path.appendleft(schema_path) + yield error def validate(self, *args, **kwargs): for error in self.iter_errors(*args, **kwargs): @@ -320,6 +388,28 @@ def create( except exceptions.UndefinedTypeCheck: 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: + return self.descend(instance, resolved) + else: + scope, resolved = resolve(ref) + self._ref_resolver.push_scope(scope) + + try: + return self.descend(instance, resolved) + finally: + self._ref_resolver.pop_scope() + def is_valid(self, instance, _schema=None): if _schema is not None: warnings.warn( @@ -337,6 +427,12 @@ def create( error = next(self.iter_errors(instance), None) return error is None + evolve_fields = [ + (field.name, field.alias) + for field in attr.fields(Validator) + if field.init + ] + if version is not None: safe = version.title().replace(" ", "").replace("-", "") Validator.__name__ = Validator.__qualname__ = f"{safe}Validator" @@ -430,7 +526,9 @@ def extend( Draft3Validator = create( - meta_schema=_utils.load_schema("draft3"), + meta_schema=SPECIFICATIONS.contents( + "http://json-schema.org/draft-03/schema#", + ), validators={ "$ref": _validators.ref, "additionalItems": _validators.additionalItems, @@ -457,12 +555,14 @@ Draft3Validator = create( type_checker=_types.draft3_type_checker, format_checker=_format.draft3_format_checker, version="draft3", - id_of=_legacy_validators.id_of_ignore_ref(property="id"), + id_of=referencing.jsonschema.DRAFT3.id_of, applicable_validators=_legacy_validators.ignore_ref_siblings, ) Draft4Validator = create( - meta_schema=_utils.load_schema("draft4"), + meta_schema=SPECIFICATIONS.contents( + "http://json-schema.org/draft-04/schema#", + ), validators={ "$ref": _validators.ref, "additionalItems": _validators.additionalItems, @@ -494,12 +594,14 @@ Draft4Validator = create( type_checker=_types.draft4_type_checker, format_checker=_format.draft4_format_checker, version="draft4", - id_of=_legacy_validators.id_of_ignore_ref(property="id"), + id_of=referencing.jsonschema.DRAFT4.id_of, applicable_validators=_legacy_validators.ignore_ref_siblings, ) Draft6Validator = create( - meta_schema=_utils.load_schema("draft6"), + meta_schema=SPECIFICATIONS.contents( + "http://json-schema.org/draft-06/schema#", + ), validators={ "$ref": _validators.ref, "additionalItems": _validators.additionalItems, @@ -536,12 +638,14 @@ Draft6Validator = create( type_checker=_types.draft6_type_checker, format_checker=_format.draft6_format_checker, version="draft6", - id_of=_legacy_validators.id_of_ignore_ref(), + id_of=referencing.jsonschema.DRAFT6.id_of, applicable_validators=_legacy_validators.ignore_ref_siblings, ) Draft7Validator = create( - meta_schema=_utils.load_schema("draft7"), + meta_schema=SPECIFICATIONS.contents( + "http://json-schema.org/draft-07/schema#", + ), validators={ "$ref": _validators.ref, "additionalItems": _validators.additionalItems, @@ -579,12 +683,14 @@ Draft7Validator = create( type_checker=_types.draft7_type_checker, format_checker=_format.draft7_format_checker, version="draft7", - id_of=_legacy_validators.id_of_ignore_ref(), + id_of=referencing.jsonschema.DRAFT7.id_of, applicable_validators=_legacy_validators.ignore_ref_siblings, ) Draft201909Validator = create( - meta_schema=_utils.load_schema("draft2019-09"), + meta_schema=SPECIFICATIONS.contents( + "https://json-schema.org/draft/2019-09/schema", + ), validators={ "$recursiveRef": _legacy_validators.recursiveRef, "$ref": _validators.ref, @@ -629,7 +735,9 @@ Draft201909Validator = create( ) Draft202012Validator = create( - meta_schema=_utils.load_schema("draft2020-12"), + meta_schema=SPECIFICATIONS.contents( + "https://json-schema.org/draft/2020-12/schema", + ), validators={ "$dynamicRef": _validators.dynamicRef, "$ref": _validators.ref, @@ -677,7 +785,7 @@ Draft202012Validator = create( _LATEST_VERSION = Draft202012Validator -class RefResolver: +class _RefResolver: """ Resolve JSON References. @@ -719,13 +827,26 @@ class RefResolver: cache_remote (bool): Whether remote refs should be cached after first resolution + + .. deprecated:: v4.18.0 + + ``RefResolver`` has been deprecated in favor of `referencing`. """ + _DEPRECATION_MESSAGE = ( + "jsonschema.RefResolver is deprecated as of v4.18.0, in favor of the " + "https://github.com/python-jsonschema/referencing library, which " + "provides more compliant referencing behavior as well as more " + "flexible APIs for customization. A future release will remove " + "RefResolver. Please file a feature request (on referencing) if you " + "are missing an API for the kind of customization you need." + ) + def __init__( self, base_uri, referrer, - store=m(), + store=HashTrieMap(), cache_remote=True, handlers=(), urljoin_cache=None, @@ -742,7 +863,12 @@ class RefResolver: self._scopes_stack = [base_uri] - self.store = _utils.URIDict(_store_schema_list()) + self.store = _utils.URIDict( + (uri, each.contents) for uri, each in SPECIFICATIONS.items() + ) + self.store.update( + (id, each.META_SCHEMA) for id, each in _META_SCHEMAS.items() + ) self.store.update(store) self.store.update( (schema["$id"], schema) @@ -755,7 +881,13 @@ class RefResolver: self._remote_cache = remote_cache @classmethod - def from_schema(cls, schema, id_of=_id_of, *args, **kwargs): + def from_schema( + cls, + schema, + id_of=referencing.jsonschema.DRAFT202012.id_of, + *args, + **kwargs, + ): """ Construct a resolver from a JSON schema object. @@ -767,10 +899,10 @@ class RefResolver: Returns: - `RefResolver` + `_RefResolver` """ - return cls(base_uri=id_of(schema), referrer=schema, *args, **kwargs) # noqa: B026, E501 + return cls(base_uri=id_of(schema) or "", referrer=schema, *args, **kwargs) # noqa: B026, E501 def push_scope(self, scope): """ @@ -796,7 +928,7 @@ class RefResolver: try: self._scopes_stack.pop() except IndexError: - raise exceptions.RefResolutionError( + raise exceptions._RefResolutionError( "Failed to pop the scope from an empty stack. " "`pop_scope()` should only be called once for every " "`push_scope()`", @@ -912,7 +1044,7 @@ class RefResolver: try: document = self.resolve_remote(url) except Exception as exc: - raise exceptions.RefResolutionError(exc) + raise exceptions._RefResolutionError(exc) return self.resolve_fragment(document, fragment) @@ -966,7 +1098,7 @@ class RefResolver: try: document = document[part] except (TypeError, LookupError): - raise exceptions.RefResolutionError( + raise exceptions._RefResolutionError( f"Unresolvable JSON pointer: {fragment!r}", ) |