Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/jsonschema/exceptions.py: 40%
193 statements
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-25 06:44 +0000
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-25 06:44 +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 ClassVar
10import heapq
11import itertools
12import warnings
14from attrs import define
15from referencing.exceptions import Unresolvable as _Unresolvable
17from jsonschema import _utils
19WEAK_MATCHES: frozenset[str] = frozenset(["anyOf", "oneOf"])
20STRONG_MATCHES: frozenset[str] = frozenset()
22_unset = _utils.Unset()
25def __getattr__(name):
26 if name == "RefResolutionError":
27 warnings.warn(
28 _RefResolutionError._DEPRECATION_MESSAGE,
29 DeprecationWarning,
30 stacklevel=2,
31 )
32 return _RefResolutionError
33 raise AttributeError(f"module {__name__} has no attribute {name}")
36class _Error(Exception):
38 _word_for_schema_in_error_message: ClassVar[str]
39 _word_for_instance_in_error_message: ClassVar[str]
41 def __init__(
42 self,
43 message: str,
44 validator=_unset,
45 path=(),
46 cause=None,
47 context=(),
48 validator_value=_unset,
49 instance=_unset,
50 schema=_unset,
51 schema_path=(),
52 parent=None,
53 type_checker=_unset,
54 ):
55 super().__init__(
56 message,
57 validator,
58 path,
59 cause,
60 context,
61 validator_value,
62 instance,
63 schema,
64 schema_path,
65 parent,
66 )
67 self.message = message
68 self.path = self.relative_path = deque(path)
69 self.schema_path = self.relative_schema_path = deque(schema_path)
70 self.context = list(context)
71 self.cause = self.__cause__ = cause
72 self.validator = validator
73 self.validator_value = validator_value
74 self.instance = instance
75 self.schema = schema
76 self.parent = parent
77 self._type_checker = type_checker
79 for error in context:
80 error.parent = self
82 def __repr__(self):
83 return f"<{self.__class__.__name__}: {self.message!r}>"
85 def __str__(self):
86 essential_for_verbose = (
87 self.validator, self.validator_value, self.instance, self.schema,
88 )
89 if any(m is _unset for m in essential_for_verbose):
90 return self.message
92 schema_path = _utils.format_as_index(
93 container=self._word_for_schema_in_error_message,
94 indices=list(self.relative_schema_path)[:-1],
95 )
96 instance_path = _utils.format_as_index(
97 container=self._word_for_instance_in_error_message,
98 indices=self.relative_path,
99 )
100 prefix = 16 * " "
102 return dedent(
103 f"""\
104 {self.message}
106 Failed validating {self.validator!r} in {schema_path}:
107 {indent(pformat(self.schema, width=72), prefix).lstrip()}
109 On {instance_path}:
110 {indent(pformat(self.instance, width=72), prefix).lstrip()}
111 """.rstrip(),
112 )
114 @classmethod
115 def create_from(cls, other):
116 return cls(**other._contents())
118 @property
119 def absolute_path(self):
120 parent = self.parent
121 if parent is None:
122 return self.relative_path
124 path = deque(self.relative_path)
125 path.extendleft(reversed(parent.absolute_path))
126 return path
128 @property
129 def absolute_schema_path(self):
130 parent = self.parent
131 if parent is None:
132 return self.relative_schema_path
134 path = deque(self.relative_schema_path)
135 path.extendleft(reversed(parent.absolute_schema_path))
136 return path
138 @property
139 def json_path(self):
140 path = "$"
141 for elem in self.absolute_path:
142 if isinstance(elem, int):
143 path += "[" + str(elem) + "]"
144 else:
145 path += "." + elem
146 return path
148 def _set(self, type_checker=None, **kwargs):
149 if type_checker is not None and self._type_checker is _unset:
150 self._type_checker = type_checker
152 for k, v in kwargs.items():
153 if getattr(self, k) is _unset:
154 setattr(self, k, v)
156 def _contents(self):
157 attrs = (
158 "message", "cause", "context", "validator", "validator_value",
159 "path", "schema_path", "instance", "schema", "parent",
160 )
161 return dict((attr, getattr(self, attr)) for attr in attrs)
163 def _matches_type(self):
164 try:
165 expected = self.schema["type"]
166 except (KeyError, TypeError):
167 return False
169 if isinstance(expected, str):
170 return self._type_checker.is_type(self.instance, expected)
172 return any(
173 self._type_checker.is_type(self.instance, expected_type)
174 for expected_type in expected
175 )
178class ValidationError(_Error):
179 """
180 An instance was invalid under a provided schema.
181 """
183 _word_for_schema_in_error_message = "schema"
184 _word_for_instance_in_error_message = "instance"
187class SchemaError(_Error):
188 """
189 A schema was invalid under its corresponding metaschema.
190 """
192 _word_for_schema_in_error_message = "metaschema"
193 _word_for_instance_in_error_message = "schema"
196@define(slots=False)
197class _RefResolutionError(Exception):
198 """
199 A ref could not be resolved.
200 """
202 _DEPRECATION_MESSAGE = (
203 "jsonschema.exceptions.RefResolutionError is deprecated as of version "
204 "4.18.0. If you wish to catch potential reference resolution errors, "
205 "directly catch referencing.exceptions.Unresolvable."
206 )
208 _cause: Exception
210 def __eq__(self, other):
211 if self.__class__ is not other.__class__:
212 return NotImplemented
213 return self._cause == other._cause
215 def __str__(self):
216 return str(self._cause)
219class _WrappedReferencingError(_RefResolutionError, _Unresolvable):
220 def __init__(self, cause: _Unresolvable):
221 object.__setattr__(self, "_wrapped", cause)
223 def __eq__(self, other):
224 if other.__class__ is self.__class__:
225 return self._wrapped == other._wrapped
226 elif other.__class__ is self._wrapped.__class__:
227 return self._wrapped == other
228 return NotImplemented
230 def __getattr__(self, attr):
231 return getattr(self._wrapped, attr)
233 def __hash__(self):
234 return hash(self._wrapped)
236 def __repr__(self):
237 return f"<WrappedReferencingError {self._wrapped!r}>"
239 def __str__(self):
240 return f"{self._wrapped.__class__.__name__}: {self._wrapped}"
243class UndefinedTypeCheck(Exception):
244 """
245 A type checker was asked to check a type it did not have registered.
246 """
248 def __init__(self, type):
249 self.type = type
251 def __str__(self):
252 return f"Type {self.type!r} is unknown to this type checker"
255class UnknownType(Exception):
256 """
257 A validator was asked to validate an instance against an unknown type.
258 """
260 def __init__(self, type, instance, schema):
261 self.type = type
262 self.instance = instance
263 self.schema = schema
265 def __str__(self):
266 prefix = 16 * " "
268 return dedent(
269 f"""\
270 Unknown type {self.type!r} for validator with schema:
271 {indent(pformat(self.schema, width=72), prefix).lstrip()}
273 While checking instance:
274 {indent(pformat(self.instance, width=72), prefix).lstrip()}
275 """.rstrip(),
276 )
279class FormatError(Exception):
280 """
281 Validating a format failed.
282 """
284 def __init__(self, message, cause=None):
285 super().__init__(message, cause)
286 self.message = message
287 self.cause = self.__cause__ = cause
289 def __str__(self):
290 return self.message
293class ErrorTree:
294 """
295 ErrorTrees make it easier to check which validations failed.
296 """
298 _instance = _unset
300 def __init__(self, errors=()):
301 self.errors = {}
302 self._contents = defaultdict(self.__class__)
304 for error in errors:
305 container = self
306 for element in error.path:
307 container = container[element]
308 container.errors[error.validator] = error
310 container._instance = error.instance
312 def __contains__(self, index):
313 """
314 Check whether ``instance[index]`` has any errors.
315 """
316 return index in self._contents
318 def __getitem__(self, index):
319 """
320 Retrieve the child tree one level down at the given ``index``.
322 If the index is not in the instance that this tree corresponds
323 to and is not known by this tree, whatever error would be raised
324 by ``instance.__getitem__`` will be propagated (usually this is
325 some subclass of `LookupError`.
326 """
327 if self._instance is not _unset and index not in self:
328 self._instance[index]
329 return self._contents[index]
331 def __setitem__(self, index, value):
332 """
333 Add an error to the tree at the given ``index``.
334 """
335 self._contents[index] = value
337 def __iter__(self):
338 """
339 Iterate (non-recursively) over the indices in the instance with errors.
340 """
341 return iter(self._contents)
343 def __len__(self):
344 """
345 Return the `total_errors`.
346 """
347 return self.total_errors
349 def __repr__(self):
350 total = len(self)
351 errors = "error" if total == 1 else "errors"
352 return f"<{self.__class__.__name__} ({total} total {errors})>"
354 @property
355 def total_errors(self):
356 """
357 The total number of errors in the entire tree, including children.
358 """
359 child_errors = sum(len(tree) for _, tree in self._contents.items())
360 return len(self.errors) + child_errors
363def by_relevance(weak=WEAK_MATCHES, strong=STRONG_MATCHES):
364 """
365 Create a key function that can be used to sort errors by relevance.
367 Arguments:
368 weak (set):
369 a collection of validation keywords to consider to be
370 "weak". If there are two errors at the same level of the
371 instance and one is in the set of weak validation keywords,
372 the other error will take priority. By default, :kw:`anyOf`
373 and :kw:`oneOf` are considered weak keywords and will be
374 superseded by other same-level validation errors.
376 strong (set):
377 a collection of validation keywords to consider to be
378 "strong"
379 """
381 def relevance(error):
382 validator = error.validator
383 return (
384 -len(error.path),
385 validator not in weak,
386 validator in strong,
387 not error._matches_type(),
388 )
390 return relevance
393relevance = by_relevance()
394"""
395A key function (e.g. to use with `sorted`) which sorts errors by relevance.
397Example:
399.. code:: python
401 sorted(validator.iter_errors(12), key=jsonschema.exceptions.relevance)
402"""
405def best_match(errors, key=relevance):
406 """
407 Try to find an error that appears to be the best match among given errors.
409 In general, errors that are higher up in the instance (i.e. for which
410 `ValidationError.path` is shorter) are considered better matches,
411 since they indicate "more" is wrong with the instance.
413 If the resulting match is either :kw:`oneOf` or :kw:`anyOf`, the
414 *opposite* assumption is made -- i.e. the deepest error is picked,
415 since these keywords only need to match once, and any other errors
416 may not be relevant.
418 Arguments:
419 errors (collections.abc.Iterable):
421 the errors to select from. Do not provide a mixture of
422 errors from different validation attempts (i.e. from
423 different instances or schemas), since it won't produce
424 sensical output.
426 key (collections.abc.Callable):
428 the key to use when sorting errors. See `relevance` and
429 transitively `by_relevance` for more details (the default is
430 to sort with the defaults of that function). Changing the
431 default is only useful if you want to change the function
432 that rates errors but still want the error context descent
433 done by this function.
435 Returns:
436 the best matching error, or ``None`` if the iterable was empty
438 .. note::
440 This function is a heuristic. Its return value may change for a given
441 set of inputs from version to version if better heuristics are added.
442 """
443 errors = iter(errors)
444 best = next(errors, None)
445 if best is None:
446 return
447 best = max(itertools.chain([best], errors), key=key)
449 while best.context:
450 # Calculate the minimum via nsmallest, because we don't recurse if
451 # all nested errors have the same relevance (i.e. if min == max == all)
452 smallest = heapq.nsmallest(2, best.context, key=key)
453 if len(smallest) == 2 and key(smallest[0]) == key(smallest[1]):
454 return best
455 best = smallest[0]
456 return best