Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/jsonschema/validators.py: 48%
380 statements
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-25 06:30 +0000
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-25 06:30 +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] # noqa: E501
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": _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": _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": _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": _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": _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": _keywords.unevaluatedProperties,
786 "uniqueItems": _keywords.uniqueItems,
787 },
788 type_checker=_types.draft201909_type_checker,
789 format_checker=_format.draft201909_format_checker,
790 version="draft2019-09",
791)
793Draft202012Validator = create(
794 meta_schema=SPECIFICATIONS.contents(
795 "https://json-schema.org/draft/2020-12/schema",
796 ),
797 validators={
798 "$dynamicRef": _keywords.dynamicRef,
799 "$ref": _keywords.ref,
800 "additionalItems": _keywords.additionalItems,
801 "additionalProperties": _keywords.additionalProperties,
802 "allOf": _keywords.allOf,
803 "anyOf": _keywords.anyOf,
804 "const": _keywords.const,
805 "contains": _keywords.contains,
806 "dependentRequired": _keywords.dependentRequired,
807 "dependentSchemas": _keywords.dependentSchemas,
808 "enum": _keywords.enum,
809 "exclusiveMaximum": _keywords.exclusiveMaximum,
810 "exclusiveMinimum": _keywords.exclusiveMinimum,
811 "format": _keywords.format,
812 "if": _keywords.if_,
813 "items": _keywords.items,
814 "maxItems": _keywords.maxItems,
815 "maxLength": _keywords.maxLength,
816 "maxProperties": _keywords.maxProperties,
817 "maximum": _keywords.maximum,
818 "minItems": _keywords.minItems,
819 "minLength": _keywords.minLength,
820 "minProperties": _keywords.minProperties,
821 "minimum": _keywords.minimum,
822 "multipleOf": _keywords.multipleOf,
823 "not": _keywords.not_,
824 "oneOf": _keywords.oneOf,
825 "pattern": _keywords.pattern,
826 "patternProperties": _keywords.patternProperties,
827 "prefixItems": _keywords.prefixItems,
828 "properties": _keywords.properties,
829 "propertyNames": _keywords.propertyNames,
830 "required": _keywords.required,
831 "type": _keywords.type,
832 "unevaluatedItems": _keywords.unevaluatedItems,
833 "unevaluatedProperties": _keywords.unevaluatedProperties,
834 "uniqueItems": _keywords.uniqueItems,
835 },
836 type_checker=_types.draft202012_type_checker,
837 format_checker=_format.draft202012_format_checker,
838 version="draft2020-12",
839)
841_LATEST_VERSION = Draft202012Validator
844class _RefResolver:
845 """
846 Resolve JSON References.
848 Arguments:
850 base_uri (str):
852 The URI of the referring document
854 referrer:
856 The actual referring document
858 store (dict):
860 A mapping from URIs to documents to cache
862 cache_remote (bool):
864 Whether remote refs should be cached after first resolution
866 handlers (dict):
868 A mapping from URI schemes to functions that should be used
869 to retrieve them
871 urljoin_cache (:func:`functools.lru_cache`):
873 A cache that will be used for caching the results of joining
874 the resolution scope to subscopes.
876 remote_cache (:func:`functools.lru_cache`):
878 A cache that will be used for caching the results of
879 resolved remote URLs.
881 Attributes:
883 cache_remote (bool):
885 Whether remote refs should be cached after first resolution
887 .. deprecated:: v4.18.0
889 ``RefResolver`` has been deprecated in favor of `referencing`.
890 """
892 _DEPRECATION_MESSAGE = (
893 "jsonschema.RefResolver is deprecated as of v4.18.0, in favor of the "
894 "https://github.com/python-jsonschema/referencing library, which "
895 "provides more compliant referencing behavior as well as more "
896 "flexible APIs for customization. A future release will remove "
897 "RefResolver. Please file a feature request (on referencing) if you "
898 "are missing an API for the kind of customization you need."
899 )
901 def __init__(
902 self,
903 base_uri,
904 referrer,
905 store=HashTrieMap(),
906 cache_remote=True,
907 handlers=(),
908 urljoin_cache=None,
909 remote_cache=None,
910 ):
911 if urljoin_cache is None:
912 urljoin_cache = lru_cache(1024)(urljoin)
913 if remote_cache is None:
914 remote_cache = lru_cache(1024)(self.resolve_from_url)
916 self.referrer = referrer
917 self.cache_remote = cache_remote
918 self.handlers = dict(handlers)
920 self._scopes_stack = [base_uri]
922 self.store = _utils.URIDict(
923 (uri, each.contents) for uri, each in SPECIFICATIONS.items()
924 )
925 self.store.update(
926 (id, each.META_SCHEMA) for id, each in _META_SCHEMAS.items()
927 )
928 self.store.update(store)
929 self.store.update(
930 (schema["$id"], schema)
931 for schema in store.values()
932 if isinstance(schema, Mapping) and "$id" in schema
933 )
934 self.store[base_uri] = referrer
936 self._urljoin_cache = urljoin_cache
937 self._remote_cache = remote_cache
939 @classmethod
940 def from_schema( # noqa: D417
941 cls,
942 schema,
943 id_of=referencing.jsonschema.DRAFT202012.id_of,
944 *args,
945 **kwargs,
946 ):
947 """
948 Construct a resolver from a JSON schema object.
950 Arguments:
952 schema:
954 the referring schema
956 Returns:
958 `_RefResolver`
959 """
960 return cls(base_uri=id_of(schema) or "", referrer=schema, *args, **kwargs) # noqa: B026, E501
962 def push_scope(self, scope):
963 """
964 Enter a given sub-scope.
966 Treats further dereferences as being performed underneath the
967 given scope.
968 """
969 self._scopes_stack.append(
970 self._urljoin_cache(self.resolution_scope, scope),
971 )
973 def pop_scope(self):
974 """
975 Exit the most recent entered scope.
977 Treats further dereferences as being performed underneath the
978 original scope.
980 Don't call this method more times than `push_scope` has been
981 called.
982 """
983 try:
984 self._scopes_stack.pop()
985 except IndexError:
986 raise exceptions._RefResolutionError(
987 "Failed to pop the scope from an empty stack. "
988 "`pop_scope()` should only be called once for every "
989 "`push_scope()`",
990 )
992 @property
993 def resolution_scope(self):
994 """
995 Retrieve the current resolution scope.
996 """
997 return self._scopes_stack[-1]
999 @property
1000 def base_uri(self):
1001 """
1002 Retrieve the current base URI, not including any fragment.
1003 """
1004 uri, _ = urldefrag(self.resolution_scope)
1005 return uri
1007 @contextlib.contextmanager
1008 def in_scope(self, scope):
1009 """
1010 Temporarily enter the given scope for the duration of the context.
1012 .. deprecated:: v4.0.0
1013 """
1014 warnings.warn(
1015 "jsonschema.RefResolver.in_scope is deprecated and will be "
1016 "removed in a future release.",
1017 DeprecationWarning,
1018 stacklevel=3,
1019 )
1020 self.push_scope(scope)
1021 try:
1022 yield
1023 finally:
1024 self.pop_scope()
1026 @contextlib.contextmanager
1027 def resolving(self, ref):
1028 """
1029 Resolve the given ``ref`` and enter its resolution scope.
1031 Exits the scope on exit of this context manager.
1033 Arguments:
1035 ref (str):
1037 The reference to resolve
1038 """
1039 url, resolved = self.resolve(ref)
1040 self.push_scope(url)
1041 try:
1042 yield resolved
1043 finally:
1044 self.pop_scope()
1046 def _find_in_referrer(self, key):
1047 return self._get_subschemas_cache()[key]
1049 @lru_cache # noqa: B019
1050 def _get_subschemas_cache(self):
1051 cache = {key: [] for key in _SUBSCHEMAS_KEYWORDS}
1052 for keyword, subschema in _search_schema(
1053 self.referrer, _match_subschema_keywords,
1054 ):
1055 cache[keyword].append(subschema)
1056 return cache
1058 @lru_cache # noqa: B019
1059 def _find_in_subschemas(self, url):
1060 subschemas = self._get_subschemas_cache()["$id"]
1061 if not subschemas:
1062 return None
1063 uri, fragment = urldefrag(url)
1064 for subschema in subschemas:
1065 id = subschema["$id"]
1066 if not isinstance(id, str):
1067 continue
1068 target_uri = self._urljoin_cache(self.resolution_scope, id)
1069 if target_uri.rstrip("/") == uri.rstrip("/"):
1070 if fragment:
1071 subschema = self.resolve_fragment(subschema, fragment)
1072 self.store[url] = subschema
1073 return url, subschema
1074 return None
1076 def resolve(self, ref):
1077 """
1078 Resolve the given reference.
1079 """
1080 url = self._urljoin_cache(self.resolution_scope, ref).rstrip("/")
1082 match = self._find_in_subschemas(url)
1083 if match is not None:
1084 return match
1086 return url, self._remote_cache(url)
1088 def resolve_from_url(self, url):
1089 """
1090 Resolve the given URL.
1091 """
1092 url, fragment = urldefrag(url)
1093 if not url:
1094 url = self.base_uri
1096 try:
1097 document = self.store[url]
1098 except KeyError:
1099 try:
1100 document = self.resolve_remote(url)
1101 except Exception as exc:
1102 raise exceptions._RefResolutionError(exc)
1104 return self.resolve_fragment(document, fragment)
1106 def resolve_fragment(self, document, fragment):
1107 """
1108 Resolve a ``fragment`` within the referenced ``document``.
1110 Arguments:
1112 document:
1114 The referent document
1116 fragment (str):
1118 a URI fragment to resolve within it
1119 """
1120 fragment = fragment.lstrip("/")
1122 if not fragment:
1123 return document
1125 if document is self.referrer:
1126 find = self._find_in_referrer
1127 else:
1129 def find(key):
1130 yield from _search_schema(document, _match_keyword(key))
1132 for keyword in ["$anchor", "$dynamicAnchor"]:
1133 for subschema in find(keyword):
1134 if fragment == subschema[keyword]:
1135 return subschema
1136 for keyword in ["id", "$id"]:
1137 for subschema in find(keyword):
1138 if "#" + fragment == subschema[keyword]:
1139 return subschema
1141 # Resolve via path
1142 parts = unquote(fragment).split("/") if fragment else []
1143 for part in parts:
1144 part = part.replace("~1", "/").replace("~0", "~")
1146 if isinstance(document, Sequence):
1147 try: # noqa: SIM105
1148 part = int(part)
1149 except ValueError:
1150 pass
1151 try:
1152 document = document[part]
1153 except (TypeError, LookupError):
1154 raise exceptions._RefResolutionError(
1155 f"Unresolvable JSON pointer: {fragment!r}",
1156 )
1158 return document
1160 def resolve_remote(self, uri):
1161 """
1162 Resolve a remote ``uri``.
1164 If called directly, does not check the store first, but after
1165 retrieving the document at the specified URI it will be saved in
1166 the store if :attr:`cache_remote` is True.
1168 .. note::
1170 If the requests_ library is present, ``jsonschema`` will use it to
1171 request the remote ``uri``, so that the correct encoding is
1172 detected and used.
1174 If it isn't, or if the scheme of the ``uri`` is not ``http`` or
1175 ``https``, UTF-8 is assumed.
1177 Arguments:
1179 uri (str):
1181 The URI to resolve
1183 Returns:
1185 The retrieved document
1187 .. _requests: https://pypi.org/project/requests/
1188 """
1189 try:
1190 import requests
1191 except ImportError:
1192 requests = None
1194 scheme = urlsplit(uri).scheme
1196 if scheme in self.handlers:
1197 result = self.handlers[scheme](uri)
1198 elif scheme in ["http", "https"] and requests:
1199 # Requests has support for detecting the correct encoding of
1200 # json over http
1201 result = requests.get(uri).json()
1202 else:
1203 # Otherwise, pass off to urllib and assume utf-8
1204 with urlopen(uri) as url:
1205 result = json.loads(url.read().decode("utf-8"))
1207 if self.cache_remote:
1208 self.store[uri] = result
1209 return result
1212_SUBSCHEMAS_KEYWORDS = ("$id", "id", "$anchor", "$dynamicAnchor")
1215def _match_keyword(keyword):
1217 def matcher(value):
1218 if keyword in value:
1219 yield value
1221 return matcher
1224def _match_subschema_keywords(value):
1225 for keyword in _SUBSCHEMAS_KEYWORDS:
1226 if keyword in value:
1227 yield keyword, value
1230def _search_schema(schema, matcher):
1231 """Breadth-first search routine."""
1232 values = deque([schema])
1233 while values:
1234 value = values.pop()
1235 if not isinstance(value, dict):
1236 continue
1237 yield from matcher(value)
1238 values.extendleft(value.values())
1241def validate(instance, schema, cls=None, *args, **kwargs): # noqa: D417
1242 """
1243 Validate an instance under the given schema.
1245 >>> validate([2, 3, 4], {"maxItems": 2})
1246 Traceback (most recent call last):
1247 ...
1248 ValidationError: [2, 3, 4] is too long
1250 :func:`~jsonschema.validators.validate` will first verify that the
1251 provided schema is itself valid, since not doing so can lead to less
1252 obvious error messages and fail in less obvious or consistent ways.
1254 If you know you have a valid schema already, especially
1255 if you intend to validate multiple instances with
1256 the same schema, you likely would prefer using the
1257 `jsonschema.protocols.Validator.validate` method directly on a
1258 specific validator (e.g. ``Draft202012Validator.validate``).
1261 Arguments:
1263 instance:
1265 The instance to validate
1267 schema:
1269 The schema to validate with
1271 cls (jsonschema.protocols.Validator):
1273 The class that will be used to validate the instance.
1275 If the ``cls`` argument is not provided, two things will happen
1276 in accordance with the specification. First, if the schema has a
1277 :kw:`$schema` keyword containing a known meta-schema [#]_ then the
1278 proper validator will be used. The specification recommends that
1279 all schemas contain :kw:`$schema` properties for this reason. If no
1280 :kw:`$schema` property is found, the default validator class is the
1281 latest released draft.
1283 Any other provided positional and keyword arguments will be passed
1284 on when instantiating the ``cls``.
1286 Raises:
1288 `jsonschema.exceptions.ValidationError`:
1290 if the instance is invalid
1292 `jsonschema.exceptions.SchemaError`:
1294 if the schema itself is invalid
1296 .. rubric:: Footnotes
1297 .. [#] known by a validator registered with
1298 `jsonschema.validators.validates`
1299 """
1300 if cls is None:
1301 cls = validator_for(schema)
1303 cls.check_schema(schema)
1304 validator = cls(schema, *args, **kwargs)
1305 error = exceptions.best_match(validator.iter_errors(instance))
1306 if error is not None:
1307 raise error
1310def validator_for(schema, default=_UNSET):
1311 """
1312 Retrieve the validator class appropriate for validating the given schema.
1314 Uses the :kw:`$schema` keyword that should be present in the given
1315 schema to look up the appropriate validator class.
1317 Arguments:
1319 schema (collections.abc.Mapping or bool):
1321 the schema to look at
1323 default:
1325 the default to return if the appropriate validator class
1326 cannot be determined.
1328 If unprovided, the default is to return the latest supported
1329 draft.
1331 Examples:
1333 The :kw:`$schema` JSON Schema keyword will control which validator
1334 class is returned:
1336 >>> schema = {
1337 ... "$schema": "https://json-schema.org/draft/2020-12/schema",
1338 ... "type": "integer",
1339 ... }
1340 >>> jsonschema.validators.validator_for(schema)
1341 <class 'jsonschema.validators.Draft202012Validator'>
1344 Here, a draft 7 schema instead will return the draft 7 validator:
1346 >>> schema = {
1347 ... "$schema": "http://json-schema.org/draft-07/schema#",
1348 ... "type": "integer",
1349 ... }
1350 >>> jsonschema.validators.validator_for(schema)
1351 <class 'jsonschema.validators.Draft7Validator'>
1354 Schemas with no ``$schema`` keyword will fallback to the default
1355 argument:
1357 >>> schema = {"type": "integer"}
1358 >>> jsonschema.validators.validator_for(
1359 ... schema, default=Draft7Validator,
1360 ... )
1361 <class 'jsonschema.validators.Draft7Validator'>
1363 or if none is provided, to the latest version supported.
1364 Always including the keyword when authoring schemas is highly
1365 recommended.
1367 """
1368 DefaultValidator = _LATEST_VERSION if default is _UNSET else default
1370 if schema is True or schema is False or "$schema" not in schema:
1371 return DefaultValidator
1372 if schema["$schema"] not in _META_SCHEMAS and default is _UNSET:
1373 warn(
1374 (
1375 "The metaschema specified by $schema was not found. "
1376 "Using the latest draft to validate, but this will raise "
1377 "an error in the future."
1378 ),
1379 DeprecationWarning,
1380 stacklevel=2,
1381 )
1382 return _META_SCHEMAS.get(schema["$schema"], DefaultValidator)