Coverage for /pythoncovmergedfiles/medio/medio/src/jsonschema/jsonschema/exceptions.py: 52%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2Validation errors, and some surrounding helpers.
3"""
4from __future__ import annotations
6from collections import defaultdict, deque
7from pprint import pformat
8from textwrap import dedent, indent
9from typing import TYPE_CHECKING, Any, ClassVar, TypeVar
10import re
11import warnings
13from attrs import define
14from referencing.exceptions import Unresolvable as _Unresolvable
16from jsonschema import _utils
18if TYPE_CHECKING:
19 from collections.abc import (
20 Callable,
21 Iterable,
22 Mapping,
23 MutableMapping,
24 Sequence,
25 )
27 from jsonschema import _types
29WEAK_MATCHES: frozenset[str] = frozenset(["anyOf", "oneOf"])
30STRONG_MATCHES: frozenset[str] = frozenset()
32_JSON_PATH_COMPATIBLE_PROPERTY_PATTERN = re.compile("^[a-zA-Z][a-zA-Z0-9_]*$")
34_unset = _utils.Unset()
37def _pretty(thing: Any, prefix: str):
38 """
39 Format something for an error message as prettily as we currently can.
40 """
41 return indent(pformat(thing, width=72, sort_dicts=False), prefix).lstrip()
44def __getattr__(name):
45 if name == "RefResolutionError":
46 warnings.warn(
47 _RefResolutionError._DEPRECATION_MESSAGE,
48 DeprecationWarning,
49 stacklevel=2,
50 )
51 return _RefResolutionError
52 raise AttributeError(f"module {__name__} has no attribute {name}")
55class _Error(Exception):
57 _word_for_schema_in_error_message: ClassVar[str]
58 _word_for_instance_in_error_message: ClassVar[str]
60 def __init__(
61 self,
62 message: str,
63 validator: str = _unset, # type: ignore[assignment]
64 path: Iterable[str | int] = (),
65 cause: Exception | None = None,
66 context=(),
67 validator_value: Any = _unset,
68 instance: Any = _unset,
69 schema: Mapping[str, Any] | bool = _unset, # type: ignore[assignment]
70 schema_path: Iterable[str | int] = (),
71 parent: _Error | None = None,
72 type_checker: _types.TypeChecker = _unset, # type: ignore[assignment]
73 ) -> None:
74 super().__init__(
75 message,
76 validator,
77 path,
78 cause,
79 context,
80 validator_value,
81 instance,
82 schema,
83 schema_path,
84 parent,
85 )
86 self.message = message
87 self.path = self.relative_path = deque(path)
88 self.schema_path = self.relative_schema_path = deque(schema_path)
89 self.context = list(context)
90 self.cause = self.__cause__ = cause
91 self.validator = validator
92 self.validator_value = validator_value
93 self.instance = instance
94 self.schema = schema
95 self.parent = parent
96 self._type_checker = type_checker
98 for error in context:
99 error.parent = self
101 def __repr__(self) -> str:
102 return f"<{self.__class__.__name__}: {self.message!r}>"
104 def __str__(self) -> str:
105 essential_for_verbose = (
106 self.validator, self.validator_value, self.instance, self.schema,
107 )
108 if any(m is _unset for m in essential_for_verbose):
109 return self.message
111 schema_path = _utils.format_as_index(
112 container=self._word_for_schema_in_error_message,
113 indices=list(self.relative_schema_path)[:-1],
114 )
115 instance_path = _utils.format_as_index(
116 container=self._word_for_instance_in_error_message,
117 indices=self.relative_path,
118 )
119 prefix = 16 * " "
121 return dedent(
122 f"""\
123 {self.message}
125 Failed validating {self.validator!r} in {schema_path}:
126 {_pretty(self.schema, prefix=prefix)}
128 On {instance_path}:
129 {_pretty(self.instance, prefix=prefix)}
130 """.rstrip(),
131 )
133 @classmethod
134 def create_from(cls, other: _Error):
135 return cls(**other._contents())
137 @property
138 def absolute_path(self) -> Sequence[str | int]:
139 parent = self.parent
140 if parent is None:
141 return self.relative_path
143 path = deque(self.relative_path)
144 path.extendleft(reversed(parent.absolute_path))
145 return path
147 @property
148 def absolute_schema_path(self) -> Sequence[str | int]:
149 parent = self.parent
150 if parent is None:
151 return self.relative_schema_path
153 path = deque(self.relative_schema_path)
154 path.extendleft(reversed(parent.absolute_schema_path))
155 return path
157 @property
158 def json_path(self) -> str:
159 path = "$"
160 for elem in self.absolute_path:
161 if isinstance(elem, int):
162 path += "[" + str(elem) + "]"
163 elif _JSON_PATH_COMPATIBLE_PROPERTY_PATTERN.match(elem):
164 path += "." + elem
165 else:
166 escaped_elem = elem.replace("\\", "\\\\").replace("'", r"\'")
167 path += "['" + escaped_elem + "']"
168 return path
170 def _set(
171 self,
172 type_checker: _types.TypeChecker | None = None,
173 **kwargs: Any,
174 ) -> None:
175 if type_checker is not None and self._type_checker is _unset:
176 self._type_checker = type_checker
178 for k, v in kwargs.items():
179 if getattr(self, k) is _unset:
180 setattr(self, k, v)
182 def _contents(self):
183 attrs = (
184 "message", "cause", "context", "validator", "validator_value",
185 "path", "schema_path", "instance", "schema", "parent",
186 )
187 return {attr: getattr(self, attr) for attr in attrs}
189 def _matches_type(self) -> bool:
190 try:
191 # We ignore this as we want to simply crash if this happens
192 expected = self.schema["type"] # type: ignore[index]
193 except (KeyError, TypeError):
194 return False
196 if isinstance(expected, str):
197 return self._type_checker.is_type(self.instance, expected)
199 return any(
200 self._type_checker.is_type(self.instance, expected_type)
201 for expected_type in expected
202 )
205_E = TypeVar("_E", bound=_Error)
208class ValidationError(_Error):
209 """
210 An instance was invalid under a provided schema.
211 """
213 _word_for_schema_in_error_message = "schema"
214 _word_for_instance_in_error_message = "instance"
217class SchemaError(_Error):
218 """
219 A schema was invalid under its corresponding metaschema.
220 """
222 _word_for_schema_in_error_message = "metaschema"
223 _word_for_instance_in_error_message = "schema"
226@define(slots=False)
227class _RefResolutionError(Exception): # noqa: PLW1641
228 """
229 A ref could not be resolved.
230 """
232 _DEPRECATION_MESSAGE = (
233 "jsonschema.exceptions.RefResolutionError is deprecated as of version "
234 "4.18.0. If you wish to catch potential reference resolution errors, "
235 "directly catch referencing.exceptions.Unresolvable."
236 )
238 _cause: Exception
240 def __eq__(self, other):
241 if self.__class__ is not other.__class__:
242 return NotImplemented # pragma: no cover -- uncovered but deprecated # noqa: E501
243 return self._cause == other._cause
245 def __str__(self) -> str:
246 return str(self._cause)
249class _WrappedReferencingError(_RefResolutionError, _Unresolvable): # pragma: no cover -- partially uncovered but to be removed # noqa: E501
250 def __init__(self, cause: _Unresolvable):
251 object.__setattr__(self, "_wrapped", cause)
253 def __eq__(self, other):
254 if other.__class__ is self.__class__:
255 return self._wrapped == other._wrapped
256 elif other.__class__ is self._wrapped.__class__:
257 return self._wrapped == other
258 return NotImplemented
260 def __getattr__(self, attr):
261 return getattr(self._wrapped, attr)
263 def __hash__(self):
264 return hash(self._wrapped)
266 def __repr__(self):
267 return f"<WrappedReferencingError {self._wrapped!r}>"
269 def __str__(self):
270 return f"{self._wrapped.__class__.__name__}: {self._wrapped}"
273class UndefinedTypeCheck(Exception):
274 """
275 A type checker was asked to check a type it did not have registered.
276 """
278 def __init__(self, type: str) -> None:
279 self.type = type
281 def __str__(self) -> str:
282 return f"Type {self.type!r} is unknown to this type checker"
285class UnknownType(Exception):
286 """
287 A validator was asked to validate an instance against an unknown type.
288 """
290 def __init__(self, type, instance, schema):
291 self.type = type
292 self.instance = instance
293 self.schema = schema
295 def __str__(self):
296 prefix = 16 * " "
298 return dedent(
299 f"""\
300 Unknown type {self.type!r} for validator with schema:
301 {_pretty(self.schema, prefix=prefix)}
303 While checking instance:
304 {_pretty(self.instance, prefix=prefix)}
305 """.rstrip(),
306 )
309class FormatError(Exception):
310 """
311 Validating a format failed.
312 """
314 def __init__(self, message, cause=None):
315 super().__init__(message, cause)
316 self.message = message
317 self.cause = self.__cause__ = cause
319 def __str__(self):
320 return self.message
323class ErrorTree:
324 """
325 ErrorTrees make it easier to check which validations failed.
327 Arguments:
329 errors:
331 the errors to populate the tree with
333 instance:
335 the instance the tree corresponds to, if known, which
336 enables indexing the tree at indices not present in it to
337 raise the same error that indexing the instance itself would
339 """
341 def __init__(
342 self,
343 errors: Iterable[ValidationError] = (),
344 *,
345 instance: Any = _unset,
346 ):
347 self.errors: MutableMapping[str, ValidationError] = {}
348 self._contents: MutableMapping[str | int, ErrorTree] = {}
349 self._instance = instance
351 for error in errors:
352 container = self
353 for element in error.path:
354 container = container._contents.setdefault(
355 element, self.__class__(),
356 )
357 container.errors[error.validator] = error
359 container._instance = error.instance
361 def __contains__(self, index: str | int):
362 """
363 Check whether ``instance[index]`` has any errors.
364 """
365 return index in self._contents
367 def __getitem__(self, index):
368 """
369 Retrieve the child tree one level down at the given ``index``.
371 If the index is not in the instance that this tree corresponds
372 to and is not known by this tree, whatever error would be raised
373 by ``instance.__getitem__`` will be propagated (usually this is
374 some subclass of `LookupError`.
375 """
376 if index in self:
377 return self._contents[index]
378 if self._instance is _unset:
379 return self.__class__()
380 return self.__class__(instance=self._instance[index])
382 def __setitem__(self, index: str | int, value: ErrorTree):
383 """
384 Add an error to the tree at the given ``index``.
386 .. deprecated:: v4.20.0
388 Setting items on an `ErrorTree` is deprecated without replacement.
389 To populate a tree, provide all of its sub-errors when you
390 construct the tree.
391 """
392 warnings.warn(
393 "ErrorTree.__setitem__ is deprecated without replacement.",
394 DeprecationWarning,
395 stacklevel=2,
396 )
397 self._contents[index] = value
399 def __iter__(self):
400 """
401 Iterate (non-recursively) over the indices in the instance with errors.
402 """
403 return iter(self._contents)
405 def __len__(self):
406 """
407 Return the `total_errors`.
408 """
409 return self.total_errors
411 def __repr__(self):
412 total = len(self)
413 errors = "error" if total == 1 else "errors"
414 return f"<{self.__class__.__name__} ({total} total {errors})>"
416 @property
417 def total_errors(self):
418 """
419 The total number of errors in the entire tree, including children.
420 """
421 child_errors = sum(len(tree) for _, tree in self._contents.items())
422 return len(self.errors) + child_errors
425def by_relevance(weak=WEAK_MATCHES, strong=STRONG_MATCHES):
426 """
427 Create a key function that can be used to sort errors by relevance.
429 Arguments:
430 weak (set):
431 a collection of validation keywords to consider to be
432 "weak". If there are two errors at the same level of the
433 instance and one is in the set of weak validation keywords,
434 the other error will take priority. By default, :kw:`anyOf`
435 and :kw:`oneOf` are considered weak keywords and will be
436 superseded by other same-level validation errors.
438 strong (set):
439 a collection of validation keywords to consider to be
440 "strong"
442 """
444 def relevance(error):
445 validator = error.validator
446 return ( # prefer errors which are ...
447 -len(error.path), # shorter path thereby more general
448 validator not in weak, # for a non-low-priority keyword
449 validator in strong, # for a high priority keyword
450 not error._matches_type(), # at least match the instance's type
451 ) # otherwise we'll treat them the same
453 return relevance
456relevance = by_relevance()
457"""
458A key function (e.g. to use with `sorted`) which sorts errors by relevance.
460Example:
462.. code:: python
464 sorted(validator.iter_errors(12), key=jsonschema.exceptions.relevance)
465"""
468def best_match(errors, key=relevance):
469 """
470 Try to find an error that appears to be the best match among given errors.
472 In general, errors that are higher up in the instance (i.e. for which
473 `ValidationError.path` is shorter) are considered better matches,
474 since they indicate "more" is wrong with the instance.
476 If the resulting match is either :kw:`oneOf` or :kw:`anyOf`, the
477 *opposite* assumption is made -- i.e. the deepest error is picked
478 among the most relevant errors in each separate subschema (preferring
479 subschemas which produced fewer errors when tied), since these
480 keywords only need to match once, and any other errors may not be
481 relevant.
483 Arguments:
484 errors (collections.abc.Iterable):
486 the errors to select from. Do not provide a mixture of
487 errors from different validation attempts (i.e. from
488 different instances or schemas), since it won't produce
489 sensical output.
491 key (collections.abc.Callable):
493 the key to use when sorting errors. See `relevance` and
494 transitively `by_relevance` for more details (the default is
495 to sort with the defaults of that function). Changing the
496 default is only useful if you want to change the function
497 that rates errors but still want the error context descent
498 done by this function.
500 Returns:
501 the best matching error, or ``None`` if the iterable was empty
503 .. note::
505 This function is a heuristic. Its return value may change for a given
506 set of inputs from version to version if better heuristics are added.
508 """
509 _, best = _most_relevant(errors, key=key)
510 if best is None:
511 return
513 while best.context:
514 # Group the errors by the subschema which produced them.
515 by_subschema: dict[Any, list[_Error]] = defaultdict(list)
516 for error in best.context:
517 index = error.schema_path[0] if error.schema_path else None
518 by_subschema[index].append(error)
520 # Rank each subschema by how deep its most relevant error is,
521 # and amongst those equally deep, by how few errors it produced
522 # (i.e. how close it was to being valid). Lower ranks are better.
523 best_rank, best_in_subschema, tied = None, None, False
524 for errors_in_subschema in by_subschema.values():
525 error_key, error = _most_relevant(errors_in_subschema, key=key)
526 rank = error_key, len(errors_in_subschema)
527 if best_rank is None or rank < best_rank:
528 best_rank, best_in_subschema, tied = rank, error, False
529 elif rank == best_rank:
530 tied = True
532 # If multiple subschemas rank equally we can't tell which was
533 # intended, so we stop here rather than descend into one of them.
534 if tied:
535 break
536 best = best_in_subschema
537 return best
540def _most_relevant(
541 errors: Iterable[_E],
542 key: Callable[[_E], Any],
543) -> tuple[Any, _E | None]:
544 """
545 Find the most relevant error along with its key, computing each key once.
547 Equally relevant errors are settled by picking the one which appears
548 earlier in the instance, which makes the choice independent of the
549 order in which the errors happened to be produced.
551 Returns ``(None, None)`` if there were no errors.
552 """
553 best_key, best = None, None
554 for error in errors:
555 error_key = key(error)
556 if (
557 best is None
558 or error_key > best_key
559 or (error_key == best_key and error.path < best.path)
560 ):
561 best_key, best = error_key, error
562 return best_key, best