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