Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/jsonschema/validators.py: 4%
383 statements
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-22 06:29 +0000
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-22 06:29 +0000
1"""
2Creation and extension of validators, with implementations for existing drafts.
3"""
4from __future__ import annotations
6from collections import deque
7from collections.abc import Iterable, Mapping, Sequence
8from functools import lru_cache
9from operator import methodcaller
10from typing import TYPE_CHECKING
11from urllib.parse import unquote, urldefrag, urljoin, urlsplit
12from urllib.request import urlopen
13from warnings import warn
14import contextlib
15import json
16import reprlib
17import warnings
19from attrs import define, field, fields
20from jsonschema_specifications import REGISTRY as SPECIFICATIONS
21from rpds import HashTrieMap
22import referencing.exceptions
23import referencing.jsonschema
25from jsonschema import (
26 _format,
27 _keywords,
28 _legacy_keywords,
29 _types,
30 _typing,
31 _utils,
32 exceptions,
33)
35if TYPE_CHECKING:
36 from jsonschema.protocols import Validator
38_UNSET = _utils.Unset()
40_VALIDATORS: dict[str, Validator] = {}
41_META_SCHEMAS = _utils.URIDict()
44def __getattr__(name):
45 if name == "ErrorTree":
46 warnings.warn(
47 "Importing ErrorTree from jsonschema.validators is deprecated. "
48 "Instead import it from jsonschema.exceptions.",
49 DeprecationWarning,
50 stacklevel=2,
51 )
52 from jsonschema.exceptions import ErrorTree
53 return ErrorTree
54 elif name == "validators":
55 warnings.warn(
56 "Accessing jsonschema.validators.validators is deprecated. "
57 "Use jsonschema.validators.validator_for with a given schema.",
58 DeprecationWarning,
59 stacklevel=2,
60 )
61 return _VALIDATORS
62 elif name == "meta_schemas":
63 warnings.warn(
64 "Accessing jsonschema.validators.meta_schemas is deprecated. "
65 "Use jsonschema.validators.validator_for with a given schema.",
66 DeprecationWarning,
67 stacklevel=2,
68 )
69 return _META_SCHEMAS
70 elif name == "RefResolver":
71 warnings.warn(
72 _RefResolver._DEPRECATION_MESSAGE,
73 DeprecationWarning,
74 stacklevel=2,
75 )
76 return _RefResolver
77 raise AttributeError(f"module {__name__} has no attribute {name}")
80def validates(version):
81 """
82 Register the decorated validator for a ``version`` of the specification.
84 Registered validators and their meta schemas will be considered when
85 parsing :kw:`$schema` keywords' URIs.
87 Arguments:
89 version (str):
91 An identifier to use as the version's name
93 Returns:
95 collections.abc.Callable:
97 a class decorator to decorate the validator with the version
98 """
100 def _validates(cls):
101 _VALIDATORS[version] = cls
102 meta_schema_id = cls.ID_OF(cls.META_SCHEMA)
103 _META_SCHEMAS[meta_schema_id] = cls
104 return cls
105 return _validates
108def _warn_for_remote_retrieve(uri: str):
109 from urllib.request import Request, urlopen
110 headers = {"User-Agent": "python-jsonschema (deprecated $ref resolution)"}
111 request = Request(uri, headers=headers) # noqa: S310
112 with urlopen(request) as response: # noqa: S310
113 warnings.warn(
114 "Automatically retrieving remote references can be a security "
115 "vulnerability and is discouraged by the JSON Schema "
116 "specifications. Relying on this behavior is deprecated "
117 "and will shortly become an error. If you are sure you want to "
118 "remotely retrieve your reference and that it is safe to do so, "
119 "you can find instructions for doing so via referencing.Registry "
120 "in the referencing documentation "
121 "(https://referencing.readthedocs.org).",
122 DeprecationWarning,
123 stacklevel=9, # Ha ha ha ha magic numbers :/
124 )
125 return referencing.Resource.from_contents(
126 json.load(response),
127 default_specification=referencing.jsonschema.DRAFT202012,
128 )
131_REMOTE_WARNING_REGISTRY = SPECIFICATIONS.combine(
132 referencing.Registry(retrieve=_warn_for_remote_retrieve), # type: ignore[call-arg]
133)
136def create(
137 meta_schema: referencing.jsonschema.ObjectSchema,
138 validators: (
139 Mapping[str, _typing.SchemaKeywordValidator]
140 | Iterable[tuple[str, _typing.SchemaKeywordValidator]]
141 ) = (),
142 version: str | None = None,
143 type_checker: _types.TypeChecker = _types.draft202012_type_checker,
144 format_checker: _format.FormatChecker = _format.draft202012_format_checker,
145 id_of: _typing.id_of = referencing.jsonschema.DRAFT202012.id_of,
146 applicable_validators: _typing.ApplicableValidators = methodcaller(
147 "items",
148 ),
149):
150 """
151 Create a new validator class.
153 Arguments:
155 meta_schema:
157 the meta schema for the new validator class
159 validators:
161 a mapping from names to callables, where each callable will
162 validate the schema property with the given name.
164 Each callable should take 4 arguments:
166 1. a validator instance,
167 2. the value of the property being validated within the
168 instance
169 3. the instance
170 4. the schema
172 version:
174 an identifier for the version that this validator class will
175 validate. If provided, the returned validator class will
176 have its ``__name__`` set to include the version, and also
177 will have `jsonschema.validators.validates` automatically
178 called for the given version.
180 type_checker:
182 a type checker, used when applying the :kw:`type` keyword.
184 If unprovided, a `jsonschema.TypeChecker` will be created
185 with a set of default types typical of JSON Schema drafts.
187 format_checker:
189 a format checker, used when applying the :kw:`format` keyword.
191 If unprovided, a `jsonschema.FormatChecker` will be created
192 with a set of default formats typical of JSON Schema drafts.
194 id_of:
196 A function that given a schema, returns its ID.
198 applicable_validators:
200 A function that, given a schema, returns the list of
201 applicable schema keywords and associated values
202 which will be used to validate the instance.
203 This is mostly used to support pre-draft 7 versions of JSON Schema
204 which specified behavior around ignoring keywords if they were
205 siblings of a ``$ref`` keyword. If you're not attempting to
206 implement similar behavior, you can typically ignore this argument
207 and leave it at its default.
209 Returns:
211 a new `jsonschema.protocols.Validator` class
212 """
213 # preemptively don't shadow the `Validator.format_checker` local
214 format_checker_arg = format_checker
216 specification = referencing.jsonschema.specification_with(
217 dialect_id=id_of(meta_schema) or "urn:unknown-dialect",
218 default=referencing.Specification.OPAQUE,
219 )
221 @define
222 class Validator:
224 VALIDATORS = dict(validators) # noqa: RUF012
225 META_SCHEMA = dict(meta_schema) # noqa: RUF012
226 TYPE_CHECKER = type_checker
227 FORMAT_CHECKER = format_checker_arg
228 ID_OF = staticmethod(id_of)
230 _APPLICABLE_VALIDATORS = applicable_validators
232 schema: referencing.jsonschema.Schema = field(repr=reprlib.repr)
233 _ref_resolver = field(default=None, repr=False, alias="resolver")
234 format_checker: _format.FormatChecker | None = field(default=None)
235 # TODO: include new meta-schemas added at runtime
236 _registry: referencing.jsonschema.SchemaRegistry = field(
237 default=_REMOTE_WARNING_REGISTRY,
238 kw_only=True,
239 repr=False,
240 )
241 _resolver = field(
242 alias="_resolver",
243 default=None,
244 kw_only=True,
245 repr=False,
246 )
248 def __init_subclass__(cls):
249 warnings.warn(
250 (
251 "Subclassing validator classes is not intended to "
252 "be part of their public API. A future version "
253 "will make doing so an error, as the behavior of "
254 "subclasses isn't guaranteed to stay the same "
255 "between releases of jsonschema. Instead, prefer "
256 "composition of validators, wrapping them in an object "
257 "owned entirely by the downstream library."
258 ),
259 DeprecationWarning,
260 stacklevel=2,
261 )
263 def evolve(self, **changes):
264 cls = self.__class__
265 schema = changes.setdefault("schema", self.schema)
266 NewValidator = validator_for(schema, default=cls)
268 for field in fields(cls): # noqa: F402
269 if not field.init:
270 continue
271 attr_name = field.name
272 init_name = field.alias
273 if init_name not in changes:
274 changes[init_name] = getattr(self, attr_name)
276 return NewValidator(**changes)
278 cls.evolve = evolve
280 def __attrs_post_init__(self):
281 if self._resolver is None:
282 registry = self._registry
283 if registry is not _REMOTE_WARNING_REGISTRY:
284 registry = SPECIFICATIONS.combine(registry)
285 resource = specification.create_resource(self.schema)
286 self._resolver = registry.resolver_with_root(resource)
288 # REMOVEME: Legacy ref resolution state management.
289 push_scope = getattr(self._ref_resolver, "push_scope", None)
290 if push_scope is not None:
291 id = id_of(self.schema)
292 if id is not None:
293 push_scope(id)
295 @classmethod
296 def check_schema(cls, schema, format_checker=_UNSET):
297 Validator = validator_for(cls.META_SCHEMA, default=cls)
298 if format_checker is _UNSET:
299 format_checker = Validator.FORMAT_CHECKER
300 validator = Validator(
301 schema=cls.META_SCHEMA,
302 format_checker=format_checker,
303 )
304 for error in validator.iter_errors(schema):
305 raise exceptions.SchemaError.create_from(error)
307 @property
308 def resolver(self):
309 warnings.warn(
310 (
311 f"Accessing {self.__class__.__name__}.resolver is "
312 "deprecated as of v4.18.0, in favor of the "
313 "https://github.com/python-jsonschema/referencing "
314 "library, which provides more compliant referencing "
315 "behavior as well as more flexible APIs for "
316 "customization."
317 ),
318 DeprecationWarning,
319 stacklevel=2,
320 )
321 if self._ref_resolver is None:
322 self._ref_resolver = _RefResolver.from_schema(
323 self.schema,
324 id_of=id_of,
325 )
326 return self._ref_resolver
328 def evolve(self, **changes):
329 schema = changes.setdefault("schema", self.schema)
330 NewValidator = validator_for(schema, default=self.__class__)
332 for (attr_name, init_name) in evolve_fields:
333 if init_name not in changes:
334 changes[init_name] = getattr(self, attr_name)
336 return NewValidator(**changes)
338 def iter_errors(self, instance, _schema=None):
339 if _schema is not None:
340 warnings.warn(
341 (
342 "Passing a schema to Validator.iter_errors "
343 "is deprecated and will be removed in a future "
344 "release. Call validator.evolve(schema=new_schema)."
345 "iter_errors(...) instead."
346 ),
347 DeprecationWarning,
348 stacklevel=2,
349 )
350 else:
351 _schema = self.schema
353 if _schema is True:
354 return
355 elif _schema is False:
356 yield exceptions.ValidationError(
357 f"False schema does not allow {instance!r}",
358 validator=None,
359 validator_value=None,
360 instance=instance,
361 schema=_schema,
362 )
363 return
365 for k, v in applicable_validators(_schema):
366 validator = self.VALIDATORS.get(k)
367 if validator is None:
368 continue
370 errors = validator(self, v, instance, _schema) or ()
371 for error in errors:
372 # set details if not already set by the called fn
373 error._set(
374 validator=k,
375 validator_value=v,
376 instance=instance,
377 schema=_schema,
378 type_checker=self.TYPE_CHECKER,
379 )
380 if k not in {"if", "$ref"}:
381 error.schema_path.appendleft(k)
382 yield error
384 def descend(
385 self,
386 instance,
387 schema,
388 path=None,
389 schema_path=None,
390 resolver=None,
391 ):
392 if schema is True:
393 return
394 elif schema is False:
395 yield exceptions.ValidationError(
396 f"False schema does not allow {instance!r}",
397 validator=None,
398 validator_value=None,
399 instance=instance,
400 schema=schema,
401 )
402 return
404 if self._ref_resolver is not None:
405 evolved = self.evolve(schema=schema)
406 else:
407 if resolver is None:
408 resolver = self._resolver.in_subresource(
409 specification.create_resource(schema),
410 )
411 evolved = self.evolve(schema=schema, _resolver=resolver)
413 for k, v in applicable_validators(schema):
414 validator = evolved.VALIDATORS.get(k)
415 if validator is None:
416 continue
418 errors = validator(evolved, v, instance, schema) or ()
419 for error in errors:
420 # set details if not already set by the called fn
421 error._set(
422 validator=k,
423 validator_value=v,
424 instance=instance,
425 schema=schema,
426 type_checker=evolved.TYPE_CHECKER,
427 )
428 if k not in {"if", "$ref"}:
429 error.schema_path.appendleft(k)
430 if path is not None:
431 error.path.appendleft(path)
432 if schema_path is not None:
433 error.schema_path.appendleft(schema_path)
434 yield error
436 def validate(self, *args, **kwargs):
437 for error in self.iter_errors(*args, **kwargs):
438 raise error
440 def is_type(self, instance, type):
441 try:
442 return self.TYPE_CHECKER.is_type(instance, type)
443 except exceptions.UndefinedTypeCheck:
444 exc = exceptions.UnknownType(type, instance, self.schema)
445 raise exc from None
447 def _validate_reference(self, ref, instance):
448 if self._ref_resolver is None:
449 try:
450 resolved = self._resolver.lookup(ref)
451 except referencing.exceptions.Unresolvable as err:
452 raise exceptions._WrappedReferencingError(err) from err
454 return self.descend(
455 instance,
456 resolved.contents,
457 resolver=resolved.resolver,
458 )
459 else:
460 resolve = getattr(self._ref_resolver, "resolve", None)
461 if resolve is None:
462 with self._ref_resolver.resolving(ref) as resolved:
463 return self.descend(instance, resolved)
464 else:
465 scope, resolved = resolve(ref)
466 self._ref_resolver.push_scope(scope)
468 try:
469 return list(self.descend(instance, resolved))
470 finally:
471 self._ref_resolver.pop_scope()
473 def is_valid(self, instance, _schema=None):
474 if _schema is not None:
475 warnings.warn(
476 (
477 "Passing a schema to Validator.is_valid is deprecated "
478 "and will be removed in a future release. Call "
479 "validator.evolve(schema=new_schema).is_valid(...) "
480 "instead."
481 ),
482 DeprecationWarning,
483 stacklevel=2,
484 )
485 self = self.evolve(schema=_schema)
487 error = next(self.iter_errors(instance), None)
488 return error is None
490 evolve_fields = [
491 (field.name, field.alias)
492 for field in fields(Validator)
493 if field.init
494 ]
496 if version is not None:
497 safe = version.title().replace(" ", "").replace("-", "")
498 Validator.__name__ = Validator.__qualname__ = f"{safe}Validator"
499 Validator = validates(version)(Validator) # type: ignore[misc]
501 return Validator
504def extend(
505 validator,
506 validators=(),
507 version=None,
508 type_checker=None,
509 format_checker=None,
510):
511 """
512 Create a new validator class by extending an existing one.
514 Arguments:
516 validator (jsonschema.protocols.Validator):
518 an existing validator class
520 validators (collections.abc.Mapping):
522 a mapping of new validator callables to extend with, whose
523 structure is as in `create`.
525 .. note::
527 Any validator callables with the same name as an
528 existing one will (silently) replace the old validator
529 callable entirely, effectively overriding any validation
530 done in the "parent" validator class.
532 If you wish to instead extend the behavior of a parent's
533 validator callable, delegate and call it directly in
534 the new validator function by retrieving it using
535 ``OldValidator.VALIDATORS["validation_keyword_name"]``.
537 version (str):
539 a version for the new validator class
541 type_checker (jsonschema.TypeChecker):
543 a type checker, used when applying the :kw:`type` keyword.
545 If unprovided, the type checker of the extended
546 `jsonschema.protocols.Validator` will be carried along.
548 format_checker (jsonschema.FormatChecker):
550 a format checker, used when applying the :kw:`format` keyword.
552 If unprovided, the format checker of the extended
553 `jsonschema.protocols.Validator` will be carried along.
555 Returns:
557 a new `jsonschema.protocols.Validator` class extending the one
558 provided
560 .. note:: Meta Schemas
562 The new validator class will have its parent's meta schema.
564 If you wish to change or extend the meta schema in the new
565 validator class, modify ``META_SCHEMA`` directly on the returned
566 class. Note that no implicit copying is done, so a copy should
567 likely be made before modifying it, in order to not affect the
568 old validator.
569 """
570 all_validators = dict(validator.VALIDATORS)
571 all_validators.update(validators)
573 if type_checker is None:
574 type_checker = validator.TYPE_CHECKER
575 if format_checker is None:
576 format_checker = validator.FORMAT_CHECKER
577 return create(
578 meta_schema=validator.META_SCHEMA,
579 validators=all_validators,
580 version=version,
581 type_checker=type_checker,
582 format_checker=format_checker,
583 id_of=validator.ID_OF,
584 applicable_validators=validator._APPLICABLE_VALIDATORS,
585 )
588Draft3Validator = create(
589 meta_schema=SPECIFICATIONS.contents(
590 "http://json-schema.org/draft-03/schema#",
591 ),
592 validators={
593 "$ref": _keywords.ref,
594 "additionalItems": _legacy_keywords.additionalItems,
595 "additionalProperties": _keywords.additionalProperties,
596 "dependencies": _legacy_keywords.dependencies_draft3,
597 "disallow": _legacy_keywords.disallow_draft3,
598 "divisibleBy": _keywords.multipleOf,
599 "enum": _keywords.enum,
600 "extends": _legacy_keywords.extends_draft3,
601 "format": _keywords.format,
602 "items": _legacy_keywords.items_draft3_draft4,
603 "maxItems": _keywords.maxItems,
604 "maxLength": _keywords.maxLength,
605 "maximum": _legacy_keywords.maximum_draft3_draft4,
606 "minItems": _keywords.minItems,
607 "minLength": _keywords.minLength,
608 "minimum": _legacy_keywords.minimum_draft3_draft4,
609 "pattern": _keywords.pattern,
610 "patternProperties": _keywords.patternProperties,
611 "properties": _legacy_keywords.properties_draft3,
612 "type": _legacy_keywords.type_draft3,
613 "uniqueItems": _keywords.uniqueItems,
614 },
615 type_checker=_types.draft3_type_checker,
616 format_checker=_format.draft3_format_checker,
617 version="draft3",
618 id_of=referencing.jsonschema.DRAFT3.id_of,
619 applicable_validators=_legacy_keywords.ignore_ref_siblings,
620)
622Draft4Validator = create(
623 meta_schema=SPECIFICATIONS.contents(
624 "http://json-schema.org/draft-04/schema#",
625 ),
626 validators={
627 "$ref": _keywords.ref,
628 "additionalItems": _legacy_keywords.additionalItems,
629 "additionalProperties": _keywords.additionalProperties,
630 "allOf": _keywords.allOf,
631 "anyOf": _keywords.anyOf,
632 "dependencies": _legacy_keywords.dependencies_draft4_draft6_draft7,
633 "enum": _keywords.enum,
634 "format": _keywords.format,
635 "items": _legacy_keywords.items_draft3_draft4,
636 "maxItems": _keywords.maxItems,
637 "maxLength": _keywords.maxLength,
638 "maxProperties": _keywords.maxProperties,
639 "maximum": _legacy_keywords.maximum_draft3_draft4,
640 "minItems": _keywords.minItems,
641 "minLength": _keywords.minLength,
642 "minProperties": _keywords.minProperties,
643 "minimum": _legacy_keywords.minimum_draft3_draft4,
644 "multipleOf": _keywords.multipleOf,
645 "not": _keywords.not_,
646 "oneOf": _keywords.oneOf,
647 "pattern": _keywords.pattern,
648 "patternProperties": _keywords.patternProperties,
649 "properties": _keywords.properties,
650 "required": _keywords.required,
651 "type": _keywords.type,
652 "uniqueItems": _keywords.uniqueItems,
653 },
654 type_checker=_types.draft4_type_checker,
655 format_checker=_format.draft4_format_checker,
656 version="draft4",
657 id_of=referencing.jsonschema.DRAFT4.id_of,
658 applicable_validators=_legacy_keywords.ignore_ref_siblings,
659)
661Draft6Validator = create(
662 meta_schema=SPECIFICATIONS.contents(
663 "http://json-schema.org/draft-06/schema#",
664 ),
665 validators={
666 "$ref": _keywords.ref,
667 "additionalItems": _legacy_keywords.additionalItems,
668 "additionalProperties": _keywords.additionalProperties,
669 "allOf": _keywords.allOf,
670 "anyOf": _keywords.anyOf,
671 "const": _keywords.const,
672 "contains": _legacy_keywords.contains_draft6_draft7,
673 "dependencies": _legacy_keywords.dependencies_draft4_draft6_draft7,
674 "enum": _keywords.enum,
675 "exclusiveMaximum": _keywords.exclusiveMaximum,
676 "exclusiveMinimum": _keywords.exclusiveMinimum,
677 "format": _keywords.format,
678 "items": _legacy_keywords.items_draft6_draft7_draft201909,
679 "maxItems": _keywords.maxItems,
680 "maxLength": _keywords.maxLength,
681 "maxProperties": _keywords.maxProperties,
682 "maximum": _keywords.maximum,
683 "minItems": _keywords.minItems,
684 "minLength": _keywords.minLength,
685 "minProperties": _keywords.minProperties,
686 "minimum": _keywords.minimum,
687 "multipleOf": _keywords.multipleOf,
688 "not": _keywords.not_,
689 "oneOf": _keywords.oneOf,
690 "pattern": _keywords.pattern,
691 "patternProperties": _keywords.patternProperties,
692 "properties": _keywords.properties,
693 "propertyNames": _keywords.propertyNames,
694 "required": _keywords.required,
695 "type": _keywords.type,
696 "uniqueItems": _keywords.uniqueItems,
697 },
698 type_checker=_types.draft6_type_checker,
699 format_checker=_format.draft6_format_checker,
700 version="draft6",
701 id_of=referencing.jsonschema.DRAFT6.id_of,
702 applicable_validators=_legacy_keywords.ignore_ref_siblings,
703)
705Draft7Validator = create(
706 meta_schema=SPECIFICATIONS.contents(
707 "http://json-schema.org/draft-07/schema#",
708 ),
709 validators={
710 "$ref": _keywords.ref,
711 "additionalItems": _legacy_keywords.additionalItems,
712 "additionalProperties": _keywords.additionalProperties,
713 "allOf": _keywords.allOf,
714 "anyOf": _keywords.anyOf,
715 "const": _keywords.const,
716 "contains": _legacy_keywords.contains_draft6_draft7,
717 "dependencies": _legacy_keywords.dependencies_draft4_draft6_draft7,
718 "enum": _keywords.enum,
719 "exclusiveMaximum": _keywords.exclusiveMaximum,
720 "exclusiveMinimum": _keywords.exclusiveMinimum,
721 "format": _keywords.format,
722 "if": _keywords.if_,
723 "items": _legacy_keywords.items_draft6_draft7_draft201909,
724 "maxItems": _keywords.maxItems,
725 "maxLength": _keywords.maxLength,
726 "maxProperties": _keywords.maxProperties,
727 "maximum": _keywords.maximum,
728 "minItems": _keywords.minItems,
729 "minLength": _keywords.minLength,
730 "minProperties": _keywords.minProperties,
731 "minimum": _keywords.minimum,
732 "multipleOf": _keywords.multipleOf,
733 "not": _keywords.not_,
734 "oneOf": _keywords.oneOf,
735 "pattern": _keywords.pattern,
736 "patternProperties": _keywords.patternProperties,
737 "properties": _keywords.properties,
738 "propertyNames": _keywords.propertyNames,
739 "required": _keywords.required,
740 "type": _keywords.type,
741 "uniqueItems": _keywords.uniqueItems,
742 },
743 type_checker=_types.draft7_type_checker,
744 format_checker=_format.draft7_format_checker,
745 version="draft7",
746 id_of=referencing.jsonschema.DRAFT7.id_of,
747 applicable_validators=_legacy_keywords.ignore_ref_siblings,
748)
750Draft201909Validator = create(
751 meta_schema=SPECIFICATIONS.contents(
752 "https://json-schema.org/draft/2019-09/schema",
753 ),
754 validators={
755 "$recursiveRef": _legacy_keywords.recursiveRef,
756 "$ref": _keywords.ref,
757 "additionalItems": _legacy_keywords.additionalItems,
758 "additionalProperties": _keywords.additionalProperties,
759 "allOf": _keywords.allOf,
760 "anyOf": _keywords.anyOf,
761 "const": _keywords.const,
762 "contains": _keywords.contains,
763 "dependentRequired": _keywords.dependentRequired,
764 "dependentSchemas": _keywords.dependentSchemas,
765 "enum": _keywords.enum,
766 "exclusiveMaximum": _keywords.exclusiveMaximum,
767 "exclusiveMinimum": _keywords.exclusiveMinimum,
768 "format": _keywords.format,
769 "if": _keywords.if_,
770 "items": _legacy_keywords.items_draft6_draft7_draft201909,
771 "maxItems": _keywords.maxItems,
772 "maxLength": _keywords.maxLength,
773 "maxProperties": _keywords.maxProperties,
774 "maximum": _keywords.maximum,
775 "minItems": _keywords.minItems,
776 "minLength": _keywords.minLength,
777 "minProperties": _keywords.minProperties,
778 "minimum": _keywords.minimum,
779 "multipleOf": _keywords.multipleOf,
780 "not": _keywords.not_,
781 "oneOf": _keywords.oneOf,
782 "pattern": _keywords.pattern,
783 "patternProperties": _keywords.patternProperties,
784 "properties": _keywords.properties,
785 "propertyNames": _keywords.propertyNames,
786 "required": _keywords.required,
787 "type": _keywords.type,
788 "unevaluatedItems": _legacy_keywords.unevaluatedItems_draft2019,
789 "unevaluatedProperties": (
790 _legacy_keywords.unevaluatedProperties_draft2019
791 ),
792 "uniqueItems": _keywords.uniqueItems,
793 },
794 type_checker=_types.draft201909_type_checker,
795 format_checker=_format.draft201909_format_checker,
796 version="draft2019-09",
797)
799Draft202012Validator = create(
800 meta_schema=SPECIFICATIONS.contents(
801 "https://json-schema.org/draft/2020-12/schema",
802 ),
803 validators={
804 "$dynamicRef": _keywords.dynamicRef,
805 "$ref": _keywords.ref,
806 "additionalProperties": _keywords.additionalProperties,
807 "allOf": _keywords.allOf,
808 "anyOf": _keywords.anyOf,
809 "const": _keywords.const,
810 "contains": _keywords.contains,
811 "dependentRequired": _keywords.dependentRequired,
812 "dependentSchemas": _keywords.dependentSchemas,
813 "enum": _keywords.enum,
814 "exclusiveMaximum": _keywords.exclusiveMaximum,
815 "exclusiveMinimum": _keywords.exclusiveMinimum,
816 "format": _keywords.format,
817 "if": _keywords.if_,
818 "items": _keywords.items,
819 "maxItems": _keywords.maxItems,
820 "maxLength": _keywords.maxLength,
821 "maxProperties": _keywords.maxProperties,
822 "maximum": _keywords.maximum,
823 "minItems": _keywords.minItems,
824 "minLength": _keywords.minLength,
825 "minProperties": _keywords.minProperties,
826 "minimum": _keywords.minimum,
827 "multipleOf": _keywords.multipleOf,
828 "not": _keywords.not_,
829 "oneOf": _keywords.oneOf,
830 "pattern": _keywords.pattern,
831 "patternProperties": _keywords.patternProperties,
832 "prefixItems": _keywords.prefixItems,
833 "properties": _keywords.properties,
834 "propertyNames": _keywords.propertyNames,
835 "required": _keywords.required,
836 "type": _keywords.type,
837 "unevaluatedItems": _keywords.unevaluatedItems,
838 "unevaluatedProperties": _keywords.unevaluatedProperties,
839 "uniqueItems": _keywords.uniqueItems,
840 },
841 type_checker=_types.draft202012_type_checker,
842 format_checker=_format.draft202012_format_checker,
843 version="draft2020-12",
844)
846_LATEST_VERSION = Draft202012Validator
849class _RefResolver:
850 """
851 Resolve JSON References.
853 Arguments:
855 base_uri (str):
857 The URI of the referring document
859 referrer:
861 The actual referring document
863 store (dict):
865 A mapping from URIs to documents to cache
867 cache_remote (bool):
869 Whether remote refs should be cached after first resolution
871 handlers (dict):
873 A mapping from URI schemes to functions that should be used
874 to retrieve them
876 urljoin_cache (:func:`functools.lru_cache`):
878 A cache that will be used for caching the results of joining
879 the resolution scope to subscopes.
881 remote_cache (:func:`functools.lru_cache`):
883 A cache that will be used for caching the results of
884 resolved remote URLs.
886 Attributes:
888 cache_remote (bool):
890 Whether remote refs should be cached after first resolution
892 .. deprecated:: v4.18.0
894 ``RefResolver`` has been deprecated in favor of `referencing`.
895 """
897 _DEPRECATION_MESSAGE = (
898 "jsonschema.RefResolver is deprecated as of v4.18.0, in favor of the "
899 "https://github.com/python-jsonschema/referencing library, which "
900 "provides more compliant referencing behavior as well as more "
901 "flexible APIs for customization. A future release will remove "
902 "RefResolver. Please file a feature request (on referencing) if you "
903 "are missing an API for the kind of customization you need."
904 )
906 def __init__(
907 self,
908 base_uri,
909 referrer,
910 store=HashTrieMap(),
911 cache_remote=True,
912 handlers=(),
913 urljoin_cache=None,
914 remote_cache=None,
915 ):
916 if urljoin_cache is None:
917 urljoin_cache = lru_cache(1024)(urljoin)
918 if remote_cache is None:
919 remote_cache = lru_cache(1024)(self.resolve_from_url)
921 self.referrer = referrer
922 self.cache_remote = cache_remote
923 self.handlers = dict(handlers)
925 self._scopes_stack = [base_uri]
927 self.store = _utils.URIDict(
928 (uri, each.contents) for uri, each in SPECIFICATIONS.items()
929 )
930 self.store.update(
931 (id, each.META_SCHEMA) for id, each in _META_SCHEMAS.items()
932 )
933 self.store.update(store)
934 self.store.update(
935 (schema["$id"], schema)
936 for schema in store.values()
937 if isinstance(schema, Mapping) and "$id" in schema
938 )
939 self.store[base_uri] = referrer
941 self._urljoin_cache = urljoin_cache
942 self._remote_cache = remote_cache
944 @classmethod
945 def from_schema( # noqa: D417
946 cls,
947 schema,
948 id_of=referencing.jsonschema.DRAFT202012.id_of,
949 *args,
950 **kwargs,
951 ):
952 """
953 Construct a resolver from a JSON schema object.
955 Arguments:
957 schema:
959 the referring schema
961 Returns:
963 `_RefResolver`
964 """
965 return cls(base_uri=id_of(schema) or "", referrer=schema, *args, **kwargs) # noqa: B026, E501
967 def push_scope(self, scope):
968 """
969 Enter a given sub-scope.
971 Treats further dereferences as being performed underneath the
972 given scope.
973 """
974 self._scopes_stack.append(
975 self._urljoin_cache(self.resolution_scope, scope),
976 )
978 def pop_scope(self):
979 """
980 Exit the most recent entered scope.
982 Treats further dereferences as being performed underneath the
983 original scope.
985 Don't call this method more times than `push_scope` has been
986 called.
987 """
988 try:
989 self._scopes_stack.pop()
990 except IndexError:
991 raise exceptions._RefResolutionError(
992 "Failed to pop the scope from an empty stack. "
993 "`pop_scope()` should only be called once for every "
994 "`push_scope()`",
995 ) from None
997 @property
998 def resolution_scope(self):
999 """
1000 Retrieve the current resolution scope.
1001 """
1002 return self._scopes_stack[-1]
1004 @property
1005 def base_uri(self):
1006 """
1007 Retrieve the current base URI, not including any fragment.
1008 """
1009 uri, _ = urldefrag(self.resolution_scope)
1010 return uri
1012 @contextlib.contextmanager
1013 def in_scope(self, scope):
1014 """
1015 Temporarily enter the given scope for the duration of the context.
1017 .. deprecated:: v4.0.0
1018 """
1019 warnings.warn(
1020 "jsonschema.RefResolver.in_scope is deprecated and will be "
1021 "removed in a future release.",
1022 DeprecationWarning,
1023 stacklevel=3,
1024 )
1025 self.push_scope(scope)
1026 try:
1027 yield
1028 finally:
1029 self.pop_scope()
1031 @contextlib.contextmanager
1032 def resolving(self, ref):
1033 """
1034 Resolve the given ``ref`` and enter its resolution scope.
1036 Exits the scope on exit of this context manager.
1038 Arguments:
1040 ref (str):
1042 The reference to resolve
1043 """
1044 url, resolved = self.resolve(ref)
1045 self.push_scope(url)
1046 try:
1047 yield resolved
1048 finally:
1049 self.pop_scope()
1051 def _find_in_referrer(self, key):
1052 return self._get_subschemas_cache()[key]
1054 @lru_cache # noqa: B019
1055 def _get_subschemas_cache(self):
1056 cache = {key: [] for key in _SUBSCHEMAS_KEYWORDS}
1057 for keyword, subschema in _search_schema(
1058 self.referrer, _match_subschema_keywords,
1059 ):
1060 cache[keyword].append(subschema)
1061 return cache
1063 @lru_cache # noqa: B019
1064 def _find_in_subschemas(self, url):
1065 subschemas = self._get_subschemas_cache()["$id"]
1066 if not subschemas:
1067 return None
1068 uri, fragment = urldefrag(url)
1069 for subschema in subschemas:
1070 id = subschema["$id"]
1071 if not isinstance(id, str):
1072 continue
1073 target_uri = self._urljoin_cache(self.resolution_scope, id)
1074 if target_uri.rstrip("/") == uri.rstrip("/"):
1075 if fragment:
1076 subschema = self.resolve_fragment(subschema, fragment)
1077 self.store[url] = subschema
1078 return url, subschema
1079 return None
1081 def resolve(self, ref):
1082 """
1083 Resolve the given reference.
1084 """
1085 url = self._urljoin_cache(self.resolution_scope, ref).rstrip("/")
1087 match = self._find_in_subschemas(url)
1088 if match is not None:
1089 return match
1091 return url, self._remote_cache(url)
1093 def resolve_from_url(self, url):
1094 """
1095 Resolve the given URL.
1096 """
1097 url, fragment = urldefrag(url)
1098 if not url:
1099 url = self.base_uri
1101 try:
1102 document = self.store[url]
1103 except KeyError:
1104 try:
1105 document = self.resolve_remote(url)
1106 except Exception as exc: # noqa: BLE001
1107 raise exceptions._RefResolutionError(exc) from exc
1109 return self.resolve_fragment(document, fragment)
1111 def resolve_fragment(self, document, fragment):
1112 """
1113 Resolve a ``fragment`` within the referenced ``document``.
1115 Arguments:
1117 document:
1119 The referent document
1121 fragment (str):
1123 a URI fragment to resolve within it
1124 """
1125 fragment = fragment.lstrip("/")
1127 if not fragment:
1128 return document
1130 if document is self.referrer:
1131 find = self._find_in_referrer
1132 else:
1134 def find(key):
1135 yield from _search_schema(document, _match_keyword(key))
1137 for keyword in ["$anchor", "$dynamicAnchor"]:
1138 for subschema in find(keyword):
1139 if fragment == subschema[keyword]:
1140 return subschema
1141 for keyword in ["id", "$id"]:
1142 for subschema in find(keyword):
1143 if "#" + fragment == subschema[keyword]:
1144 return subschema
1146 # Resolve via path
1147 parts = unquote(fragment).split("/") if fragment else []
1148 for part in parts:
1149 part = part.replace("~1", "/").replace("~0", "~")
1151 if isinstance(document, Sequence):
1152 try: # noqa: SIM105
1153 part = int(part)
1154 except ValueError:
1155 pass
1156 try:
1157 document = document[part]
1158 except (TypeError, LookupError) as err:
1159 raise exceptions._RefResolutionError(
1160 f"Unresolvable JSON pointer: {fragment!r}",
1161 ) from err
1163 return document
1165 def resolve_remote(self, uri):
1166 """
1167 Resolve a remote ``uri``.
1169 If called directly, does not check the store first, but after
1170 retrieving the document at the specified URI it will be saved in
1171 the store if :attr:`cache_remote` is True.
1173 .. note::
1175 If the requests_ library is present, ``jsonschema`` will use it to
1176 request the remote ``uri``, so that the correct encoding is
1177 detected and used.
1179 If it isn't, or if the scheme of the ``uri`` is not ``http`` or
1180 ``https``, UTF-8 is assumed.
1182 Arguments:
1184 uri (str):
1186 The URI to resolve
1188 Returns:
1190 The retrieved document
1192 .. _requests: https://pypi.org/project/requests/
1193 """
1194 try:
1195 import requests
1196 except ImportError:
1197 requests = None
1199 scheme = urlsplit(uri).scheme
1201 if scheme in self.handlers:
1202 result = self.handlers[scheme](uri)
1203 elif scheme in ["http", "https"] and requests:
1204 # Requests has support for detecting the correct encoding of
1205 # json over http
1206 result = requests.get(uri).json()
1207 else:
1208 # Otherwise, pass off to urllib and assume utf-8
1209 with urlopen(uri) as url: # noqa: S310
1210 result = json.loads(url.read().decode("utf-8"))
1212 if self.cache_remote:
1213 self.store[uri] = result
1214 return result
1217_SUBSCHEMAS_KEYWORDS = ("$id", "id", "$anchor", "$dynamicAnchor")
1220def _match_keyword(keyword):
1222 def matcher(value):
1223 if keyword in value:
1224 yield value
1226 return matcher
1229def _match_subschema_keywords(value):
1230 for keyword in _SUBSCHEMAS_KEYWORDS:
1231 if keyword in value:
1232 yield keyword, value
1235def _search_schema(schema, matcher):
1236 """Breadth-first search routine."""
1237 values = deque([schema])
1238 while values:
1239 value = values.pop()
1240 if not isinstance(value, dict):
1241 continue
1242 yield from matcher(value)
1243 values.extendleft(value.values())
1246def validate(instance, schema, cls=None, *args, **kwargs): # noqa: D417
1247 """
1248 Validate an instance under the given schema.
1250 >>> validate([2, 3, 4], {"maxItems": 2})
1251 Traceback (most recent call last):
1252 ...
1253 ValidationError: [2, 3, 4] is too long
1255 :func:`~jsonschema.validators.validate` will first verify that the
1256 provided schema is itself valid, since not doing so can lead to less
1257 obvious error messages and fail in less obvious or consistent ways.
1259 If you know you have a valid schema already, especially
1260 if you intend to validate multiple instances with
1261 the same schema, you likely would prefer using the
1262 `jsonschema.protocols.Validator.validate` method directly on a
1263 specific validator (e.g. ``Draft202012Validator.validate``).
1266 Arguments:
1268 instance:
1270 The instance to validate
1272 schema:
1274 The schema to validate with
1276 cls (jsonschema.protocols.Validator):
1278 The class that will be used to validate the instance.
1280 If the ``cls`` argument is not provided, two things will happen
1281 in accordance with the specification. First, if the schema has a
1282 :kw:`$schema` keyword containing a known meta-schema [#]_ then the
1283 proper validator will be used. The specification recommends that
1284 all schemas contain :kw:`$schema` properties for this reason. If no
1285 :kw:`$schema` property is found, the default validator class is the
1286 latest released draft.
1288 Any other provided positional and keyword arguments will be passed
1289 on when instantiating the ``cls``.
1291 Raises:
1293 `jsonschema.exceptions.ValidationError`:
1295 if the instance is invalid
1297 `jsonschema.exceptions.SchemaError`:
1299 if the schema itself is invalid
1301 .. rubric:: Footnotes
1302 .. [#] known by a validator registered with
1303 `jsonschema.validators.validates`
1304 """
1305 if cls is None:
1306 cls = validator_for(schema)
1308 cls.check_schema(schema)
1309 validator = cls(schema, *args, **kwargs)
1310 error = exceptions.best_match(validator.iter_errors(instance))
1311 if error is not None:
1312 raise error
1315def validator_for(schema, default=_UNSET) -> Validator:
1316 """
1317 Retrieve the validator class appropriate for validating the given schema.
1319 Uses the :kw:`$schema` keyword that should be present in the given
1320 schema to look up the appropriate validator class.
1322 Arguments:
1324 schema (collections.abc.Mapping or bool):
1326 the schema to look at
1328 default:
1330 the default to return if the appropriate validator class
1331 cannot be determined.
1333 If unprovided, the default is to return the latest supported
1334 draft.
1336 Examples:
1338 The :kw:`$schema` JSON Schema keyword will control which validator
1339 class is returned:
1341 >>> schema = {
1342 ... "$schema": "https://json-schema.org/draft/2020-12/schema",
1343 ... "type": "integer",
1344 ... }
1345 >>> jsonschema.validators.validator_for(schema)
1346 <class 'jsonschema.validators.Draft202012Validator'>
1349 Here, a draft 7 schema instead will return the draft 7 validator:
1351 >>> schema = {
1352 ... "$schema": "http://json-schema.org/draft-07/schema#",
1353 ... "type": "integer",
1354 ... }
1355 >>> jsonschema.validators.validator_for(schema)
1356 <class 'jsonschema.validators.Draft7Validator'>
1359 Schemas with no ``$schema`` keyword will fallback to the default
1360 argument:
1362 >>> schema = {"type": "integer"}
1363 >>> jsonschema.validators.validator_for(
1364 ... schema, default=Draft7Validator,
1365 ... )
1366 <class 'jsonschema.validators.Draft7Validator'>
1368 or if none is provided, to the latest version supported.
1369 Always including the keyword when authoring schemas is highly
1370 recommended.
1372 """
1373 DefaultValidator = _LATEST_VERSION if default is _UNSET else default
1375 if schema is True or schema is False or "$schema" not in schema:
1376 return DefaultValidator
1377 if schema["$schema"] not in _META_SCHEMAS and default is _UNSET:
1378 warn(
1379 (
1380 "The metaschema specified by $schema was not found. "
1381 "Using the latest draft to validate, but this will raise "
1382 "an error in the future."
1383 ),
1384 DeprecationWarning,
1385 stacklevel=2,
1386 )
1387 return _META_SCHEMAS.get(schema["$schema"], DefaultValidator)