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