Coverage for /pythoncovmergedfiles/medio/medio/src/jsonschema/jsonschema/exceptions.py: 59%
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
10import heapq
11import itertools
12import warnings
14from attrs import define
15from referencing.exceptions import Unresolvable as _Unresolvable
17from jsonschema import _utils
19if TYPE_CHECKING:
20 from collections.abc import Iterable, Mapping, MutableMapping, Sequence
22 from jsonschema import _types
24WEAK_MATCHES: frozenset[str] = frozenset(["anyOf", "oneOf"])
25STRONG_MATCHES: frozenset[str] = frozenset()
27_unset = _utils.Unset()
30def _pretty(thing: Any, prefix: str):
31 """
32 Format something for an error message as prettily as we currently can.
33 """
34 return indent(pformat(thing, width=72, sort_dicts=False), prefix).lstrip()
37def __getattr__(name):
38 if name == "RefResolutionError":
39 warnings.warn(
40 _RefResolutionError._DEPRECATION_MESSAGE,
41 DeprecationWarning,
42 stacklevel=2,
43 )
44 return _RefResolutionError
45 raise AttributeError(f"module {__name__} has no attribute {name}")
48class _Error(Exception):
50 _word_for_schema_in_error_message: ClassVar[str]
51 _word_for_instance_in_error_message: ClassVar[str]
53 def __init__(
54 self,
55 message: str,
56 validator: str = _unset, # type: ignore[assignment]
57 path: Iterable[str | int] = (),
58 cause: Exception | None = None,
59 context=(),
60 validator_value: Any = _unset,
61 instance: Any = _unset,
62 schema: Mapping[str, Any] | bool = _unset, # type: ignore[assignment]
63 schema_path: Iterable[str | int] = (),
64 parent: _Error | None = None,
65 type_checker: _types.TypeChecker = _unset, # type: ignore[assignment]
66 ) -> None:
67 super().__init__(
68 message,
69 validator,
70 path,
71 cause,
72 context,
73 validator_value,
74 instance,
75 schema,
76 schema_path,
77 parent,
78 )
79 self.message = message
80 self.path = self.relative_path = deque(path)
81 self.schema_path = self.relative_schema_path = deque(schema_path)
82 self.context = list(context)
83 self.cause = self.__cause__ = cause
84 self.validator = validator
85 self.validator_value = validator_value
86 self.instance = instance
87 self.schema = schema
88 self.parent = parent
89 self._type_checker = type_checker
91 for error in context:
92 error.parent = self
94 def __repr__(self) -> str:
95 return f"<{self.__class__.__name__}: {self.message!r}>"
97 def __str__(self) -> str:
98 essential_for_verbose = (
99 self.validator, self.validator_value, self.instance, self.schema,
100 )
101 if any(m is _unset for m in essential_for_verbose):
102 return self.message
104 schema_path = _utils.format_as_index(
105 container=self._word_for_schema_in_error_message,
106 indices=list(self.relative_schema_path)[:-1],
107 )
108 instance_path = _utils.format_as_index(
109 container=self._word_for_instance_in_error_message,
110 indices=self.relative_path,
111 )
112 prefix = 16 * " "
114 return dedent(
115 f"""\
116 {self.message}
118 Failed validating {self.validator!r} in {schema_path}:
119 {_pretty(self.schema, prefix=prefix)}
121 On {instance_path}:
122 {_pretty(self.instance, prefix=prefix)}
123 """.rstrip(),
124 )
126 @classmethod
127 def create_from(cls, other: _Error):
128 return cls(**other._contents())
130 @property
131 def absolute_path(self) -> Sequence[str | int]:
132 parent = self.parent
133 if parent is None:
134 return self.relative_path
136 path = deque(self.relative_path)
137 path.extendleft(reversed(parent.absolute_path))
138 return path
140 @property
141 def absolute_schema_path(self) -> Sequence[str | int]:
142 parent = self.parent
143 if parent is None:
144 return self.relative_schema_path
146 path = deque(self.relative_schema_path)
147 path.extendleft(reversed(parent.absolute_schema_path))
148 return path
150 @property
151 def json_path(self) -> str:
152 path = "$"
153 for elem in self.absolute_path:
154 if isinstance(elem, int):
155 path += "[" + str(elem) + "]"
156 else:
157 path += "." + elem
158 return path
160 def _set(
161 self,
162 type_checker: _types.TypeChecker | None = None,
163 **kwargs: Any,
164 ) -> None:
165 if type_checker is not None and self._type_checker is _unset:
166 self._type_checker = type_checker
168 for k, v in kwargs.items():
169 if getattr(self, k) is _unset:
170 setattr(self, k, v)
172 def _contents(self):
173 attrs = (
174 "message", "cause", "context", "validator", "validator_value",
175 "path", "schema_path", "instance", "schema", "parent",
176 )
177 return {attr: getattr(self, attr) for attr in attrs}
179 def _matches_type(self) -> bool:
180 try:
181 # We ignore this as we want to simply crash if this happens
182 expected = self.schema["type"] # type: ignore[index]
183 except (KeyError, TypeError):
184 return False
186 if isinstance(expected, str):
187 return self._type_checker.is_type(self.instance, expected)
189 return any(
190 self._type_checker.is_type(self.instance, expected_type)
191 for expected_type in expected
192 )
195class ValidationError(_Error):
196 """
197 An instance was invalid under a provided schema.
198 """
200 _word_for_schema_in_error_message = "schema"
201 _word_for_instance_in_error_message = "instance"
204class SchemaError(_Error):
205 """
206 A schema was invalid under its corresponding metaschema.
207 """
209 _word_for_schema_in_error_message = "metaschema"
210 _word_for_instance_in_error_message = "schema"
213@define(slots=False)
214class _RefResolutionError(Exception):
215 """
216 A ref could not be resolved.
217 """
219 _DEPRECATION_MESSAGE = (
220 "jsonschema.exceptions.RefResolutionError is deprecated as of version "
221 "4.18.0. If you wish to catch potential reference resolution errors, "
222 "directly catch referencing.exceptions.Unresolvable."
223 )
225 _cause: Exception
227 def __eq__(self, other):
228 if self.__class__ is not other.__class__:
229 return NotImplemented # pragma: no cover -- uncovered but deprecated # noqa: E501
230 return self._cause == other._cause
232 def __str__(self) -> str:
233 return str(self._cause)
236class _WrappedReferencingError(_RefResolutionError, _Unresolvable): # pragma: no cover -- partially uncovered but to be removed # noqa: E501
237 def __init__(self, cause: _Unresolvable):
238 object.__setattr__(self, "_wrapped", cause)
240 def __eq__(self, other):
241 if other.__class__ is self.__class__:
242 return self._wrapped == other._wrapped
243 elif other.__class__ is self._wrapped.__class__:
244 return self._wrapped == other
245 return NotImplemented
247 def __getattr__(self, attr):
248 return getattr(self._wrapped, attr)
250 def __hash__(self):
251 return hash(self._wrapped)
253 def __repr__(self):
254 return f"<WrappedReferencingError {self._wrapped!r}>"
256 def __str__(self):
257 return f"{self._wrapped.__class__.__name__}: {self._wrapped}"
260class UndefinedTypeCheck(Exception):
261 """
262 A type checker was asked to check a type it did not have registered.
263 """
265 def __init__(self, type: str) -> None:
266 self.type = type
268 def __str__(self) -> str:
269 return f"Type {self.type!r} is unknown to this type checker"
272class UnknownType(Exception):
273 """
274 A validator was asked to validate an instance against an unknown type.
275 """
277 def __init__(self, type, instance, schema):
278 self.type = type
279 self.instance = instance
280 self.schema = schema
282 def __str__(self):
283 prefix = 16 * " "
285 return dedent(
286 f"""\
287 Unknown type {self.type!r} for validator with schema:
288 {_pretty(self.schema, prefix=prefix)}
290 While checking instance:
291 {_pretty(self.instance, prefix=prefix)}
292 """.rstrip(),
293 )
296class FormatError(Exception):
297 """
298 Validating a format failed.
299 """
301 def __init__(self, message, cause=None):
302 super().__init__(message, cause)
303 self.message = message
304 self.cause = self.__cause__ = cause
306 def __str__(self):
307 return self.message
310class ErrorTree:
311 """
312 ErrorTrees make it easier to check which validations failed.
313 """
315 _instance = _unset
317 def __init__(self, errors: Iterable[ValidationError] = ()):
318 self.errors: MutableMapping[str, ValidationError] = {}
319 self._contents: Mapping[str, ErrorTree] = defaultdict(self.__class__)
321 for error in errors:
322 container = self
323 for element in error.path:
324 container = container[element]
325 container.errors[error.validator] = error
327 container._instance = error.instance
329 def __contains__(self, index: str | int):
330 """
331 Check whether ``instance[index]`` has any errors.
332 """
333 return index in self._contents
335 def __getitem__(self, index):
336 """
337 Retrieve the child tree one level down at the given ``index``.
339 If the index is not in the instance that this tree corresponds
340 to and is not known by this tree, whatever error would be raised
341 by ``instance.__getitem__`` will be propagated (usually this is
342 some subclass of `LookupError`.
343 """
344 if self._instance is not _unset and index not in self:
345 self._instance[index]
346 return self._contents[index]
348 def __setitem__(self, index: str | int, value: ErrorTree):
349 """
350 Add an error to the tree at the given ``index``.
352 .. deprecated:: v4.20.0
354 Setting items on an `ErrorTree` is deprecated without replacement.
355 To populate a tree, provide all of its sub-errors when you
356 construct the tree.
357 """
358 warnings.warn(
359 "ErrorTree.__setitem__ is deprecated without replacement.",
360 DeprecationWarning,
361 stacklevel=2,
362 )
363 self._contents[index] = value # type: ignore[index]
365 def __iter__(self):
366 """
367 Iterate (non-recursively) over the indices in the instance with errors.
368 """
369 return iter(self._contents)
371 def __len__(self):
372 """
373 Return the `total_errors`.
374 """
375 return self.total_errors
377 def __repr__(self):
378 total = len(self)
379 errors = "error" if total == 1 else "errors"
380 return f"<{self.__class__.__name__} ({total} total {errors})>"
382 @property
383 def total_errors(self):
384 """
385 The total number of errors in the entire tree, including children.
386 """
387 child_errors = sum(len(tree) for _, tree in self._contents.items())
388 return len(self.errors) + child_errors
391def by_relevance(weak=WEAK_MATCHES, strong=STRONG_MATCHES):
392 """
393 Create a key function that can be used to sort errors by relevance.
395 Arguments:
396 weak (set):
397 a collection of validation keywords to consider to be
398 "weak". If there are two errors at the same level of the
399 instance and one is in the set of weak validation keywords,
400 the other error will take priority. By default, :kw:`anyOf`
401 and :kw:`oneOf` are considered weak keywords and will be
402 superseded by other same-level validation errors.
404 strong (set):
405 a collection of validation keywords to consider to be
406 "strong"
408 """
410 def relevance(error):
411 validator = error.validator
412 return ( # prefer errors which are ...
413 -len(error.path), # 'deeper' and thereby more specific
414 error.path, # earlier (for sibling errors)
415 validator not in weak, # for a non-low-priority keyword
416 validator in strong, # for a high priority keyword
417 not error._matches_type(), # at least match the instance's type
418 ) # otherwise we'll treat them the same
420 return relevance
423relevance = by_relevance()
424"""
425A key function (e.g. to use with `sorted`) which sorts errors by relevance.
427Example:
429.. code:: python
431 sorted(validator.iter_errors(12), key=jsonschema.exceptions.relevance)
432"""
435def best_match(errors, key=relevance):
436 """
437 Try to find an error that appears to be the best match among given errors.
439 In general, errors that are higher up in the instance (i.e. for which
440 `ValidationError.path` is shorter) are considered better matches,
441 since they indicate "more" is wrong with the instance.
443 If the resulting match is either :kw:`oneOf` or :kw:`anyOf`, the
444 *opposite* assumption is made -- i.e. the deepest error is picked,
445 since these keywords only need to match once, and any other errors
446 may not be relevant.
448 Arguments:
449 errors (collections.abc.Iterable):
451 the errors to select from. Do not provide a mixture of
452 errors from different validation attempts (i.e. from
453 different instances or schemas), since it won't produce
454 sensical output.
456 key (collections.abc.Callable):
458 the key to use when sorting errors. See `relevance` and
459 transitively `by_relevance` for more details (the default is
460 to sort with the defaults of that function). Changing the
461 default is only useful if you want to change the function
462 that rates errors but still want the error context descent
463 done by this function.
465 Returns:
466 the best matching error, or ``None`` if the iterable was empty
468 .. note::
470 This function is a heuristic. Its return value may change for a given
471 set of inputs from version to version if better heuristics are added.
473 """
474 errors = iter(errors)
475 best = next(errors, None)
476 if best is None:
477 return
478 best = max(itertools.chain([best], errors), key=key)
480 while best.context:
481 # Calculate the minimum via nsmallest, because we don't recurse if
482 # all nested errors have the same relevance (i.e. if min == max == all)
483 smallest = heapq.nsmallest(2, best.context, key=key)
484 if len(smallest) == 2 and key(smallest[0]) == key(smallest[1]): # noqa: PLR2004
485 return best
486 best = smallest[0]
487 return best