1"""
2Validation errors, and some surrounding helpers.
3"""
4from __future__ import annotations
5
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
12
13from attrs import define
14from referencing.exceptions import Unresolvable as _Unresolvable
15
16from jsonschema import _utils
17
18if TYPE_CHECKING:
19 from collections.abc import Iterable, Mapping, MutableMapping, Sequence
20
21 from jsonschema import _types
22
23WEAK_MATCHES: frozenset[str] = frozenset(["anyOf", "oneOf"])
24STRONG_MATCHES: frozenset[str] = frozenset()
25
26_unset = _utils.Unset()
27
28
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()
34
35
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}")
45
46
47class _Error(Exception):
48
49 _word_for_schema_in_error_message: ClassVar[str]
50 _word_for_instance_in_error_message: ClassVar[str]
51
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
89
90 for error in context:
91 error.parent = self
92
93 def __repr__(self) -> str:
94 return f"<{self.__class__.__name__}: {self.message!r}>"
95
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
102
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 * " "
112
113 return dedent(
114 f"""\
115 {self.message}
116
117 Failed validating {self.validator!r} in {schema_path}:
118 {_pretty(self.schema, prefix=prefix)}
119
120 On {instance_path}:
121 {_pretty(self.instance, prefix=prefix)}
122 """.rstrip(),
123 )
124
125 @classmethod
126 def create_from(cls, other: _Error):
127 return cls(**other._contents())
128
129 @property
130 def absolute_path(self) -> Sequence[str | int]:
131 parent = self.parent
132 if parent is None:
133 return self.relative_path
134
135 path = deque(self.relative_path)
136 path.extendleft(reversed(parent.absolute_path))
137 return path
138
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
144
145 path = deque(self.relative_schema_path)
146 path.extendleft(reversed(parent.absolute_schema_path))
147 return path
148
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
158
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
166
167 for k, v in kwargs.items():
168 if getattr(self, k) is _unset:
169 setattr(self, k, v)
170
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}
177
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
184
185 if isinstance(expected, str):
186 return self._type_checker.is_type(self.instance, expected)
187
188 return any(
189 self._type_checker.is_type(self.instance, expected_type)
190 for expected_type in expected
191 )
192
193
194class ValidationError(_Error):
195 """
196 An instance was invalid under a provided schema.
197 """
198
199 _word_for_schema_in_error_message = "schema"
200 _word_for_instance_in_error_message = "instance"
201
202
203class SchemaError(_Error):
204 """
205 A schema was invalid under its corresponding metaschema.
206 """
207
208 _word_for_schema_in_error_message = "metaschema"
209 _word_for_instance_in_error_message = "schema"
210
211
212@define(slots=False)
213class _RefResolutionError(Exception):
214 """
215 A ref could not be resolved.
216 """
217
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 )
223
224 _cause: Exception
225
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
230
231 def __str__(self) -> str:
232 return str(self._cause)
233
234
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)
238
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
245
246 def __getattr__(self, attr):
247 return getattr(self._wrapped, attr)
248
249 def __hash__(self):
250 return hash(self._wrapped)
251
252 def __repr__(self):
253 return f"<WrappedReferencingError {self._wrapped!r}>"
254
255 def __str__(self):
256 return f"{self._wrapped.__class__.__name__}: {self._wrapped}"
257
258
259class UndefinedTypeCheck(Exception):
260 """
261 A type checker was asked to check a type it did not have registered.
262 """
263
264 def __init__(self, type: str) -> None:
265 self.type = type
266
267 def __str__(self) -> str:
268 return f"Type {self.type!r} is unknown to this type checker"
269
270
271class UnknownType(Exception):
272 """
273 A validator was asked to validate an instance against an unknown type.
274 """
275
276 def __init__(self, type, instance, schema):
277 self.type = type
278 self.instance = instance
279 self.schema = schema
280
281 def __str__(self):
282 prefix = 16 * " "
283
284 return dedent(
285 f"""\
286 Unknown type {self.type!r} for validator with schema:
287 {_pretty(self.schema, prefix=prefix)}
288
289 While checking instance:
290 {_pretty(self.instance, prefix=prefix)}
291 """.rstrip(),
292 )
293
294
295class FormatError(Exception):
296 """
297 Validating a format failed.
298 """
299
300 def __init__(self, message, cause=None):
301 super().__init__(message, cause)
302 self.message = message
303 self.cause = self.__cause__ = cause
304
305 def __str__(self):
306 return self.message
307
308
309class ErrorTree:
310 """
311 ErrorTrees make it easier to check which validations failed.
312 """
313
314 _instance = _unset
315
316 def __init__(self, errors: Iterable[ValidationError] = ()):
317 self.errors: MutableMapping[str, ValidationError] = {}
318 self._contents: Mapping[str, ErrorTree] = defaultdict(self.__class__)
319
320 for error in errors:
321 container = self
322 for element in error.path:
323 container = container[element]
324 container.errors[error.validator] = error
325
326 container._instance = error.instance
327
328 def __contains__(self, index: str | int):
329 """
330 Check whether ``instance[index]`` has any errors.
331 """
332 return index in self._contents
333
334 def __getitem__(self, index):
335 """
336 Retrieve the child tree one level down at the given ``index``.
337
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]
346
347 def __setitem__(self, index: str | int, value: ErrorTree):
348 """
349 Add an error to the tree at the given ``index``.
350
351 .. deprecated:: v4.20.0
352
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]
363
364 def __iter__(self):
365 """
366 Iterate (non-recursively) over the indices in the instance with errors.
367 """
368 return iter(self._contents)
369
370 def __len__(self):
371 """
372 Return the `total_errors`.
373 """
374 return self.total_errors
375
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})>"
380
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
388
389
390def by_relevance(weak=WEAK_MATCHES, strong=STRONG_MATCHES):
391 """
392 Create a key function that can be used to sort errors by relevance.
393
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.
402
403 strong (set):
404 a collection of validation keywords to consider to be
405 "strong"
406
407 """
408
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
418
419 return relevance
420
421
422relevance = by_relevance()
423"""
424A key function (e.g. to use with `sorted`) which sorts errors by relevance.
425
426Example:
427
428.. code:: python
429
430 sorted(validator.iter_errors(12), key=jsonschema.exceptions.relevance)
431"""
432
433
434def best_match(errors, key=relevance):
435 """
436 Try to find an error that appears to be the best match among given errors.
437
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.
441
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.
446
447 Arguments:
448 errors (collections.abc.Iterable):
449
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.
454
455 key (collections.abc.Callable):
456
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.
463
464 Returns:
465 the best matching error, or ``None`` if the iterable was empty
466
467 .. note::
468
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.
471
472 """
473 best = max(errors, key=key, default=None)
474 if best is None:
475 return
476
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