codekingpro/portable-devtools
115k
1"""2Creation and extension of validators, with implementations for existing drafts.3"""4from __future__ import annotations5 6from collections import deque7from collections.abc import Iterable, Mapping, Sequence8from functools import lru_cache9from operator import methodcaller10from typing import TYPE_CHECKING11from urllib.parse import unquote, urldefrag, urljoin, urlsplit12from warnings import warn13import contextlib14import json15import reprlib16import warnings17 18from attrs import define, field, fields19from jsonschema_specifications import REGISTRY as SPECIFICATIONS20from rpds import HashTrieMap21import referencing.exceptions22import referencing.jsonschema23 24from jsonschema import (25 _format,26 _keywords,27 _legacy_keywords,28 _types,29 _typing,30 _utils,31 exceptions,32)33 34if TYPE_CHECKING:35 from jsonschema.protocols import Validator36 37_UNSET = _utils.Unset()38 39_VALIDATORS: dict[str, Validator] = {}40_META_SCHEMAS = _utils.URIDict()41 42 43def __getattr__(name):44 if name == "ErrorTree":45 warnings.warn(46 "Importing ErrorTree from jsonschema.validators is deprecated. "47 "Instead import it from jsonschema.exceptions.",48 DeprecationWarning,49 stacklevel=2,50 )51 from jsonschema.exceptions import ErrorTree52 return ErrorTree53 elif name == "validators":54 warnings.warn(55 "Accessing jsonschema.validators.validators is deprecated. "56 "Use jsonschema.validators.validator_for with a given schema.",57 DeprecationWarning,58 stacklevel=2,59 )60 return _VALIDATORS61 elif name == "meta_schemas":62 warnings.warn(63 "Accessing jsonschema.validators.meta_schemas is deprecated. "64 "Use jsonschema.validators.validator_for with a given schema.",65 DeprecationWarning,66 stacklevel=2,67 )68 return _META_SCHEMAS69 elif name == "RefResolver":70 warnings.warn(71 _RefResolver._DEPRECATION_MESSAGE,72 DeprecationWarning,73 stacklevel=2,74 )75 return _RefResolver76 raise AttributeError(f"module {__name__} has no attribute {name}")77 78 79def validates(version):80 """81 Register the decorated validator for a ``version`` of the specification.82 83 Registered validators and their meta schemas will be considered when84 parsing :kw:`$schema` keywords' URIs.85 86 Arguments:87 88 version (str):89 90 An identifier to use as the version's name91 92 Returns:93 94 collections.abc.Callable:95 96 a class decorator to decorate the validator with the version97 98 """99 100 def _validates(cls):101 _VALIDATORS[version] = cls102 meta_schema_id = cls.ID_OF(cls.META_SCHEMA)103 _META_SCHEMAS[meta_schema_id] = cls104 return cls105 return _validates106 107 108def _warn_for_remote_retrieve(uri: str):109 from urllib.request import Request, urlopen110 headers = {"User-Agent": "python-jsonschema (deprecated $ref resolution)"}111 request = Request(uri, headers=headers) # noqa: S310112 with urlopen(request) as response: # noqa: S310113 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 )129 130 131_REMOTE_WARNING_REGISTRY = SPECIFICATIONS.combine(132 referencing.Registry(retrieve=_warn_for_remote_retrieve), # type: ignore[call-arg]133)134 135 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) -> type[Validator]:150 """151 Create a new validator class.152 153 Arguments:154 155 meta_schema:156 157 the meta schema for the new validator class158 159 validators:160 161 a mapping from names to callables, where each callable will162 validate the schema property with the given name.163 164 Each callable should take 4 arguments:165 166 1. a validator instance,167 2. the value of the property being validated within the168 instance169 3. the instance170 4. the schema171 172 version:173 174 an identifier for the version that this validator class will175 validate. If provided, the returned validator class will176 have its ``__name__`` set to include the version, and also177 will have `jsonschema.validators.validates` automatically178 called for the given version.179 180 type_checker:181 182 a type checker, used when applying the :kw:`type` keyword.183 184 If unprovided, a `jsonschema.TypeChecker` will be created185 with a set of default types typical of JSON Schema drafts.186 187 format_checker:188 189 a format checker, used when applying the :kw:`format` keyword.190 191 If unprovided, a `jsonschema.FormatChecker` will be created192 with a set of default formats typical of JSON Schema drafts.193 194 id_of:195 196 A function that given a schema, returns its ID.197 198 applicable_validators:199 200 A function that, given a schema, returns the list of201 applicable schema keywords and associated values202 which will be used to validate the instance.203 This is mostly used to support pre-draft 7 versions of JSON Schema204 which specified behavior around ignoring keywords if they were205 siblings of a ``$ref`` keyword. If you're not attempting to206 implement similar behavior, you can typically ignore this argument207 and leave it at its default.208 209 Returns:210 211 a new `jsonschema.protocols.Validator` class212 213 """214 # preemptively don't shadow the `Validator.format_checker` local215 format_checker_arg = format_checker216 217 specification = referencing.jsonschema.specification_with(218 dialect_id=id_of(meta_schema) or "urn:unknown-dialect",219 default=referencing.Specification.OPAQUE,220 )221 222 @define223 class Validator:224 225 VALIDATORS = dict(validators) # noqa: RUF012226 META_SCHEMA = dict(meta_schema) # noqa: RUF012227 TYPE_CHECKER = type_checker228 FORMAT_CHECKER = format_checker_arg229 ID_OF = staticmethod(id_of)230 231 _APPLICABLE_VALIDATORS = applicable_validators232 _validators = field(init=False, repr=False, eq=False)233 234 schema: referencing.jsonschema.Schema = field(repr=reprlib.repr)235 _ref_resolver = field(default=None, repr=False, alias="resolver")236 format_checker: _format.FormatChecker | None = field(default=None)237 # TODO: include new meta-schemas added at runtime238 _registry: referencing.jsonschema.SchemaRegistry = field(239 default=_REMOTE_WARNING_REGISTRY,240 kw_only=True,241 repr=False,242 )243 _resolver = field(244 alias="_resolver",245 default=None,246 kw_only=True,247 repr=False,248 )249 250 def __init_subclass__(cls):251 warnings.warn(252 (253 "Subclassing validator classes is not intended to "254 "be part of their public API. A future version "255 "will make doing so an error, as the behavior of "256 "subclasses isn't guaranteed to stay the same "257 "between releases of jsonschema. Instead, prefer "258 "composition of validators, wrapping them in an object "259 "owned entirely by the downstream library."260 ),261 DeprecationWarning,262 stacklevel=2,263 )264 265 def evolve(self, **changes):266 cls = self.__class__267 schema = changes.setdefault("schema", self.schema)268 NewValidator = validator_for(schema, default=cls)269 270 for field in fields(cls): # noqa: F402271 if not field.init:272 continue273 attr_name = field.name274 init_name = field.alias275 if init_name not in changes:276 changes[init_name] = getattr(self, attr_name)277 278 return NewValidator(**changes)279 280 cls.evolve = evolve281 282 def __attrs_post_init__(self):283 if self._resolver is None:284 registry = self._registry285 if registry is not _REMOTE_WARNING_REGISTRY:286 registry = SPECIFICATIONS.combine(registry)287 resource = specification.create_resource(self.schema)288 self._resolver = registry.resolver_with_root(resource)289 290 if self.schema is True or self.schema is False:291 self._validators = []292 else:293 self._validators = [294 (self.VALIDATORS[k], k, v)295 for k, v in applicable_validators(self.schema)296 if k in self.VALIDATORS297 ]298 299 # REMOVEME: Legacy ref resolution state management.300 push_scope = getattr(self._ref_resolver, "push_scope", None)301 if push_scope is not None:302 id = id_of(self.schema)303 if id is not None:304 push_scope(id)305 306 @classmethod307 def check_schema(cls, schema, format_checker=_UNSET):308 Validator = validator_for(cls.META_SCHEMA, default=cls)309 if format_checker is _UNSET:310 format_checker = Validator.FORMAT_CHECKER311 validator = Validator(312 schema=cls.META_SCHEMA,313 format_checker=format_checker,314 )315 for error in validator.iter_errors(schema):316 raise exceptions.SchemaError.create_from(error)317 318 @property319 def resolver(self):320 warnings.warn(321 (322 f"Accessing {self.__class__.__name__}.resolver is "323 "deprecated as of v4.18.0, in favor of the "324 "https://github.com/python-jsonschema/referencing "325 "library, which provides more compliant referencing "326 "behavior as well as more flexible APIs for "327 "customization."328 ),329 DeprecationWarning,330 stacklevel=2,331 )332 if self._ref_resolver is None:333 self._ref_resolver = _RefResolver.from_schema(334 self.schema,335 id_of=id_of,336 )337 return self._ref_resolver338 339 def evolve(self, **changes):340 schema = changes.setdefault("schema", self.schema)341 NewValidator = validator_for(schema, default=self.__class__)342 343 for (attr_name, init_name) in evolve_fields:344 if init_name not in changes:345 changes[init_name] = getattr(self, attr_name)346 347 return NewValidator(**changes)348 349 def iter_errors(self, instance, _schema=None):350 if _schema is not None:351 warnings.warn(352 (353 "Passing a schema to Validator.iter_errors "354 "is deprecated and will be removed in a future "355 "release. Call validator.evolve(schema=new_schema)."356 "iter_errors(...) instead."357 ),358 DeprecationWarning,359 stacklevel=2,360 )361 validators = [362 (self.VALIDATORS[k], k, v)363 for k, v in applicable_validators(_schema)364 if k in self.VALIDATORS365 ]366 else:367 _schema, validators = self.schema, self._validators368 369 if _schema is True:370 return371 elif _schema is False:372 yield exceptions.ValidationError(373 f"False schema does not allow {instance!r}",374 validator=None,375 validator_value=None,376 instance=instance,377 schema=_schema,378 )379 return380 381 for validator, k, v in validators:382 errors = validator(self, v, instance, _schema) or ()383 for error in errors:384 # set details if not already set by the called fn385 error._set(386 validator=k,387 validator_value=v,388 instance=instance,389 schema=_schema,390 type_checker=self.TYPE_CHECKER,391 )392 if k not in {"if", "$ref"}:393 error.schema_path.appendleft(k)394 yield error395 396 def descend(397 self,398 instance,399 schema,400 path=None,401 schema_path=None,402 resolver=None,403 ):404 if schema is True:405 return406 elif schema is False:407 yield exceptions.ValidationError(408 f"False schema does not allow {instance!r}",409 validator=None,410 validator_value=None,411 instance=instance,412 schema=schema,413 )414 return415 416 if self._ref_resolver is not None:417 evolved = self.evolve(schema=schema)418 else:419 if resolver is None:420 resolver = self._resolver.in_subresource(421 specification.create_resource(schema),422 )423 evolved = self.evolve(schema=schema, _resolver=resolver)424 425 for k, v in applicable_validators(schema):426 validator = evolved.VALIDATORS.get(k)427 if validator is None:428 continue429 430 errors = validator(evolved, v, instance, schema) or ()431 for error in errors:432 # set details if not already set by the called fn433 error._set(434 validator=k,435 validator_value=v,436 instance=instance,437 schema=schema,438 type_checker=evolved.TYPE_CHECKER,439 )440 if k not in {"if", "$ref"}:441 error.schema_path.appendleft(k)442 if path is not None:443 error.path.appendleft(path)444 if schema_path is not None:445 error.schema_path.appendleft(schema_path)446 yield error447 448 def validate(self, *args, **kwargs):449 for error in self.iter_errors(*args, **kwargs):450 raise error451 452 def is_type(self, instance, type):453 try:454 return self.TYPE_CHECKER.is_type(instance, type)455 except exceptions.UndefinedTypeCheck:456 exc = exceptions.UnknownType(type, instance, self.schema)457 raise exc from None458 459 def _validate_reference(self, ref, instance):460 if self._ref_resolver is None:461 try:462 resolved = self._resolver.lookup(ref)463 except referencing.exceptions.Unresolvable as err:464 raise exceptions._WrappedReferencingError(err) from err465 466 return self.descend(467 instance,468 resolved.contents,469 resolver=resolved.resolver,470 )471 else:472 resolve = getattr(self._ref_resolver, "resolve", None)473 if resolve is None:474 with self._ref_resolver.resolving(ref) as resolved:475 return self.descend(instance, resolved)476 else:477 scope, resolved = resolve(ref)478 self._ref_resolver.push_scope(scope)479 480 try:481 return list(self.descend(instance, resolved))482 finally:483 self._ref_resolver.pop_scope()484 485 def is_valid(self, instance, _schema=None):486 if _schema is not None:487 warnings.warn(488 (489 "Passing a schema to Validator.is_valid is deprecated "490 "and will be removed in a future release. Call "491 "validator.evolve(schema=new_schema).is_valid(...) "492 "instead."493 ),494 DeprecationWarning,495 stacklevel=2,496 )497 self = self.evolve(schema=_schema)498 499 error = next(self.iter_errors(instance), None)500 return error is None501 502 evolve_fields = [503 (field.name, field.alias)504 for field in fields(Validator)505 if field.init506 ]507 508 if version is not None:509 safe = version.title().replace(" ", "").replace("-", "")510 Validator.__name__ = Validator.__qualname__ = f"{safe}Validator"511 Validator = validates(version)(Validator) # type: ignore[misc]512 513 return Validator # type: ignore[return-value]514 515 516def extend(517 validator,518 validators=(),519 version=None,520 type_checker=None,521 format_checker=None,522):523 """524 Create a new validator class by extending an existing one.525 526 Arguments:527 528 validator (jsonschema.protocols.Validator):529 530 an existing validator class531 532 validators (collections.abc.Mapping):533 534 a mapping of new validator callables to extend with, whose535 structure is as in `create`.536 537 .. note::538 539 Any validator callables with the same name as an540 existing one will (silently) replace the old validator541 callable entirely, effectively overriding any validation542 done in the "parent" validator class.543 544 If you wish to instead extend the behavior of a parent's545 validator callable, delegate and call it directly in546 the new validator function by retrieving it using547 ``OldValidator.VALIDATORS["validation_keyword_name"]``.548 549 version (str):550 551 a version for the new validator class552 553 type_checker (jsonschema.TypeChecker):554 555 a type checker, used when applying the :kw:`type` keyword.556 557 If unprovided, the type checker of the extended558 `jsonschema.protocols.Validator` will be carried along.559 560 format_checker (jsonschema.FormatChecker):561 562 a format checker, used when applying the :kw:`format` keyword.563 564 If unprovided, the format checker of the extended565 `jsonschema.protocols.Validator` will be carried along.566 567 Returns:568 569 a new `jsonschema.protocols.Validator` class extending the one570 provided571 572 .. note:: Meta Schemas573 574 The new validator class will have its parent's meta schema.575 576 If you wish to change or extend the meta schema in the new577 validator class, modify ``META_SCHEMA`` directly on the returned578 class. Note that no implicit copying is done, so a copy should579 likely be made before modifying it, in order to not affect the580 old validator.581 582 """583 all_validators = dict(validator.VALIDATORS)584 all_validators.update(validators)585 586 if type_checker is None:587 type_checker = validator.TYPE_CHECKER588 if format_checker is None:589 format_checker = validator.FORMAT_CHECKER590 return create(591 meta_schema=validator.META_SCHEMA,592 validators=all_validators,593 version=version,594 type_checker=type_checker,595 format_checker=format_checker,596 id_of=validator.ID_OF,597 applicable_validators=validator._APPLICABLE_VALIDATORS,598 )599 600 601Draft3Validator = create(602 meta_schema=SPECIFICATIONS.contents(603 "http://json-schema.org/draft-03/schema#",604 ),605 validators={606 "$ref": _keywords.ref,607 "additionalItems": _legacy_keywords.additionalItems,608 "additionalProperties": _keywords.additionalProperties,609 "dependencies": _legacy_keywords.dependencies_draft3,610 "disallow": _legacy_keywords.disallow_draft3,611 "divisibleBy": _keywords.multipleOf,612 "enum": _keywords.enum,613 "extends": _legacy_keywords.extends_draft3,614 "format": _keywords.format,615 "items": _legacy_keywords.items_draft3_draft4,616 "maxItems": _keywords.maxItems,617 "maxLength": _keywords.maxLength,618 "maximum": _legacy_keywords.maximum_draft3_draft4,619 "minItems": _keywords.minItems,620 "minLength": _keywords.minLength,621 "minimum": _legacy_keywords.minimum_draft3_draft4,622 "pattern": _keywords.pattern,623 "patternProperties": _keywords.patternProperties,624 "properties": _legacy_keywords.properties_draft3,625 "type": _legacy_keywords.type_draft3,626 "uniqueItems": _keywords.uniqueItems,627 },628 type_checker=_types.draft3_type_checker,629 format_checker=_format.draft3_format_checker,630 version="draft3",631 id_of=referencing.jsonschema.DRAFT3.id_of,632 applicable_validators=_legacy_keywords.ignore_ref_siblings,633)634 635Draft4Validator = create(636 meta_schema=SPECIFICATIONS.contents(637 "http://json-schema.org/draft-04/schema#",638 ),639 validators={640 "$ref": _keywords.ref,641 "additionalItems": _legacy_keywords.additionalItems,642 "additionalProperties": _keywords.additionalProperties,643 "allOf": _keywords.allOf,644 "anyOf": _keywords.anyOf,645 "dependencies": _legacy_keywords.dependencies_draft4_draft6_draft7,646 "enum": _keywords.enum,647 "format": _keywords.format,648 "items": _legacy_keywords.items_draft3_draft4,649 "maxItems": _keywords.maxItems,650 "maxLength": _keywords.maxLength,651 "maxProperties": _keywords.maxProperties,652 "maximum": _legacy_keywords.maximum_draft3_draft4,653 "minItems": _keywords.minItems,654 "minLength": _keywords.minLength,655 "minProperties": _keywords.minProperties,656 "minimum": _legacy_keywords.minimum_draft3_draft4,657 "multipleOf": _keywords.multipleOf,658 "not": _keywords.not_,659 "oneOf": _keywords.oneOf,660 "pattern": _keywords.pattern,661 "patternProperties": _keywords.patternProperties,662 "properties": _keywords.properties,663 "required": _keywords.required,664 "type": _keywords.type,665 "uniqueItems": _keywords.uniqueItems,666 },667 type_checker=_types.draft4_type_checker,668 format_checker=_format.draft4_format_checker,669 version="draft4",670 id_of=referencing.jsonschema.DRAFT4.id_of,671 applicable_validators=_legacy_keywords.ignore_ref_siblings,672)673 674Draft6Validator = create(675 meta_schema=SPECIFICATIONS.contents(676 "http://json-schema.org/draft-06/schema#",677 ),678 validators={679 "$ref": _keywords.ref,680 "additionalItems": _legacy_keywords.additionalItems,681 "additionalProperties": _keywords.additionalProperties,682 "allOf": _keywords.allOf,683 "anyOf": _keywords.anyOf,684 "const": _keywords.const,685 "contains": _legacy_keywords.contains_draft6_draft7,686 "dependencies": _legacy_keywords.dependencies_draft4_draft6_draft7,687 "enum": _keywords.enum,688 "exclusiveMaximum": _keywords.exclusiveMaximum,689 "exclusiveMinimum": _keywords.exclusiveMinimum,690 "format": _keywords.format,691 "items": _legacy_keywords.items_draft6_draft7_draft201909,692 "maxItems": _keywords.maxItems,693 "maxLength": _keywords.maxLength,694 "maxProperties": _keywords.maxProperties,695 "maximum": _keywords.maximum,696 "minItems": _keywords.minItems,697 "minLength": _keywords.minLength,698 "minProperties": _keywords.minProperties,699 "minimum": _keywords.minimum,700 "multipleOf": _keywords.multipleOf,701 "not": _keywords.not_,702 "oneOf": _keywords.oneOf,703 "pattern": _keywords.pattern,704 "patternProperties": _keywords.patternProperties,705 "properties": _keywords.properties,706 "propertyNames": _keywords.propertyNames,707 "required": _keywords.required,708 "type": _keywords.type,709 "uniqueItems": _keywords.uniqueItems,710 },711 type_checker=_types.draft6_type_checker,712 format_checker=_format.draft6_format_checker,713 version="draft6",714 id_of=referencing.jsonschema.DRAFT6.id_of,715 applicable_validators=_legacy_keywords.ignore_ref_siblings,716)717 718Draft7Validator = create(719 meta_schema=SPECIFICATIONS.contents(720 "http://json-schema.org/draft-07/schema#",721 ),722 validators={723 "$ref": _keywords.ref,724 "additionalItems": _legacy_keywords.additionalItems,725 "additionalProperties": _keywords.additionalProperties,726 "allOf": _keywords.allOf,727 "anyOf": _keywords.anyOf,728 "const": _keywords.const,729 "contains": _legacy_keywords.contains_draft6_draft7,730 "dependencies": _legacy_keywords.dependencies_draft4_draft6_draft7,731 "enum": _keywords.enum,732 "exclusiveMaximum": _keywords.exclusiveMaximum,733 "exclusiveMinimum": _keywords.exclusiveMinimum,734 "format": _keywords.format,735 "if": _keywords.if_,736 "items": _legacy_keywords.items_draft6_draft7_draft201909,737 "maxItems": _keywords.maxItems,738 "maxLength": _keywords.maxLength,739 "maxProperties": _keywords.maxProperties,740 "maximum": _keywords.maximum,741 "minItems": _keywords.minItems,742 "minLength": _keywords.minLength,743 "minProperties": _keywords.minProperties,744 "minimum": _keywords.minimum,745 "multipleOf": _keywords.multipleOf,746 "not": _keywords.not_,747 "oneOf": _keywords.oneOf,748 "pattern": _keywords.pattern,749 "patternProperties": _keywords.patternProperties,750 "properties": _keywords.properties,751 "propertyNames": _keywords.propertyNames,752 "required": _keywords.required,753 "type": _keywords.type,754 "uniqueItems": _keywords.uniqueItems,755 },756 type_checker=_types.draft7_type_checker,757 format_checker=_format.draft7_format_checker,758 version="draft7",759 id_of=referencing.jsonschema.DRAFT7.id_of,760 applicable_validators=_legacy_keywords.ignore_ref_siblings,761)762 763Draft201909Validator = create(764 meta_schema=SPECIFICATIONS.contents(765 "https://json-schema.org/draft/2019-09/schema",766 ),767 validators={768 "$recursiveRef": _legacy_keywords.recursiveRef,769 "$ref": _keywords.ref,770 "additionalItems": _legacy_keywords.additionalItems,771 "additionalProperties": _keywords.additionalProperties,772 "allOf": _keywords.allOf,773 "anyOf": _keywords.anyOf,774 "const": _keywords.const,775 "contains": _keywords.contains,776 "dependentRequired": _keywords.dependentRequired,777 "dependentSchemas": _keywords.dependentSchemas,778 "enum": _keywords.enum,779 "exclusiveMaximum": _keywords.exclusiveMaximum,780 "exclusiveMinimum": _keywords.exclusiveMinimum,781 "format": _keywords.format,782 "if": _keywords.if_,783 "items": _legacy_keywords.items_draft6_draft7_draft201909,784 "maxItems": _keywords.maxItems,785 "maxLength": _keywords.maxLength,786 "maxProperties": _keywords.maxProperties,787 "maximum": _keywords.maximum,788 "minItems": _keywords.minItems,789 "minLength": _keywords.minLength,790 "minProperties": _keywords.minProperties,791 "minimum": _keywords.minimum,792 "multipleOf": _keywords.multipleOf,793 "not": _keywords.not_,794 "oneOf": _keywords.oneOf,795 "pattern": _keywords.pattern,796 "patternProperties": _keywords.patternProperties,797 "properties": _keywords.properties,798 "propertyNames": _keywords.propertyNames,799 "required": _keywords.required,800 "type": _keywords.type,801 "unevaluatedItems": _legacy_keywords.unevaluatedItems_draft2019,802 "unevaluatedProperties": (803 _legacy_keywords.unevaluatedProperties_draft2019804 ),805 "uniqueItems": _keywords.uniqueItems,806 },807 type_checker=_types.draft201909_type_checker,808 format_checker=_format.draft201909_format_checker,809 version="draft2019-09",810)811 812Draft202012Validator = create(813 meta_schema=SPECIFICATIONS.contents(814 "https://json-schema.org/draft/2020-12/schema",815 ),816 validators={817 "$dynamicRef": _keywords.dynamicRef,818 "$ref": _keywords.ref,819 "additionalProperties": _keywords.additionalProperties,820 "allOf": _keywords.allOf,821 "anyOf": _keywords.anyOf,822 "const": _keywords.const,823 "contains": _keywords.contains,824 "dependentRequired": _keywords.dependentRequired,825 "dependentSchemas": _keywords.dependentSchemas,826 "enum": _keywords.enum,827 "exclusiveMaximum": _keywords.exclusiveMaximum,828 "exclusiveMinimum": _keywords.exclusiveMinimum,829 "format": _keywords.format,830 "if": _keywords.if_,831 "items": _keywords.items,832 "maxItems": _keywords.maxItems,833 "maxLength": _keywords.maxLength,834 "maxProperties": _keywords.maxProperties,835 "maximum": _keywords.maximum,836 "minItems": _keywords.minItems,837 "minLength": _keywords.minLength,838 "minProperties": _keywords.minProperties,839 "minimum": _keywords.minimum,840 "multipleOf": _keywords.multipleOf,841 "not": _keywords.not_,842 "oneOf": _keywords.oneOf,843 "pattern": _keywords.pattern,844 "patternProperties": _keywords.patternProperties,845 "prefixItems": _keywords.prefixItems,846 "properties": _keywords.properties,847 "propertyNames": _keywords.propertyNames,848 "required": _keywords.required,849 "type": _keywords.type,850 "unevaluatedItems": _keywords.unevaluatedItems,851 "unevaluatedProperties": _keywords.unevaluatedProperties,852 "uniqueItems": _keywords.uniqueItems,853 },854 type_checker=_types.draft202012_type_checker,855 format_checker=_format.draft202012_format_checker,856 version="draft2020-12",857)858 859_LATEST_VERSION: type[Validator] = Draft202012Validator860 861 862class _RefResolver:863 """864 Resolve JSON References.865 866 Arguments:867 868 base_uri (str):869 870 The URI of the referring document871 872 referrer:873 874 The actual referring document875 876 store (dict):877 878 A mapping from URIs to documents to cache879 880 cache_remote (bool):881 882 Whether remote refs should be cached after first resolution883 884 handlers (dict):885 886 A mapping from URI schemes to functions that should be used887 to retrieve them888 889 urljoin_cache (:func:`functools.lru_cache`):890 891 A cache that will be used for caching the results of joining892 the resolution scope to subscopes.893 894 remote_cache (:func:`functools.lru_cache`):895 896 A cache that will be used for caching the results of897 resolved remote URLs.898 899 Attributes:900 901 cache_remote (bool):902 903 Whether remote refs should be cached after first resolution904 905 .. deprecated:: v4.18.0906 907 ``RefResolver`` has been deprecated in favor of `referencing`.908 909 """910 911 _DEPRECATION_MESSAGE = (912 "jsonschema.RefResolver is deprecated as of v4.18.0, in favor of the "913 "https://github.com/python-jsonschema/referencing library, which "914 "provides more compliant referencing behavior as well as more "915 "flexible APIs for customization. A future release will remove "916 "RefResolver. Please file a feature request (on referencing) if you "917 "are missing an API for the kind of customization you need."918 )919 920 def __init__(921 self,922 base_uri,923 referrer,924 store=HashTrieMap(),925 cache_remote=True,926 handlers=(),927 urljoin_cache=None,928 remote_cache=None,929 ):930 if urljoin_cache is None:931 urljoin_cache = lru_cache(1024)(urljoin)932 if remote_cache is None:933 remote_cache = lru_cache(1024)(self.resolve_from_url)934 935 self.referrer = referrer936 self.cache_remote = cache_remote937 self.handlers = dict(handlers)938 939 self._scopes_stack = [base_uri]940 941 self.store = _utils.URIDict(942 (uri, each.contents) for uri, each in SPECIFICATIONS.items()943 )944 self.store.update(945 (id, each.META_SCHEMA) for id, each in _META_SCHEMAS.items()946 )947 self.store.update(store)948 self.store.update(949 (schema["$id"], schema)950 for schema in store.values()951 if isinstance(schema, Mapping) and "$id" in schema952 )953 self.store[base_uri] = referrer954 955 self._urljoin_cache = urljoin_cache956 self._remote_cache = remote_cache957 958 @classmethod959 def from_schema( # noqa: D417960 cls,961 schema,962 id_of=referencing.jsonschema.DRAFT202012.id_of,963 *args,964 **kwargs,965 ):966 """967 Construct a resolver from a JSON schema object.968 969 Arguments:970 971 schema:972 973 the referring schema974 975 Returns:976 977 `_RefResolver`978 979 """980 return cls(base_uri=id_of(schema) or "", referrer=schema, *args, **kwargs) # noqa: B026, E501981 982 def push_scope(self, scope):983 """984 Enter a given sub-scope.985 986 Treats further dereferences as being performed underneath the987 given scope.988 """989 self._scopes_stack.append(990 self._urljoin_cache(self.resolution_scope, scope),991 )992 993 def pop_scope(self):994 """995 Exit the most recent entered scope.996 997 Treats further dereferences as being performed underneath the998 original scope.999 1000 Don't call this method more times than `push_scope` has been1001 called.1002 """1003 try:1004 self._scopes_stack.pop()1005 except IndexError:1006 raise exceptions._RefResolutionError(1007 "Failed to pop the scope from an empty stack. "1008 "`pop_scope()` should only be called once for every "1009 "`push_scope()`",1010 ) from None1011 1012 @property1013 def resolution_scope(self):1014 """1015 Retrieve the current resolution scope.1016 """1017 return self._scopes_stack[-1]1018 1019 @property1020 def base_uri(self):1021 """1022 Retrieve the current base URI, not including any fragment.1023 """1024 uri, _ = urldefrag(self.resolution_scope)1025 return uri1026 1027 @contextlib.contextmanager1028 def in_scope(self, scope):1029 """1030 Temporarily enter the given scope for the duration of the context.1031 1032 .. deprecated:: v4.0.01033 """1034 warnings.warn(1035 "jsonschema.RefResolver.in_scope is deprecated and will be "1036 "removed in a future release.",1037 DeprecationWarning,1038 stacklevel=3,1039 )1040 self.push_scope(scope)1041 try:1042 yield1043 finally:1044 self.pop_scope()1045 1046 @contextlib.contextmanager1047 def resolving(self, ref):1048 """1049 Resolve the given ``ref`` and enter its resolution scope.1050 1051 Exits the scope on exit of this context manager.1052 1053 Arguments:1054 1055 ref (str):1056 1057 The reference to resolve1058 1059 """1060 url, resolved = self.resolve(ref)1061 self.push_scope(url)1062 try:1063 yield resolved1064 finally:1065 self.pop_scope()1066 1067 def _find_in_referrer(self, key):1068 return self._get_subschemas_cache()[key]1069 1070 @lru_cache # noqa: B0191071 def _get_subschemas_cache(self):1072 cache = {key: [] for key in _SUBSCHEMAS_KEYWORDS}1073 for keyword, subschema in _search_schema(1074 self.referrer, _match_subschema_keywords,1075 ):1076 cache[keyword].append(subschema)1077 return cache1078 1079 @lru_cache # noqa: B0191080 def _find_in_subschemas(self, url):1081 subschemas = self._get_subschemas_cache()["$id"]1082 if not subschemas:1083 return None1084 uri, fragment = urldefrag(url)1085 for subschema in subschemas:1086 id = subschema["$id"]1087 if not isinstance(id, str):1088 continue1089 target_uri = self._urljoin_cache(self.resolution_scope, id)1090 if target_uri.rstrip("/") == uri.rstrip("/"):1091 if fragment:1092 subschema = self.resolve_fragment(subschema, fragment)1093 self.store[url] = subschema1094 return url, subschema1095 return None1096 1097 def resolve(self, ref):1098 """1099 Resolve the given reference.1100 """1101 url = self._urljoin_cache(self.resolution_scope, ref).rstrip("/")1102 1103 match = self._find_in_subschemas(url)1104 if match is not None:1105 return match1106 1107 return url, self._remote_cache(url)1108 1109 def resolve_from_url(self, url):1110 """1111 Resolve the given URL.1112 """1113 url, fragment = urldefrag(url)1114 if not url:1115 url = self.base_uri1116 1117 try:1118 document = self.store[url]1119 except KeyError:1120 try:1121 document = self.resolve_remote(url)1122 except Exception as exc:1123 raise exceptions._RefResolutionError(exc) from exc1124 1125 return self.resolve_fragment(document, fragment)1126 1127 def resolve_fragment(self, document, fragment):1128 """1129 Resolve a ``fragment`` within the referenced ``document``.1130 1131 Arguments:1132 1133 document:1134 1135 The referent document1136 1137 fragment (str):1138 1139 a URI fragment to resolve within it1140 1141 """1142 fragment = fragment.lstrip("/")1143 1144 if not fragment:1145 return document1146 1147 if document is self.referrer:1148 find = self._find_in_referrer1149 else:1150 1151 def find(key):1152 yield from _search_schema(document, _match_keyword(key))1153 1154 for keyword in ["$anchor", "$dynamicAnchor"]:1155 for subschema in find(keyword):1156 if fragment == subschema[keyword]:1157 return subschema1158 for keyword in ["id", "$id"]:1159 for subschema in find(keyword):1160 if "#" + fragment == subschema[keyword]:1161 return subschema1162 1163 # Resolve via path1164 parts = unquote(fragment).split("/") if fragment else []1165 for part in parts:1166 part = part.replace("~1", "/").replace("~0", "~")1167 1168 if isinstance(document, Sequence):1169 try: # noqa: SIM1051170 part = int(part)1171 except ValueError:1172 pass1173 try:1174 document = document[part]1175 except (TypeError, LookupError) as err:1176 raise exceptions._RefResolutionError(1177 f"Unresolvable JSON pointer: {fragment!r}",1178 ) from err1179 1180 return document1181 1182 def resolve_remote(self, uri):1183 """1184 Resolve a remote ``uri``.1185 1186 If called directly, does not check the store first, but after1187 retrieving the document at the specified URI it will be saved in1188 the store if :attr:`cache_remote` is True.1189 1190 .. note::1191 1192 If the requests_ library is present, ``jsonschema`` will use it to1193 request the remote ``uri``, so that the correct encoding is1194 detected and used.1195 1196 If it isn't, or if the scheme of the ``uri`` is not ``http`` or1197 ``https``, UTF-8 is assumed.1198 1199 Arguments:1200 