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