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