Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/jsonschema/exceptions.py: 41%
177 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:51 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:51 +0000
1"""
2Validation errors, and some surrounding helpers.
3"""
4from __future__ import annotations
6from collections import defaultdict, deque
7from collections.abc import Iterable, Mapping, MutableMapping
8from pprint import pformat
9from textwrap import dedent, indent
10from typing import ClassVar
11import heapq
12import itertools
13import warnings
15from attrs import define
16from referencing.exceptions import Unresolvable as _Unresolvable
18from jsonschema import _utils
20WEAK_MATCHES: frozenset[str] = frozenset(["anyOf", "oneOf"])
21STRONG_MATCHES: frozenset[str] = frozenset()
23_unset = _utils.Unset()
26def __getattr__(name):
27 if name == "RefResolutionError":
28 warnings.warn(
29 _RefResolutionError._DEPRECATION_MESSAGE,
30 DeprecationWarning,
31 stacklevel=2,
32 )
33 return _RefResolutionError
34 raise AttributeError(f"module {__name__} has no attribute {name}")
37class _Error(Exception):
39 _word_for_schema_in_error_message: ClassVar[str]
40 _word_for_instance_in_error_message: ClassVar[str]
42 def __init__(
43 self,
44 message: str,
45 validator=_unset,
46 path=(),
47 cause=None,
48 context=(),
49 validator_value=_unset,
50 instance=_unset,
51 schema=_unset,
52 schema_path=(),
53 parent=None,
54 type_checker=_unset,
55 ):
56 super().__init__(
57 message,
58 validator,
59 path,
60 cause,
61 context,
62 validator_value,
63 instance,
64 schema,
65 schema_path,
66 parent,
67 )
68 self.message = message
69 self.path = self.relative_path = deque(path)
70 self.schema_path = self.relative_schema_path = deque(schema_path)
71 self.context = list(context)
72 self.cause = self.__cause__ = cause
73 self.validator = validator
74 self.validator_value = validator_value
75 self.instance = instance
76 self.schema = schema
77 self.parent = parent
78 self._type_checker = type_checker
80 for error in context:
81 error.parent = self
83 def __repr__(self):
84 return f"<{self.__class__.__name__}: {self.message!r}>"
86 def __str__(self):
87 essential_for_verbose = (
88 self.validator, self.validator_value, self.instance, self.schema,
89 )
90 if any(m is _unset for m in essential_for_verbose):
91 return self.message
93 schema_path = _utils.format_as_index(
94 container=self._word_for_schema_in_error_message,
95 indices=list(self.relative_schema_path)[:-1],
96 )
97 instance_path = _utils.format_as_index(
98 container=self._word_for_instance_in_error_message,
99 indices=self.relative_path,
100 )
101 prefix = 16 * " "
103 return dedent(
104 f"""\
105 {self.message}
107 Failed validating {self.validator!r} in {schema_path}:
108 {indent(pformat(self.schema, width=72), prefix).lstrip()}
110 On {instance_path}:
111 {indent(pformat(self.instance, width=72), prefix).lstrip()}
112 """.rstrip(),
113 )
115 @classmethod
116 def create_from(cls, other):
117 return cls(**other._contents())
119 @property
120 def absolute_path(self):
121 parent = self.parent
122 if parent is None:
123 return self.relative_path
125 path = deque(self.relative_path)
126 path.extendleft(reversed(parent.absolute_path))
127 return path
129 @property
130 def absolute_schema_path(self):
131 parent = self.parent
132 if parent is None:
133 return self.relative_schema_path
135 path = deque(self.relative_schema_path)
136 path.extendleft(reversed(parent.absolute_schema_path))
137 return path
139 @property
140 def json_path(self):
141 path = "$"
142 for elem in self.absolute_path:
143 if isinstance(elem, int):
144 path += "[" + str(elem) + "]"
145 else:
146 path += "." + elem
147 return path
149 def _set(self, type_checker=None, **kwargs):
150 if type_checker is not None and self._type_checker is _unset:
151 self._type_checker = type_checker
153 for k, v in kwargs.items():
154 if getattr(self, k) is _unset:
155 setattr(self, k, v)
157 def _contents(self):
158 attrs = (
159 "message", "cause", "context", "validator", "validator_value",
160 "path", "schema_path", "instance", "schema", "parent",
161 )
162 return dict((attr, getattr(self, attr)) for attr in attrs)
164 def _matches_type(self):
165 try:
166 expected = self.schema["type"]
167 except (KeyError, TypeError):
168 return False
170 if isinstance(expected, str):
171 return self._type_checker.is_type(self.instance, expected)
173 return any(
174 self._type_checker.is_type(self.instance, expected_type)
175 for expected_type in expected
176 )
179class ValidationError(_Error):
180 """
181 An instance was invalid under a provided schema.
182 """
184 _word_for_schema_in_error_message = "schema"
185 _word_for_instance_in_error_message = "instance"
188class SchemaError(_Error):
189 """
190 A schema was invalid under its corresponding metaschema.
191 """
193 _word_for_schema_in_error_message = "metaschema"
194 _word_for_instance_in_error_message = "schema"
197@define(slots=False)
198class _RefResolutionError(Exception):
199 """
200 A ref could not be resolved.
201 """
203 _DEPRECATION_MESSAGE = (
204 "jsonschema.exceptions.RefResolutionError is deprecated as of version "
205 "4.18.0. If you wish to catch potential reference resolution errors, "
206 "directly catch referencing.exceptions.Unresolvable."
207 )
209 _cause: Exception
211 def __eq__(self, other):
212 if self.__class__ is not other.__class__:
213 return NotImplemented # pragma: no cover -- uncovered but deprecated # noqa: E501
214 return self._cause == other._cause
216 def __str__(self):
217 return str(self._cause)
220class _WrappedReferencingError(_RefResolutionError, _Unresolvable): # pragma: no cover -- partially uncovered but to be removed # noqa: E501
221 def __init__(self, cause: _Unresolvable):
222 object.__setattr__(self, "_wrapped", cause)
224 def __eq__(self, other):
225 if other.__class__ is self.__class__:
226 return self._wrapped == other._wrapped
227 elif other.__class__ is self._wrapped.__class__:
228 return self._wrapped == other
229 return NotImplemented
231 def __getattr__(self, attr):
232 return getattr(self._wrapped, attr)
234 def __hash__(self):
235 return hash(self._wrapped)
237 def __repr__(self):
238 return f"<WrappedReferencingError {self._wrapped!r}>"
240 def __str__(self):
241 return f"{self._wrapped.__class__.__name__}: {self._wrapped}"
244class UndefinedTypeCheck(Exception):
245 """
246 A type checker was asked to check a type it did not have registered.
247 """
249 def __init__(self, type):
250 self.type = type
252 def __str__(self):
253 return f"Type {self.type!r} is unknown to this type checker"
256class UnknownType(Exception):
257 """
258 A validator was asked to validate an instance against an unknown type.
259 """
261 def __init__(self, type, instance, schema):
262 self.type = type
263 self.instance = instance
264 self.schema = schema
266 def __str__(self):
267 prefix = 16 * " "
269 return dedent(
270 f"""\
271 Unknown type {self.type!r} for validator with schema:
272 {indent(pformat(self.schema, width=72), prefix).lstrip()}
274 While checking instance:
275 {indent(pformat(self.instance, width=72), prefix).lstrip()}
276 """.rstrip(),
277 )
280class FormatError(Exception):
281 """
282 Validating a format failed.
283 """
285 def __init__(self, message, cause=None):
286 super().__init__(message, cause)
287 self.message = message
288 self.cause = self.__cause__ = cause
290 def __str__(self):
291 return self.message
294class ErrorTree:
295 """
296 ErrorTrees make it easier to check which validations failed.
297 """
299 _instance = _unset
301 def __init__(self, errors: Iterable[ValidationError] = ()):
302 self.errors: MutableMapping[str, ValidationError] = {}
303 self._contents: Mapping[str, ErrorTree] = defaultdict(self.__class__)
305 for error in errors:
306 container = self
307 for element in error.path:
308 container = container[element]
309 container.errors[error.validator] = error
311 container._instance = error.instance
313 def __contains__(self, index: str | int):
314 """
315 Check whether ``instance[index]`` has any errors.
316 """
317 return index in self._contents
319 def __getitem__(self, index):
320 """
321 Retrieve the child tree one level down at the given ``index``.
323 If the index is not in the instance that this tree corresponds
324 to and is not known by this tree, whatever error would be raised
325 by ``instance.__getitem__`` will be propagated (usually this is
326 some subclass of `LookupError`.
327 """
328 if self._instance is not _unset and index not in self:
329 self._instance[index]
330 return self._contents[index]
332 def __setitem__(self, index: str | int, value: ErrorTree):
333 """
334 Add an error to the tree at the given ``index``.
336 .. deprecated:: v4.20.0
338 Setting items on an `ErrorTree` is deprecated without replacement.
339 To populate a tree, provide all of its sub-errors when you
340 construct the tree.
341 """
342 warnings.warn(
343 "ErrorTree.__setitem__ is deprecated without replacement.",
344 DeprecationWarning,
345 stacklevel=2,
346 )
347 self._contents[index] = value # type: ignore[index]
349 def __iter__(self):
350 """
351 Iterate (non-recursively) over the indices in the instance with errors.
352 """
353 return iter(self._contents)
355 def __len__(self):
356 """
357 Return the `total_errors`.
358 """
359 return self.total_errors
361 def __repr__(self):
362 total = len(self)
363 errors = "error" if total == 1 else "errors"
364 return f"<{self.__class__.__name__} ({total} total {errors})>"
366 @property
367 def total_errors(self):
368 """
369 The total number of errors in the entire tree, including children.
370 """
371 child_errors = sum(len(tree) for _, tree in self._contents.items())
372 return len(self.errors) + child_errors
375def by_relevance(weak=WEAK_MATCHES, strong=STRONG_MATCHES):
376 """
377 Create a key function that can be used to sort errors by relevance.
379 Arguments:
380 weak (set):
381 a collection of validation keywords to consider to be
382 "weak". If there are two errors at the same level of the
383 instance and one is in the set of weak validation keywords,
384 the other error will take priority. By default, :kw:`anyOf`
385 and :kw:`oneOf` are considered weak keywords and will be
386 superseded by other same-level validation errors.
388 strong (set):
389 a collection of validation keywords to consider to be
390 "strong"
391 """
393 def relevance(error):
394 validator = error.validator
395 return (
396 -len(error.path),
397 validator not in weak,
398 validator in strong,
399 not error._matches_type(),
400 )
402 return relevance
405relevance = by_relevance()
406"""
407A key function (e.g. to use with `sorted`) which sorts errors by relevance.
409Example:
411.. code:: python
413 sorted(validator.iter_errors(12), key=jsonschema.exceptions.relevance)
414"""
417def best_match(errors, key=relevance):
418 """
419 Try to find an error that appears to be the best match among given errors.
421 In general, errors that are higher up in the instance (i.e. for which
422 `ValidationError.path` is shorter) are considered better matches,
423 since they indicate "more" is wrong with the instance.
425 If the resulting match is either :kw:`oneOf` or :kw:`anyOf`, the
426 *opposite* assumption is made -- i.e. the deepest error is picked,
427 since these keywords only need to match once, and any other errors
428 may not be relevant.
430 Arguments:
431 errors (collections.abc.Iterable):
433 the errors to select from. Do not provide a mixture of
434 errors from different validation attempts (i.e. from
435 different instances or schemas), since it won't produce
436 sensical output.
438 key (collections.abc.Callable):
440 the key to use when sorting errors. See `relevance` and
441 transitively `by_relevance` for more details (the default is
442 to sort with the defaults of that function). Changing the
443 default is only useful if you want to change the function
444 that rates errors but still want the error context descent
445 done by this function.
447 Returns:
448 the best matching error, or ``None`` if the iterable was empty
450 .. note::
452 This function is a heuristic. Its return value may change for a given
453 set of inputs from version to version if better heuristics are added.
454 """
455 errors = iter(errors)
456 best = next(errors, None)
457 if best is None:
458 return
459 best = max(itertools.chain([best], errors), key=key)
461 while best.context:
462 # Calculate the minimum via nsmallest, because we don't recurse if
463 # all nested errors have the same relevance (i.e. if min == max == all)
464 smallest = heapq.nsmallest(2, best.context, key=key)
465 if len(smallest) == 2 and key(smallest[0]) == key(smallest[1]):
466 return best
467 best = smallest[0]
468 return best