123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410 |
- """
- Creation and extension of validators, with implementations for existing drafts.
- """
- from __future__ import annotations
- from collections import deque
- from collections.abc import Iterable, Mapping, Sequence
- from functools import lru_cache
- from operator import methodcaller
- from typing import TYPE_CHECKING
- from urllib.parse import unquote, urldefrag, urljoin, urlsplit
- from urllib.request import urlopen
- from warnings import warn
- import contextlib
- import json
- import reprlib
- import warnings
- from attrs import define, field, fields
- from jsonschema_specifications import REGISTRY as SPECIFICATIONS
- from rpds import HashTrieMap
- import referencing.exceptions
- import referencing.jsonschema
- from jsonschema import (
- _format,
- _keywords,
- _legacy_keywords,
- _types,
- _typing,
- _utils,
- exceptions,
- )
- if TYPE_CHECKING:
- from jsonschema.protocols import Validator
- _UNSET = _utils.Unset()
- _VALIDATORS: dict[str, Validator] = {}
- _META_SCHEMAS = _utils.URIDict()
- def __getattr__(name):
- if name == "ErrorTree":
- warnings.warn(
- "Importing ErrorTree from jsonschema.validators is deprecated. "
- "Instead import it from jsonschema.exceptions.",
- DeprecationWarning,
- stacklevel=2,
- )
- from jsonschema.exceptions import ErrorTree
- return ErrorTree
- elif name == "validators":
- warnings.warn(
- "Accessing jsonschema.validators.validators is deprecated. "
- "Use jsonschema.validators.validator_for with a given schema.",
- DeprecationWarning,
- stacklevel=2,
- )
- return _VALIDATORS
- elif name == "meta_schemas":
- warnings.warn(
- "Accessing jsonschema.validators.meta_schemas is deprecated. "
- "Use jsonschema.validators.validator_for with a given schema.",
- DeprecationWarning,
- 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}")
- def validates(version):
- """
- Register the decorated validator for a ``version`` of the specification.
- Registered validators and their meta schemas will be considered when
- parsing :kw:`$schema` keywords' URIs.
- Arguments:
- version (str):
- An identifier to use as the version's name
- Returns:
- collections.abc.Callable:
- a class decorator to decorate the validator with the version
- """
- def _validates(cls):
- _VALIDATORS[version] = cls
- meta_schema_id = cls.ID_OF(cls.META_SCHEMA)
- _META_SCHEMAS[meta_schema_id] = cls
- return cls
- return _validates
- def _warn_for_remote_retrieve(uri: str):
- from urllib.request import Request, urlopen
- headers = {"User-Agent": "python-jsonschema (deprecated $ref resolution)"}
- request = Request(uri, headers=headers) # noqa: S310
- with urlopen(request) as response: # noqa: S310
- warnings.warn(
- "Automatically retrieving remote references can be a security "
- "vulnerability and is discouraged by the JSON Schema "
- "specifications. Relying on this behavior is deprecated "
- "and will shortly become an error. If you are sure you want to "
- "remotely retrieve your reference and that it is safe to do so, "
- "you can find instructions for doing so via referencing.Registry "
- "in the referencing documentation "
- "(https://referencing.readthedocs.org).",
- DeprecationWarning,
- stacklevel=9, # Ha ha ha ha magic numbers :/
- )
- return referencing.Resource.from_contents(
- json.load(response),
- default_specification=referencing.jsonschema.DRAFT202012,
- )
- _REMOTE_WARNING_REGISTRY = SPECIFICATIONS.combine(
- referencing.Registry(retrieve=_warn_for_remote_retrieve), # type: ignore[call-arg]
- )
- def create(
- meta_schema: referencing.jsonschema.ObjectSchema,
- validators: (
- Mapping[str, _typing.SchemaKeywordValidator]
- | Iterable[tuple[str, _typing.SchemaKeywordValidator]]
- ) = (),
- version: str | None = None,
- type_checker: _types.TypeChecker = _types.draft202012_type_checker,
- format_checker: _format.FormatChecker = _format.draft202012_format_checker,
- id_of: _typing.id_of = referencing.jsonschema.DRAFT202012.id_of,
- applicable_validators: _typing.ApplicableValidators = methodcaller(
- "items",
- ),
- ):
- """
- Create a new validator class.
- Arguments:
- meta_schema:
- the meta schema for the new validator class
- validators:
- a mapping from names to callables, where each callable will
- validate the schema property with the given name.
- Each callable should take 4 arguments:
- 1. a validator instance,
- 2. the value of the property being validated within the
- instance
- 3. the instance
- 4. the schema
- version:
- an identifier for the version that this validator class will
- validate. If provided, the returned validator class will
- have its ``__name__`` set to include the version, and also
- will have `jsonschema.validators.validates` automatically
- called for the given version.
- type_checker:
- a type checker, used when applying the :kw:`type` keyword.
- If unprovided, a `jsonschema.TypeChecker` will be created
- with a set of default types typical of JSON Schema drafts.
- format_checker:
- a format checker, used when applying the :kw:`format` keyword.
- If unprovided, a `jsonschema.FormatChecker` will be created
- with a set of default formats typical of JSON Schema drafts.
- id_of:
- A function that given a schema, returns its ID.
- applicable_validators:
- A function that, given a schema, returns the list of
- applicable schema keywords and associated values
- which will be used to validate the instance.
- This is mostly used to support pre-draft 7 versions of JSON Schema
- which specified behavior around ignoring keywords if they were
- siblings of a ``$ref`` keyword. If you're not attempting to
- implement similar behavior, you can typically ignore this argument
- and leave it at its default.
- Returns:
- a new `jsonschema.protocols.Validator` class
- """
- # 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) or "urn:unknown-dialect",
- default=referencing.Specification.OPAQUE,
- )
- @define
- class Validator:
- VALIDATORS = dict(validators) # noqa: RUF012
- META_SCHEMA = dict(meta_schema) # noqa: RUF012
- TYPE_CHECKER = type_checker
- FORMAT_CHECKER = format_checker_arg
- ID_OF = staticmethod(id_of)
- _APPLICABLE_VALIDATORS = applicable_validators
- _validators = field(init=False, repr=False, eq=False)
- schema: referencing.jsonschema.Schema = field(repr=reprlib.repr)
- _ref_resolver = field(default=None, repr=False, alias="resolver")
- format_checker: _format.FormatChecker | None = field(default=None)
- # TODO: include new meta-schemas added at runtime
- _registry: referencing.jsonschema.SchemaRegistry = field(
- default=_REMOTE_WARNING_REGISTRY,
- kw_only=True,
- repr=False,
- )
- _resolver = field(
- alias="_resolver",
- default=None,
- kw_only=True,
- repr=False,
- )
- def __init_subclass__(cls):
- warnings.warn(
- (
- "Subclassing validator classes is not intended to "
- "be part of their public API. A future version "
- "will make doing so an error, as the behavior of "
- "subclasses isn't guaranteed to stay the same "
- "between releases of jsonschema. Instead, prefer "
- "composition of validators, wrapping them in an object "
- "owned entirely by the downstream library."
- ),
- DeprecationWarning,
- stacklevel=2,
- )
- def evolve(self, **changes):
- cls = self.__class__
- schema = changes.setdefault("schema", self.schema)
- NewValidator = validator_for(schema, default=cls)
- for field in fields(cls): # noqa: F402
- 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:
- registry = self._registry
- if registry is not _REMOTE_WARNING_REGISTRY:
- registry = SPECIFICATIONS.combine(registry)
- resource = specification.create_resource(self.schema)
- self._resolver = registry.resolver_with_root(resource)
- if self.schema is True or self.schema is False:
- self._validators = []
- else:
- self._validators = [
- (self.VALIDATORS[k], k, v)
- for k, v in applicable_validators(self.schema)
- if k in self.VALIDATORS
- ]
- # REMOVEME: Legacy ref resolution state management.
- push_scope = getattr(self._ref_resolver, "push_scope", None)
- if push_scope is not None:
- id = id_of(self.schema)
- if id is not None:
- push_scope(id)
- @classmethod
- def check_schema(cls, schema, format_checker=_UNSET):
- Validator = validator_for(cls.META_SCHEMA, default=cls)
- if format_checker is _UNSET:
- format_checker = Validator.FORMAT_CHECKER
- validator = Validator(
- schema=cls.META_SCHEMA,
- format_checker=format_checker,
- )
- for error in validator.iter_errors(schema):
- raise exceptions.SchemaError.create_from(error)
- @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=self.__class__)
- for (attr_name, init_name) in evolve_fields:
- if init_name not in changes:
- changes[init_name] = getattr(self, attr_name)
- return NewValidator(**changes)
- def iter_errors(self, instance, _schema=None):
- if _schema is not None:
- warnings.warn(
- (
- "Passing a schema to Validator.iter_errors "
- "is deprecated and will be removed in a future "
- "release. Call validator.evolve(schema=new_schema)."
- "iter_errors(...) instead."
- ),
- DeprecationWarning,
- stacklevel=2,
- )
- validators = [
- (self.VALIDATORS[k], k, v)
- for k, v in applicable_validators(_schema)
- if k in self.VALIDATORS
- ]
- else:
- _schema, validators = self.schema, self._validators
- 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
- for validator, k, v in validators:
- 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
- if self._ref_resolver is not None:
- evolved = self.evolve(schema=schema)
- else:
- 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):
- raise error
- def is_type(self, instance, type):
- try:
- return self.TYPE_CHECKER.is_type(instance, type)
- except exceptions.UndefinedTypeCheck:
- exc = exceptions.UnknownType(type, instance, self.schema)
- raise exc from None
- def _validate_reference(self, ref, instance):
- if self._ref_resolver is None:
- try:
- resolved = self._resolver.lookup(ref)
- except referencing.exceptions.Unresolvable as err:
- raise exceptions._WrappedReferencingError(err) from err
- 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 list(self.descend(instance, resolved))
- finally:
- self._ref_resolver.pop_scope()
- def is_valid(self, instance, _schema=None):
- if _schema is not None:
- warnings.warn(
- (
- "Passing a schema to Validator.is_valid is deprecated "
- "and will be removed in a future release. Call "
- "validator.evolve(schema=new_schema).is_valid(...) "
- "instead."
- ),
- DeprecationWarning,
- stacklevel=2,
- )
- self = self.evolve(schema=_schema)
- error = next(self.iter_errors(instance), None)
- return error is None
- evolve_fields = [
- (field.name, field.alias)
- for field in fields(Validator)
- if field.init
- ]
- if version is not None:
- safe = version.title().replace(" ", "").replace("-", "")
- Validator.__name__ = Validator.__qualname__ = f"{safe}Validator"
- Validator = validates(version)(Validator) # type: ignore[misc]
- return Validator
- def extend(
- validator,
- validators=(),
- version=None,
- type_checker=None,
- format_checker=None,
- ):
- """
- Create a new validator class by extending an existing one.
- Arguments:
- validator (jsonschema.protocols.Validator):
- an existing validator class
- validators (collections.abc.Mapping):
- a mapping of new validator callables to extend with, whose
- structure is as in `create`.
- .. note::
- Any validator callables with the same name as an
- existing one will (silently) replace the old validator
- callable entirely, effectively overriding any validation
- done in the "parent" validator class.
- If you wish to instead extend the behavior of a parent's
- validator callable, delegate and call it directly in
- the new validator function by retrieving it using
- ``OldValidator.VALIDATORS["validation_keyword_name"]``.
- version (str):
- a version for the new validator class
- type_checker (jsonschema.TypeChecker):
- a type checker, used when applying the :kw:`type` keyword.
- If unprovided, the type checker of the extended
- `jsonschema.protocols.Validator` will be carried along.
- format_checker (jsonschema.FormatChecker):
- a format checker, used when applying the :kw:`format` keyword.
- If unprovided, the format checker of the extended
- `jsonschema.protocols.Validator` will be carried along.
- Returns:
- a new `jsonschema.protocols.Validator` class extending the one
- provided
- .. note:: Meta Schemas
- The new validator class will have its parent's meta schema.
- If you wish to change or extend the meta schema in the new
- validator class, modify ``META_SCHEMA`` directly on the returned
- class. Note that no implicit copying is done, so a copy should
- likely be made before modifying it, in order to not affect the
- old validator.
- """
- all_validators = dict(validator.VALIDATORS)
- all_validators.update(validators)
- if type_checker is None:
- type_checker = validator.TYPE_CHECKER
- if format_checker is None:
- format_checker = validator.FORMAT_CHECKER
- return create(
- meta_schema=validator.META_SCHEMA,
- validators=all_validators,
- version=version,
- type_checker=type_checker,
- format_checker=format_checker,
- id_of=validator.ID_OF,
- applicable_validators=validator._APPLICABLE_VALIDATORS,
- )
- Draft3Validator = create(
- meta_schema=SPECIFICATIONS.contents(
- "http://json-schema.org/draft-03/schema#",
- ),
- validators={
- "$ref": _keywords.ref,
- "additionalItems": _legacy_keywords.additionalItems,
- "additionalProperties": _keywords.additionalProperties,
- "dependencies": _legacy_keywords.dependencies_draft3,
- "disallow": _legacy_keywords.disallow_draft3,
- "divisibleBy": _keywords.multipleOf,
- "enum": _keywords.enum,
- "extends": _legacy_keywords.extends_draft3,
- "format": _keywords.format,
- "items": _legacy_keywords.items_draft3_draft4,
- "maxItems": _keywords.maxItems,
- "maxLength": _keywords.maxLength,
- "maximum": _legacy_keywords.maximum_draft3_draft4,
- "minItems": _keywords.minItems,
- "minLength": _keywords.minLength,
- "minimum": _legacy_keywords.minimum_draft3_draft4,
- "pattern": _keywords.pattern,
- "patternProperties": _keywords.patternProperties,
- "properties": _legacy_keywords.properties_draft3,
- "type": _legacy_keywords.type_draft3,
- "uniqueItems": _keywords.uniqueItems,
- },
- type_checker=_types.draft3_type_checker,
- format_checker=_format.draft3_format_checker,
- version="draft3",
- id_of=referencing.jsonschema.DRAFT3.id_of,
- applicable_validators=_legacy_keywords.ignore_ref_siblings,
- )
- Draft4Validator = create(
- meta_schema=SPECIFICATIONS.contents(
- "http://json-schema.org/draft-04/schema#",
- ),
- validators={
- "$ref": _keywords.ref,
- "additionalItems": _legacy_keywords.additionalItems,
- "additionalProperties": _keywords.additionalProperties,
- "allOf": _keywords.allOf,
- "anyOf": _keywords.anyOf,
- "dependencies": _legacy_keywords.dependencies_draft4_draft6_draft7,
- "enum": _keywords.enum,
- "format": _keywords.format,
- "items": _legacy_keywords.items_draft3_draft4,
- "maxItems": _keywords.maxItems,
- "maxLength": _keywords.maxLength,
- "maxProperties": _keywords.maxProperties,
- "maximum": _legacy_keywords.maximum_draft3_draft4,
- "minItems": _keywords.minItems,
- "minLength": _keywords.minLength,
- "minProperties": _keywords.minProperties,
- "minimum": _legacy_keywords.minimum_draft3_draft4,
- "multipleOf": _keywords.multipleOf,
- "not": _keywords.not_,
- "oneOf": _keywords.oneOf,
- "pattern": _keywords.pattern,
- "patternProperties": _keywords.patternProperties,
- "properties": _keywords.properties,
- "required": _keywords.required,
- "type": _keywords.type,
- "uniqueItems": _keywords.uniqueItems,
- },
- type_checker=_types.draft4_type_checker,
- format_checker=_format.draft4_format_checker,
- version="draft4",
- id_of=referencing.jsonschema.DRAFT4.id_of,
- applicable_validators=_legacy_keywords.ignore_ref_siblings,
- )
- Draft6Validator = create(
- meta_schema=SPECIFICATIONS.contents(
- "http://json-schema.org/draft-06/schema#",
- ),
- validators={
- "$ref": _keywords.ref,
- "additionalItems": _legacy_keywords.additionalItems,
- "additionalProperties": _keywords.additionalProperties,
- "allOf": _keywords.allOf,
- "anyOf": _keywords.anyOf,
- "const": _keywords.const,
- "contains": _legacy_keywords.contains_draft6_draft7,
- "dependencies": _legacy_keywords.dependencies_draft4_draft6_draft7,
- "enum": _keywords.enum,
- "exclusiveMaximum": _keywords.exclusiveMaximum,
- "exclusiveMinimum": _keywords.exclusiveMinimum,
- "format": _keywords.format,
- "items": _legacy_keywords.items_draft6_draft7_draft201909,
- "maxItems": _keywords.maxItems,
- "maxLength": _keywords.maxLength,
- "maxProperties": _keywords.maxProperties,
- "maximum": _keywords.maximum,
- "minItems": _keywords.minItems,
- "minLength": _keywords.minLength,
- "minProperties": _keywords.minProperties,
- "minimum": _keywords.minimum,
- "multipleOf": _keywords.multipleOf,
- "not": _keywords.not_,
- "oneOf": _keywords.oneOf,
- "pattern": _keywords.pattern,
- "patternProperties": _keywords.patternProperties,
- "properties": _keywords.properties,
- "propertyNames": _keywords.propertyNames,
- "required": _keywords.required,
- "type": _keywords.type,
- "uniqueItems": _keywords.uniqueItems,
- },
- type_checker=_types.draft6_type_checker,
- format_checker=_format.draft6_format_checker,
- version="draft6",
- id_of=referencing.jsonschema.DRAFT6.id_of,
- applicable_validators=_legacy_keywords.ignore_ref_siblings,
- )
- Draft7Validator = create(
- meta_schema=SPECIFICATIONS.contents(
- "http://json-schema.org/draft-07/schema#",
- ),
- validators={
- "$ref": _keywords.ref,
- "additionalItems": _legacy_keywords.additionalItems,
- "additionalProperties": _keywords.additionalProperties,
- "allOf": _keywords.allOf,
- "anyOf": _keywords.anyOf,
- "const": _keywords.const,
- "contains": _legacy_keywords.contains_draft6_draft7,
- "dependencies": _legacy_keywords.dependencies_draft4_draft6_draft7,
- "enum": _keywords.enum,
- "exclusiveMaximum": _keywords.exclusiveMaximum,
- "exclusiveMinimum": _keywords.exclusiveMinimum,
- "format": _keywords.format,
- "if": _keywords.if_,
- "items": _legacy_keywords.items_draft6_draft7_draft201909,
- "maxItems": _keywords.maxItems,
- "maxLength": _keywords.maxLength,
- "maxProperties": _keywords.maxProperties,
- "maximum": _keywords.maximum,
- "minItems": _keywords.minItems,
- "minLength": _keywords.minLength,
- "minProperties": _keywords.minProperties,
- "minimum": _keywords.minimum,
- "multipleOf": _keywords.multipleOf,
- "not": _keywords.not_,
- "oneOf": _keywords.oneOf,
- "pattern": _keywords.pattern,
- "patternProperties": _keywords.patternProperties,
- "properties": _keywords.properties,
- "propertyNames": _keywords.propertyNames,
- "required": _keywords.required,
- "type": _keywords.type,
- "uniqueItems": _keywords.uniqueItems,
- },
- type_checker=_types.draft7_type_checker,
- format_checker=_format.draft7_format_checker,
- version="draft7",
- id_of=referencing.jsonschema.DRAFT7.id_of,
- applicable_validators=_legacy_keywords.ignore_ref_siblings,
- )
- Draft201909Validator = create(
- meta_schema=SPECIFICATIONS.contents(
- "https://json-schema.org/draft/2019-09/schema",
- ),
- validators={
- "$recursiveRef": _legacy_keywords.recursiveRef,
- "$ref": _keywords.ref,
- "additionalItems": _legacy_keywords.additionalItems,
- "additionalProperties": _keywords.additionalProperties,
- "allOf": _keywords.allOf,
- "anyOf": _keywords.anyOf,
- "const": _keywords.const,
- "contains": _keywords.contains,
- "dependentRequired": _keywords.dependentRequired,
- "dependentSchemas": _keywords.dependentSchemas,
- "enum": _keywords.enum,
- "exclusiveMaximum": _keywords.exclusiveMaximum,
- "exclusiveMinimum": _keywords.exclusiveMinimum,
- "format": _keywords.format,
- "if": _keywords.if_,
- "items": _legacy_keywords.items_draft6_draft7_draft201909,
- "maxItems": _keywords.maxItems,
- "maxLength": _keywords.maxLength,
- "maxProperties": _keywords.maxProperties,
- "maximum": _keywords.maximum,
- "minItems": _keywords.minItems,
- "minLength": _keywords.minLength,
- "minProperties": _keywords.minProperties,
- "minimum": _keywords.minimum,
- "multipleOf": _keywords.multipleOf,
- "not": _keywords.not_,
- "oneOf": _keywords.oneOf,
- "pattern": _keywords.pattern,
- "patternProperties": _keywords.patternProperties,
- "properties": _keywords.properties,
- "propertyNames": _keywords.propertyNames,
- "required": _keywords.required,
- "type": _keywords.type,
- "unevaluatedItems": _legacy_keywords.unevaluatedItems_draft2019,
- "unevaluatedProperties": (
- _legacy_keywords.unevaluatedProperties_draft2019
- ),
- "uniqueItems": _keywords.uniqueItems,
- },
- type_checker=_types.draft201909_type_checker,
- format_checker=_format.draft201909_format_checker,
- version="draft2019-09",
- )
- Draft202012Validator = create(
- meta_schema=SPECIFICATIONS.contents(
- "https://json-schema.org/draft/2020-12/schema",
- ),
- validators={
- "$dynamicRef": _keywords.dynamicRef,
- "$ref": _keywords.ref,
- "additionalProperties": _keywords.additionalProperties,
- "allOf": _keywords.allOf,
- "anyOf": _keywords.anyOf,
- "const": _keywords.const,
- "contains": _keywords.contains,
- "dependentRequired": _keywords.dependentRequired,
- "dependentSchemas": _keywords.dependentSchemas,
- "enum": _keywords.enum,
- "exclusiveMaximum": _keywords.exclusiveMaximum,
- "exclusiveMinimum": _keywords.exclusiveMinimum,
- "format": _keywords.format,
- "if": _keywords.if_,
- "items": _keywords.items,
- "maxItems": _keywords.maxItems,
- "maxLength": _keywords.maxLength,
- "maxProperties": _keywords.maxProperties,
- "maximum": _keywords.maximum,
- "minItems": _keywords.minItems,
- "minLength": _keywords.minLength,
- "minProperties": _keywords.minProperties,
- "minimum": _keywords.minimum,
- "multipleOf": _keywords.multipleOf,
- "not": _keywords.not_,
- "oneOf": _keywords.oneOf,
- "pattern": _keywords.pattern,
- "patternProperties": _keywords.patternProperties,
- "prefixItems": _keywords.prefixItems,
- "properties": _keywords.properties,
- "propertyNames": _keywords.propertyNames,
- "required": _keywords.required,
- "type": _keywords.type,
- "unevaluatedItems": _keywords.unevaluatedItems,
- "unevaluatedProperties": _keywords.unevaluatedProperties,
- "uniqueItems": _keywords.uniqueItems,
- },
- type_checker=_types.draft202012_type_checker,
- format_checker=_format.draft202012_format_checker,
- version="draft2020-12",
- )
- _LATEST_VERSION = Draft202012Validator
- class _RefResolver:
- """
- Resolve JSON References.
- Arguments:
- base_uri (str):
- The URI of the referring document
- referrer:
- The actual referring document
- store (dict):
- A mapping from URIs to documents to cache
- cache_remote (bool):
- Whether remote refs should be cached after first resolution
- handlers (dict):
- A mapping from URI schemes to functions that should be used
- to retrieve them
- urljoin_cache (:func:`functools.lru_cache`):
- A cache that will be used for caching the results of joining
- the resolution scope to subscopes.
- remote_cache (:func:`functools.lru_cache`):
- A cache that will be used for caching the results of
- resolved remote URLs.
- Attributes:
- 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=HashTrieMap(),
- cache_remote=True,
- handlers=(),
- urljoin_cache=None,
- remote_cache=None,
- ):
- if urljoin_cache is None:
- urljoin_cache = lru_cache(1024)(urljoin)
- if remote_cache is None:
- remote_cache = lru_cache(1024)(self.resolve_from_url)
- self.referrer = referrer
- self.cache_remote = cache_remote
- self.handlers = dict(handlers)
- self._scopes_stack = [base_uri]
- 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)
- for schema in store.values()
- if isinstance(schema, Mapping) and "$id" in schema
- )
- self.store[base_uri] = referrer
- self._urljoin_cache = urljoin_cache
- self._remote_cache = remote_cache
- @classmethod
- def from_schema( # noqa: D417
- cls,
- schema,
- id_of=referencing.jsonschema.DRAFT202012.id_of,
- *args,
- **kwargs,
- ):
- """
- Construct a resolver from a JSON schema object.
- Arguments:
- schema:
- the referring schema
- Returns:
- `_RefResolver`
- """
- return cls(base_uri=id_of(schema) or "", referrer=schema, *args, **kwargs) # noqa: B026, E501
- def push_scope(self, scope):
- """
- Enter a given sub-scope.
- Treats further dereferences as being performed underneath the
- given scope.
- """
- self._scopes_stack.append(
- self._urljoin_cache(self.resolution_scope, scope),
- )
- def pop_scope(self):
- """
- Exit the most recent entered scope.
- Treats further dereferences as being performed underneath the
- original scope.
- Don't call this method more times than `push_scope` has been
- called.
- """
- try:
- self._scopes_stack.pop()
- except IndexError:
- raise exceptions._RefResolutionError(
- "Failed to pop the scope from an empty stack. "
- "`pop_scope()` should only be called once for every "
- "`push_scope()`",
- ) from None
- @property
- def resolution_scope(self):
- """
- Retrieve the current resolution scope.
- """
- return self._scopes_stack[-1]
- @property
- def base_uri(self):
- """
- Retrieve the current base URI, not including any fragment.
- """
- uri, _ = urldefrag(self.resolution_scope)
- return uri
- @contextlib.contextmanager
- def in_scope(self, scope):
- """
- Temporarily enter the given scope for the duration of the context.
- .. deprecated:: v4.0.0
- """
- warnings.warn(
- "jsonschema.RefResolver.in_scope is deprecated and will be "
- "removed in a future release.",
- DeprecationWarning,
- stacklevel=3,
- )
- self.push_scope(scope)
- try:
- yield
- finally:
- self.pop_scope()
- @contextlib.contextmanager
- def resolving(self, ref):
- """
- Resolve the given ``ref`` and enter its resolution scope.
- Exits the scope on exit of this context manager.
- Arguments:
- ref (str):
- The reference to resolve
- """
- url, resolved = self.resolve(ref)
- self.push_scope(url)
- try:
- yield resolved
- finally:
- self.pop_scope()
- def _find_in_referrer(self, key):
- return self._get_subschemas_cache()[key]
- @lru_cache # noqa: B019
- def _get_subschemas_cache(self):
- cache = {key: [] for key in _SUBSCHEMAS_KEYWORDS}
- for keyword, subschema in _search_schema(
- self.referrer, _match_subschema_keywords,
- ):
- cache[keyword].append(subschema)
- return cache
- @lru_cache # noqa: B019
- def _find_in_subschemas(self, url):
- subschemas = self._get_subschemas_cache()["$id"]
- if not subschemas:
- return None
- uri, fragment = urldefrag(url)
- for subschema in subschemas:
- id = subschema["$id"]
- if not isinstance(id, str):
- continue
- target_uri = self._urljoin_cache(self.resolution_scope, id)
- if target_uri.rstrip("/") == uri.rstrip("/"):
- if fragment:
- subschema = self.resolve_fragment(subschema, fragment)
- self.store[url] = subschema
- return url, subschema
- return None
- def resolve(self, ref):
- """
- Resolve the given reference.
- """
- url = self._urljoin_cache(self.resolution_scope, ref).rstrip("/")
- match = self._find_in_subschemas(url)
- if match is not None:
- return match
- return url, self._remote_cache(url)
- def resolve_from_url(self, url):
- """
- Resolve the given URL.
- """
- url, fragment = urldefrag(url)
- if not url:
- url = self.base_uri
- try:
- document = self.store[url]
- except KeyError:
- try:
- document = self.resolve_remote(url)
- except Exception as exc:
- raise exceptions._RefResolutionError(exc) from exc
- return self.resolve_fragment(document, fragment)
- def resolve_fragment(self, document, fragment):
- """
- Resolve a ``fragment`` within the referenced ``document``.
- Arguments:
- document:
- The referent document
- fragment (str):
- a URI fragment to resolve within it
- """
- fragment = fragment.lstrip("/")
- if not fragment:
- return document
- if document is self.referrer:
- find = self._find_in_referrer
- else:
- def find(key):
- yield from _search_schema(document, _match_keyword(key))
- for keyword in ["$anchor", "$dynamicAnchor"]:
- for subschema in find(keyword):
- if fragment == subschema[keyword]:
- return subschema
- for keyword in ["id", "$id"]:
- for subschema in find(keyword):
- if "#" + fragment == subschema[keyword]:
- return subschema
- # Resolve via path
- parts = unquote(fragment).split("/") if fragment else []
- for part in parts:
- part = part.replace("~1", "/").replace("~0", "~")
- if isinstance(document, Sequence):
- try: # noqa: SIM105
- part = int(part)
- except ValueError:
- pass
- try:
- document = document[part]
- except (TypeError, LookupError) as err:
- raise exceptions._RefResolutionError(
- f"Unresolvable JSON pointer: {fragment!r}",
- ) from err
- return document
- def resolve_remote(self, uri):
- """
- Resolve a remote ``uri``.
- If called directly, does not check the store first, but after
- retrieving the document at the specified URI it will be saved in
- the store if :attr:`cache_remote` is True.
- .. note::
- If the requests_ library is present, ``jsonschema`` will use it to
- request the remote ``uri``, so that the correct encoding is
- detected and used.
- If it isn't, or if the scheme of the ``uri`` is not ``http`` or
- ``https``, UTF-8 is assumed.
- Arguments:
- uri (str):
- The URI to resolve
- Returns:
- The retrieved document
- .. _requests: https://pypi.org/project/requests/
- """
- try:
- import requests
- except ImportError:
- requests = None
- scheme = urlsplit(uri).scheme
- if scheme in self.handlers:
- result = self.handlers[scheme](uri)
- elif scheme in ["http", "https"] and requests:
- # Requests has support for detecting the correct encoding of
- # json over http
- result = requests.get(uri).json()
- else:
- # Otherwise, pass off to urllib and assume utf-8
- with urlopen(uri) as url: # noqa: S310
- result = json.loads(url.read().decode("utf-8"))
- if self.cache_remote:
- self.store[uri] = result
- return result
- _SUBSCHEMAS_KEYWORDS = ("$id", "id", "$anchor", "$dynamicAnchor")
- def _match_keyword(keyword):
- def matcher(value):
- if keyword in value:
- yield value
- return matcher
- def _match_subschema_keywords(value):
- for keyword in _SUBSCHEMAS_KEYWORDS:
- if keyword in value:
- yield keyword, value
- def _search_schema(schema, matcher):
- """Breadth-first search routine."""
- values = deque([schema])
- while values:
- value = values.pop()
- if not isinstance(value, dict):
- continue
- yield from matcher(value)
- values.extendleft(value.values())
- def validate(instance, schema, cls=None, *args, **kwargs): # noqa: D417
- """
- Validate an instance under the given schema.
- >>> validate([2, 3, 4], {"maxItems": 2})
- Traceback (most recent call last):
- ...
- ValidationError: [2, 3, 4] is too long
- :func:`~jsonschema.validators.validate` will first verify that the
- provided schema is itself valid, since not doing so can lead to less
- obvious error messages and fail in less obvious or consistent ways.
- If you know you have a valid schema already, especially
- if you intend to validate multiple instances with
- the same schema, you likely would prefer using the
- `jsonschema.protocols.Validator.validate` method directly on a
- specific validator (e.g. ``Draft202012Validator.validate``).
- Arguments:
- instance:
- The instance to validate
- schema:
- The schema to validate with
- cls (jsonschema.protocols.Validator):
- The class that will be used to validate the instance.
- If the ``cls`` argument is not provided, two things will happen
- in accordance with the specification. First, if the schema has a
- :kw:`$schema` keyword containing a known meta-schema [#]_ then the
- proper validator will be used. The specification recommends that
- all schemas contain :kw:`$schema` properties for this reason. If no
- :kw:`$schema` property is found, the default validator class is the
- latest released draft.
- Any other provided positional and keyword arguments will be passed
- on when instantiating the ``cls``.
- Raises:
- `jsonschema.exceptions.ValidationError`:
- if the instance is invalid
- `jsonschema.exceptions.SchemaError`:
- if the schema itself is invalid
- .. rubric:: Footnotes
- .. [#] known by a validator registered with
- `jsonschema.validators.validates`
- """
- if cls is None:
- cls = validator_for(schema)
- cls.check_schema(schema)
- validator = cls(schema, *args, **kwargs)
- error = exceptions.best_match(validator.iter_errors(instance))
- if error is not None:
- raise error
- def validator_for(
- schema,
- default: Validator | _utils.Unset = _UNSET,
- ) -> type[Validator]:
- """
- Retrieve the validator class appropriate for validating the given schema.
- Uses the :kw:`$schema` keyword that should be present in the given
- schema to look up the appropriate validator class.
- Arguments:
- schema (collections.abc.Mapping or bool):
- the schema to look at
- default:
- the default to return if the appropriate validator class
- cannot be determined.
- If unprovided, the default is to return the latest supported
- draft.
- Examples:
- The :kw:`$schema` JSON Schema keyword will control which validator
- class is returned:
- >>> schema = {
- ... "$schema": "https://json-schema.org/draft/2020-12/schema",
- ... "type": "integer",
- ... }
- >>> jsonschema.validators.validator_for(schema)
- <class 'jsonschema.validators.Draft202012Validator'>
- Here, a draft 7 schema instead will return the draft 7 validator:
- >>> schema = {
- ... "$schema": "http://json-schema.org/draft-07/schema#",
- ... "type": "integer",
- ... }
- >>> jsonschema.validators.validator_for(schema)
- <class 'jsonschema.validators.Draft7Validator'>
- Schemas with no ``$schema`` keyword will fallback to the default
- argument:
- >>> schema = {"type": "integer"}
- >>> jsonschema.validators.validator_for(
- ... schema, default=Draft7Validator,
- ... )
- <class 'jsonschema.validators.Draft7Validator'>
- or if none is provided, to the latest version supported.
- Always including the keyword when authoring schemas is highly
- recommended.
- """
- DefaultValidator = _LATEST_VERSION if default is _UNSET else default
- if schema is True or schema is False or "$schema" not in schema:
- return DefaultValidator
- if schema["$schema"] not in _META_SCHEMAS and default is _UNSET:
- warn(
- (
- "The metaschema specified by $schema was not found. "
- "Using the latest draft to validate, but this will raise "
- "an error in the future."
- ),
- DeprecationWarning,
- stacklevel=2,
- )
- return _META_SCHEMAS.get(schema["$schema"], DefaultValidator)
|