Coverage for /pythoncovmergedfiles/medio/medio/src/jsonschema/jsonschema/exceptions.py: 44%
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 warnings
13from attrs import define
14from referencing.exceptions import Unresolvable as _Unresolvable
16from jsonschema import _utils
18if TYPE_CHECKING:
19 from collections.abc import Iterable, Mapping, MutableMapping, Sequence
21 from jsonschema import _types
23WEAK_MATCHES: frozenset[str] = frozenset(["anyOf", "oneOf"])
24STRONG_MATCHES: frozenset[str] = frozenset()
26_unset = _utils.Unset()
29def _pretty(thing: Any, prefix: str):
30 """
31 Format something for an error message as prettily as we currently can.
32 """
33 return indent(pformat(thing, width=72, sort_dicts=False), prefix).lstrip()
36def __getattr__(name):
37 if name == "RefResolutionError":
38 warnings.warn(
39 _RefResolutionError._DEPRECATION_MESSAGE,
40 DeprecationWarning,
41 stacklevel=2,
42 )
43 return _RefResolutionError
44 raise AttributeError(f"module {__name__} has no attribute {name}")
47class _Error(Exception):
49 _word_for_schema_in_error_message: ClassVar[str]
50 _word_for_instance_in_error_message: ClassVar[str]
52 def __init__(
53 self,
54 message: str,
55 validator: str = _unset, # type: ignore[assignment]
56 path: Iterable[str | int] = (),
57 cause: Exception | None = None,
58 context=(),
59 validator_value: Any = _unset,
60 instance: Any = _unset,
61 schema: Mapping[str, Any] | bool = _unset, # type: ignore[assignment]
62 schema_path: Iterable[str | int] = (),
63 parent: _Error | None = None,
64 type_checker: _types.TypeChecker = _unset, # type: ignore[assignment]
65 ) -> None:
66 super().__init__(
67 message,
68 validator,
69 path,
70 cause,
71 context,
72 validator_value,
73 instance,
74 schema,
75 schema_path,
76 parent,
77 )
78 self.message = message
79 self.path = self.relative_path = deque(path)
80 self.schema_path = self.relative_schema_path = deque(schema_path)
81 self.context = list(context)
82 self.cause = self.__cause__ = cause
83 self.validator = validator
84 self.validator_value = validator_value
85 self.instance = instance
86 self.schema = schema
87 self.parent = parent
88 self._type_checker = type_checker
90 for error in context:
91 error.parent = self
93 def __repr__(self) -> str:
94 return f"<{self.__class__.__name__}: {self.message!r}>"
96 def __str__(self) -> str:
97 essential_for_verbose = (
98 self.validator, self.validator_value, self.instance, self.schema,
99 )
100 if any(m is _unset for m in essential_for_verbose):
101 return self.message
103 schema_path = _utils.format_as_index(
104 container=self._word_for_schema_in_error_message,
105 indices=list(self.relative_schema_path)[:-1],
106 )
107 instance_path = _utils.format_as_index(
108 container=self._word_for_instance_in_error_message,
109 indices=self.relative_path,
110 )
111 prefix = 16 * " "
113 return dedent(
114 f"""\
115 {self.message}
117 Failed validating {self.validator!r} in {schema_path}:
118 {_pretty(self.schema, prefix=prefix)}
120 On {instance_path}:
121 {_pretty(self.instance, prefix=prefix)}
122 """.rstrip(),
123 )
125 @classmethod
126 def create_from(cls, other: _Error):
127 return cls(**other._contents())
129 @property
130 def absolute_path(self) -> Sequence[str | int]:
131 parent = self.parent
132 if parent is None:
133 return self.relative_path
135 path = deque(self.relative_path)
136 path.extendleft(reversed(parent.absolute_path))
137 return path
139 @property
140 def absolute_schema_path(self) -> Sequence[str | int]:
141 parent = self.parent
142 if parent is None:
143 return self.relative_schema_path
145 path = deque(self.relative_schema_path)
146 path.extendleft(reversed(parent.absolute_schema_path))
147 return path
149 @property
150 def json_path(self) -> str:
151 path = "$"
152 for elem in self.absolute_path:
153 if isinstance(elem, int):
154 path += "[" + str(elem) + "]"
155 else:
156 path += "." + elem
157 return path
159 def _set(
160 self,
161 type_checker: _types.TypeChecker | None = None,
162 **kwargs: Any,
163 ) -> None:
164 if type_checker is not None and self._type_checker is _unset:
165 self._type_checker = type_checker
167 for k, v in kwargs.items():
168 if getattr(self, k) is _unset:
169 setattr(self, k, v)
171 def _contents(self):
172 attrs = (
173 "message", "cause", "context", "validator", "validator_value",
174 "path", "schema_path", "instance", "schema", "parent",
175 )
176 return {attr: getattr(self, attr) for attr in attrs}
178 def _matches_type(self) -> bool:
179 try:
180 # We ignore this as we want to simply crash if this happens
181 expected = self.schema["type"] # type: ignore[index]
182 except (KeyError, TypeError):
183 return False
185 if isinstance(expected, str):
186 return self._type_checker.is_type(self.instance, expected)
188 return any(
189 self._type_checker.is_type(self.instance, expected_type)
190 for expected_type in expected
191 )
194class ValidationError(_Error):
195 """
196 An instance was invalid under a provided schema.
197 """
199 _word_for_schema_in_error_message = "schema"
200 _word_for_instance_in_error_message = "instance"
203class SchemaError(_Error):
204 """
205 A schema was invalid under its corresponding metaschema.
206 """
208 _word_for_schema_in_error_message = "metaschema"
209 _word_for_instance_in_error_message = "schema"
212@define(slots=False)
213class _RefResolutionError(Exception):
214 """
215 A ref could not be resolved.
216 """
218 _DEPRECATION_MESSAGE = (
219 "jsonschema.exceptions.RefResolutionError is deprecated as of version "
220 "4.18.0. If you wish to catch potential reference resolution errors, "
221 "directly catch referencing.exceptions.Unresolvable."
222 )
224 _cause: Exception
226 def __eq__(self, other):
227 if self.__class__ is not other.__class__:
228 return NotImplemented # pragma: no cover -- uncovered but deprecated # noqa: E501
229 return self._cause == other._cause
231 def __str__(self) -> str:
232 return str(self._cause)
235class _WrappedReferencingError(_RefResolutionError, _Unresolvable): # pragma: no cover -- partially uncovered but to be removed # noqa: E501
236 def __init__(self, cause: _Unresolvable):
237 object.__setattr__(self, "_wrapped", cause)
239 def __eq__(self, other):
240 if other.__class__ is self.__class__:
241 return self._wrapped == other._wrapped
242 elif other.__class__ is self._wrapped.__class__:
243 return self._wrapped == other
244 return NotImplemented
246 def __getattr__(self, attr):
247 return getattr(self._wrapped, attr)
249 def __hash__(self):
250 return hash(self._wrapped)
252 def __repr__(self):
253 return f"<WrappedReferencingError {self._wrapped!r}>"
255 def __str__(self):
256 return f"{self._wrapped.__class__.__name__}: {self._wrapped}"
259class UndefinedTypeCheck(Exception):
260 """
261 A type checker was asked to check a type it did not have registered.
262 """
264 def __init__(self, type: str) -> None:
265 self.type = type
267 def __str__(self) -> str:
268 return f"Type {self.type!r} is unknown to this type checker"
271class UnknownType(Exception):
272 """
273 A validator was asked to validate an instance against an unknown type.
274 """
276 def __init__(self, type, instance, schema):
277 self.type = type
278 self.instance = instance
279 self.schema = schema
281 def __str__(self):
282 prefix = 16 * " "
284 return dedent(
285 f"""\
286 Unknown type {self.type!r} for validator with schema:
287 {_pretty(self.schema, prefix=prefix)}
289 While checking instance:
290 {_pretty(self.instance, prefix=prefix)}
291 """.rstrip(),
292 )
295class FormatError(Exception):
296 """
297 Validating a format failed.
298 """
300 def __init__(self, message, cause=None):
301 super().__init__(message, cause)
302 self.message = message
303 self.cause = self.__cause__ = cause
305 def __str__(self):
306 return self.message
309class ErrorTree:
310 """
311 ErrorTrees make it easier to check which validations failed.
312 """
314 _instance = _unset
316 def __init__(self, errors: Iterable[ValidationError] = ()):
317 self.errors: MutableMapping[str, ValidationError] = {}
318 self._contents: Mapping[str, ErrorTree] = defaultdict(self.__class__)
320 for error in errors:
321 container = self
322 for element in error.path:
323 container = container[element]
324 container.errors[error.validator] = error
326 container._instance = error.instance
328 def __contains__(self, index: str | int):
329 """
330 Check whether ``instance[index]`` has any errors.
331 """
332 return index in self._contents
334 def __getitem__(self, index):
335 """
336 Retrieve the child tree one level down at the given ``index``.
338 If the index is not in the instance that this tree corresponds
339 to and is not known by this tree, whatever error would be raised
340 by ``instance.__getitem__`` will be propagated (usually this is
341 some subclass of `LookupError`.
342 """
343 if self._instance is not _unset and index not in self:
344 self._instance[index]
345 return self._contents[index]
347 def __setitem__(self, index: str | int, value: ErrorTree):
348 """
349 Add an error to the tree at the given ``index``.
351 .. deprecated:: v4.20.0
353 Setting items on an `ErrorTree` is deprecated without replacement.
354 To populate a tree, provide all of its sub-errors when you
355 construct the tree.
356 """
357 warnings.warn(
358 "ErrorTree.__setitem__ is deprecated without replacement.",
359 DeprecationWarning,
360 stacklevel=2,
361 )
362 self._contents[index] = value # type: ignore[index]
364 def __iter__(self):
365 """
366 Iterate (non-recursively) over the indices in the instance with errors.
367 """
368 return iter(self._contents)
370 def __len__(self):
371 """
372 Return the `total_errors`.
373 """
374 return self.total_errors
376 def __repr__(self):
377 total = len(self)
378 errors = "error" if total == 1 else "errors"
379 return f"<{self.__class__.__name__} ({total} total {errors})>"
381 @property
382 def total_errors(self):
383 """
384 The total number of errors in the entire tree, including children.
385 """
386 child_errors = sum(len(tree) for _, tree in self._contents.items())
387 return len(self.errors) + child_errors
390def by_relevance(weak=WEAK_MATCHES, strong=STRONG_MATCHES):
391 """
392 Create a key function that can be used to sort errors by relevance.
394 Arguments:
395 weak (set):
396 a collection of validation keywords to consider to be
397 "weak". If there are two errors at the same level of the
398 instance and one is in the set of weak validation keywords,
399 the other error will take priority. By default, :kw:`anyOf`
400 and :kw:`oneOf` are considered weak keywords and will be
401 superseded by other same-level validation errors.
403 strong (set):
404 a collection of validation keywords to consider to be
405 "strong"
407 """
409 def relevance(error):
410 validator = error.validator
411 return ( # prefer errors which are ...
412 -len(error.path), # 'deeper' and thereby more specific
413 error.path, # earlier (for sibling errors)
414 validator not in weak, # for a non-low-priority keyword
415 validator in strong, # for a high priority keyword
416 not error._matches_type(), # at least match the instance's type
417 ) # otherwise we'll treat them the same
419 return relevance
422relevance = by_relevance()
423"""
424A key function (e.g. to use with `sorted`) which sorts errors by relevance.
426Example:
428.. code:: python
430 sorted(validator.iter_errors(12), key=jsonschema.exceptions.relevance)
431"""
434def best_match(errors, key=relevance):
435 """
436 Try to find an error that appears to be the best match among given errors.
438 In general, errors that are higher up in the instance (i.e. for which
439 `ValidationError.path` is shorter) are considered better matches,
440 since they indicate "more" is wrong with the instance.
442 If the resulting match is either :kw:`oneOf` or :kw:`anyOf`, the
443 *opposite* assumption is made -- i.e. the deepest error is picked,
444 since these keywords only need to match once, and any other errors
445 may not be relevant.
447 Arguments:
448 errors (collections.abc.Iterable):
450 the errors to select from. Do not provide a mixture of
451 errors from different validation attempts (i.e. from
452 different instances or schemas), since it won't produce
453 sensical output.
455 key (collections.abc.Callable):
457 the key to use when sorting errors. See `relevance` and
458 transitively `by_relevance` for more details (the default is
459 to sort with the defaults of that function). Changing the
460 default is only useful if you want to change the function
461 that rates errors but still want the error context descent
462 done by this function.
464 Returns:
465 the best matching error, or ``None`` if the iterable was empty
467 .. note::
469 This function is a heuristic. Its return value may change for a given
470 set of inputs from version to version if better heuristics are added.
472 """
473 best = max(errors, key=key, default=None)
474 if best is None:
475 return
477 while best.context:
478 # Calculate the minimum via nsmallest, because we don't recurse if
479 # all nested errors have the same relevance (i.e. if min == max == all)
480 smallest = heapq.nsmallest(2, best.context, key=key)
481 if len(smallest) == 2 and key(smallest[0]) == key(smallest[1]): # noqa: PLR2004
482 return best
483 best = smallest[0]
484 return best