Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/jsonschema/exceptions.py: 40%
178 statements
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-22 06:29 +0000
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-22 06:29 +0000
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, 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
22WEAK_MATCHES: frozenset[str] = frozenset(["anyOf", "oneOf"])
23STRONG_MATCHES: frozenset[str] = frozenset()
25_unset = _utils.Unset()
28def __getattr__(name):
29 if name == "RefResolutionError":
30 warnings.warn(
31 _RefResolutionError._DEPRECATION_MESSAGE,
32 DeprecationWarning,
33 stacklevel=2,
34 )
35 return _RefResolutionError
36 raise AttributeError(f"module {__name__} has no attribute {name}")
39class _Error(Exception):
41 _word_for_schema_in_error_message: ClassVar[str]
42 _word_for_instance_in_error_message: ClassVar[str]
44 def __init__(
45 self,
46 message: str,
47 validator=_unset,
48 path=(),
49 cause=None,
50 context=(),
51 validator_value=_unset,
52 instance=_unset,
53 schema=_unset,
54 schema_path=(),
55 parent=None,
56 type_checker=_unset,
57 ):
58 super().__init__(
59 message,
60 validator,
61 path,
62 cause,
63 context,
64 validator_value,
65 instance,
66 schema,
67 schema_path,
68 parent,
69 )
70 self.message = message
71 self.path = self.relative_path = deque(path)
72 self.schema_path = self.relative_schema_path = deque(schema_path)
73 self.context = list(context)
74 self.cause = self.__cause__ = cause
75 self.validator = validator
76 self.validator_value = validator_value
77 self.instance = instance
78 self.schema = schema
79 self.parent = parent
80 self._type_checker = type_checker
82 for error in context:
83 error.parent = self
85 def __repr__(self):
86 return f"<{self.__class__.__name__}: {self.message!r}>"
88 def __str__(self):
89 essential_for_verbose = (
90 self.validator, self.validator_value, self.instance, self.schema,
91 )
92 if any(m is _unset for m in essential_for_verbose):
93 return self.message
95 schema_path = _utils.format_as_index(
96 container=self._word_for_schema_in_error_message,
97 indices=list(self.relative_schema_path)[:-1],
98 )
99 instance_path = _utils.format_as_index(
100 container=self._word_for_instance_in_error_message,
101 indices=self.relative_path,
102 )
103 prefix = 16 * " "
105 return dedent(
106 f"""\
107 {self.message}
109 Failed validating {self.validator!r} in {schema_path}:
110 {indent(pformat(self.schema, width=72), prefix).lstrip()}
112 On {instance_path}:
113 {indent(pformat(self.instance, width=72), prefix).lstrip()}
114 """.rstrip(),
115 )
117 @classmethod
118 def create_from(cls, other):
119 return cls(**other._contents())
121 @property
122 def absolute_path(self):
123 parent = self.parent
124 if parent is None:
125 return self.relative_path
127 path = deque(self.relative_path)
128 path.extendleft(reversed(parent.absolute_path))
129 return path
131 @property
132 def absolute_schema_path(self):
133 parent = self.parent
134 if parent is None:
135 return self.relative_schema_path
137 path = deque(self.relative_schema_path)
138 path.extendleft(reversed(parent.absolute_schema_path))
139 return path
141 @property
142 def json_path(self):
143 path = "$"
144 for elem in self.absolute_path:
145 if isinstance(elem, int):
146 path += "[" + str(elem) + "]"
147 else:
148 path += "." + elem
149 return path
151 def _set(self, type_checker=None, **kwargs):
152 if type_checker is not None and self._type_checker is _unset:
153 self._type_checker = type_checker
155 for k, v in kwargs.items():
156 if getattr(self, k) is _unset:
157 setattr(self, k, v)
159 def _contents(self):
160 attrs = (
161 "message", "cause", "context", "validator", "validator_value",
162 "path", "schema_path", "instance", "schema", "parent",
163 )
164 return {attr: getattr(self, attr) for attr in attrs}
166 def _matches_type(self):
167 try:
168 expected = self.schema["type"]
169 except (KeyError, TypeError):
170 return False
172 if isinstance(expected, str):
173 return self._type_checker.is_type(self.instance, expected)
175 return any(
176 self._type_checker.is_type(self.instance, expected_type)
177 for expected_type in expected
178 )
181class ValidationError(_Error):
182 """
183 An instance was invalid under a provided schema.
184 """
186 _word_for_schema_in_error_message = "schema"
187 _word_for_instance_in_error_message = "instance"
190class SchemaError(_Error):
191 """
192 A schema was invalid under its corresponding metaschema.
193 """
195 _word_for_schema_in_error_message = "metaschema"
196 _word_for_instance_in_error_message = "schema"
199@define(slots=False)
200class _RefResolutionError(Exception):
201 """
202 A ref could not be resolved.
203 """
205 _DEPRECATION_MESSAGE = (
206 "jsonschema.exceptions.RefResolutionError is deprecated as of version "
207 "4.18.0. If you wish to catch potential reference resolution errors, "
208 "directly catch referencing.exceptions.Unresolvable."
209 )
211 _cause: Exception
213 def __eq__(self, other):
214 if self.__class__ is not other.__class__:
215 return NotImplemented # pragma: no cover -- uncovered but deprecated # noqa: E501
216 return self._cause == other._cause
218 def __str__(self):
219 return str(self._cause)
222class _WrappedReferencingError(_RefResolutionError, _Unresolvable): # pragma: no cover -- partially uncovered but to be removed # noqa: E501
223 def __init__(self, cause: _Unresolvable):
224 object.__setattr__(self, "_wrapped", cause)
226 def __eq__(self, other):
227 if other.__class__ is self.__class__:
228 return self._wrapped == other._wrapped
229 elif other.__class__ is self._wrapped.__class__:
230 return self._wrapped == other
231 return NotImplemented
233 def __getattr__(self, attr):
234 return getattr(self._wrapped, attr)
236 def __hash__(self):
237 return hash(self._wrapped)
239 def __repr__(self):
240 return f"<WrappedReferencingError {self._wrapped!r}>"
242 def __str__(self):
243 return f"{self._wrapped.__class__.__name__}: {self._wrapped}"
246class UndefinedTypeCheck(Exception):
247 """
248 A type checker was asked to check a type it did not have registered.
249 """
251 def __init__(self, type):
252 self.type = type
254 def __str__(self):
255 return f"Type {self.type!r} is unknown to this type checker"
258class UnknownType(Exception):
259 """
260 A validator was asked to validate an instance against an unknown type.
261 """
263 def __init__(self, type, instance, schema):
264 self.type = type
265 self.instance = instance
266 self.schema = schema
268 def __str__(self):
269 prefix = 16 * " "
271 return dedent(
272 f"""\
273 Unknown type {self.type!r} for validator with schema:
274 {indent(pformat(self.schema, width=72), prefix).lstrip()}
276 While checking instance:
277 {indent(pformat(self.instance, width=72), prefix).lstrip()}
278 """.rstrip(),
279 )
282class FormatError(Exception):
283 """
284 Validating a format failed.
285 """
287 def __init__(self, message, cause=None):
288 super().__init__(message, cause)
289 self.message = message
290 self.cause = self.__cause__ = cause
292 def __str__(self):
293 return self.message
296class ErrorTree:
297 """
298 ErrorTrees make it easier to check which validations failed.
299 """
301 _instance = _unset
303 def __init__(self, errors: Iterable[ValidationError] = ()):
304 self.errors: MutableMapping[str, ValidationError] = {}
305 self._contents: Mapping[str, ErrorTree] = defaultdict(self.__class__)
307 for error in errors:
308 container = self
309 for element in error.path:
310 container = container[element]
311 container.errors[error.validator] = error
313 container._instance = error.instance
315 def __contains__(self, index: str | int):
316 """
317 Check whether ``instance[index]`` has any errors.
318 """
319 return index in self._contents
321 def __getitem__(self, index):
322 """
323 Retrieve the child tree one level down at the given ``index``.
325 If the index is not in the instance that this tree corresponds
326 to and is not known by this tree, whatever error would be raised
327 by ``instance.__getitem__`` will be propagated (usually this is
328 some subclass of `LookupError`.
329 """
330 if self._instance is not _unset and index not in self:
331 self._instance[index]
332 return self._contents[index]
334 def __setitem__(self, index: str | int, value: ErrorTree):
335 """
336 Add an error to the tree at the given ``index``.
338 .. deprecated:: v4.20.0
340 Setting items on an `ErrorTree` is deprecated without replacement.
341 To populate a tree, provide all of its sub-errors when you
342 construct the tree.
343 """
344 warnings.warn(
345 "ErrorTree.__setitem__ is deprecated without replacement.",
346 DeprecationWarning,
347 stacklevel=2,
348 )
349 self._contents[index] = value # type: ignore[index]
351 def __iter__(self):
352 """
353 Iterate (non-recursively) over the indices in the instance with errors.
354 """
355 return iter(self._contents)
357 def __len__(self):
358 """
359 Return the `total_errors`.
360 """
361 return self.total_errors
363 def __repr__(self):
364 total = len(self)
365 errors = "error" if total == 1 else "errors"
366 return f"<{self.__class__.__name__} ({total} total {errors})>"
368 @property
369 def total_errors(self):
370 """
371 The total number of errors in the entire tree, including children.
372 """
373 child_errors = sum(len(tree) for _, tree in self._contents.items())
374 return len(self.errors) + child_errors
377def by_relevance(weak=WEAK_MATCHES, strong=STRONG_MATCHES):
378 """
379 Create a key function that can be used to sort errors by relevance.
381 Arguments:
382 weak (set):
383 a collection of validation keywords to consider to be
384 "weak". If there are two errors at the same level of the
385 instance and one is in the set of weak validation keywords,
386 the other error will take priority. By default, :kw:`anyOf`
387 and :kw:`oneOf` are considered weak keywords and will be
388 superseded by other same-level validation errors.
390 strong (set):
391 a collection of validation keywords to consider to be
392 "strong"
393 """
395 def relevance(error):
396 validator = error.validator
397 return (
398 -len(error.path),
399 validator not in weak,
400 validator in strong,
401 not error._matches_type(),
402 )
404 return relevance
407relevance = by_relevance()
408"""
409A key function (e.g. to use with `sorted`) which sorts errors by relevance.
411Example:
413.. code:: python
415 sorted(validator.iter_errors(12), key=jsonschema.exceptions.relevance)
416"""
419def best_match(errors, key=relevance):
420 """
421 Try to find an error that appears to be the best match among given errors.
423 In general, errors that are higher up in the instance (i.e. for which
424 `ValidationError.path` is shorter) are considered better matches,
425 since they indicate "more" is wrong with the instance.
427 If the resulting match is either :kw:`oneOf` or :kw:`anyOf`, the
428 *opposite* assumption is made -- i.e. the deepest error is picked,
429 since these keywords only need to match once, and any other errors
430 may not be relevant.
432 Arguments:
433 errors (collections.abc.Iterable):
435 the errors to select from. Do not provide a mixture of
436 errors from different validation attempts (i.e. from
437 different instances or schemas), since it won't produce
438 sensical output.
440 key (collections.abc.Callable):
442 the key to use when sorting errors. See `relevance` and
443 transitively `by_relevance` for more details (the default is
444 to sort with the defaults of that function). Changing the
445 default is only useful if you want to change the function
446 that rates errors but still want the error context descent
447 done by this function.
449 Returns:
450 the best matching error, or ``None`` if the iterable was empty
452 .. note::
454 This function is a heuristic. Its return value may change for a given
455 set of inputs from version to version if better heuristics are added.
456 """
457 errors = iter(errors)
458 best = next(errors, None)
459 if best is None:
460 return
461 best = max(itertools.chain([best], errors), key=key)
463 while best.context:
464 # Calculate the minimum via nsmallest, because we don't recurse if
465 # all nested errors have the same relevance (i.e. if min == max == all)
466 smallest = heapq.nsmallest(2, best.context, key=key)
467 if len(smallest) == 2 and key(smallest[0]) == key(smallest[1]): # noqa: PLR2004
468 return best
469 best = smallest[0]
470 return best