Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/jsonschema/exceptions.py: 38%
161 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-01 06:54 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-01 06:54 +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
9import heapq
10import itertools
12import attr
14from jsonschema import _utils
16WEAK_MATCHES: frozenset[str] = frozenset(["anyOf", "oneOf"])
17STRONG_MATCHES: frozenset[str] = frozenset()
19_unset = _utils.Unset()
22class _Error(Exception):
23 def __init__(
24 self,
25 message,
26 validator=_unset,
27 path=(),
28 cause=None,
29 context=(),
30 validator_value=_unset,
31 instance=_unset,
32 schema=_unset,
33 schema_path=(),
34 parent=None,
35 type_checker=_unset,
36 ):
37 super(_Error, self).__init__(
38 message,
39 validator,
40 path,
41 cause,
42 context,
43 validator_value,
44 instance,
45 schema,
46 schema_path,
47 parent,
48 )
49 self.message = message
50 self.path = self.relative_path = deque(path)
51 self.schema_path = self.relative_schema_path = deque(schema_path)
52 self.context = list(context)
53 self.cause = self.__cause__ = cause
54 self.validator = validator
55 self.validator_value = validator_value
56 self.instance = instance
57 self.schema = schema
58 self.parent = parent
59 self._type_checker = type_checker
61 for error in context:
62 error.parent = self
64 def __repr__(self):
65 return f"<{self.__class__.__name__}: {self.message!r}>"
67 def __str__(self):
68 essential_for_verbose = (
69 self.validator, self.validator_value, self.instance, self.schema,
70 )
71 if any(m is _unset for m in essential_for_verbose):
72 return self.message
74 schema_path = _utils.format_as_index(
75 container=self._word_for_schema_in_error_message,
76 indices=list(self.relative_schema_path)[:-1],
77 )
78 instance_path = _utils.format_as_index(
79 container=self._word_for_instance_in_error_message,
80 indices=self.relative_path,
81 )
82 prefix = 16 * " "
84 return dedent(
85 f"""\
86 {self.message}
88 Failed validating {self.validator!r} in {schema_path}:
89 {indent(pformat(self.schema, width=72), prefix).lstrip()}
91 On {instance_path}:
92 {indent(pformat(self.instance, width=72), prefix).lstrip()}
93 """.rstrip(),
94 )
96 @classmethod
97 def create_from(cls, other):
98 return cls(**other._contents())
100 @property
101 def absolute_path(self):
102 parent = self.parent
103 if parent is None:
104 return self.relative_path
106 path = deque(self.relative_path)
107 path.extendleft(reversed(parent.absolute_path))
108 return path
110 @property
111 def absolute_schema_path(self):
112 parent = self.parent
113 if parent is None:
114 return self.relative_schema_path
116 path = deque(self.relative_schema_path)
117 path.extendleft(reversed(parent.absolute_schema_path))
118 return path
120 @property
121 def json_path(self):
122 path = "$"
123 for elem in self.absolute_path:
124 if isinstance(elem, int):
125 path += "[" + str(elem) + "]"
126 else:
127 path += "." + elem
128 return path
130 def _set(self, type_checker=None, **kwargs):
131 if type_checker is not None and self._type_checker is _unset:
132 self._type_checker = type_checker
134 for k, v in kwargs.items():
135 if getattr(self, k) is _unset:
136 setattr(self, k, v)
138 def _contents(self):
139 attrs = (
140 "message", "cause", "context", "validator", "validator_value",
141 "path", "schema_path", "instance", "schema", "parent",
142 )
143 return dict((attr, getattr(self, attr)) for attr in attrs)
145 def _matches_type(self):
146 try:
147 expected = self.schema["type"]
148 except (KeyError, TypeError):
149 return False
151 if isinstance(expected, str):
152 return self._type_checker.is_type(self.instance, expected)
154 return any(
155 self._type_checker.is_type(self.instance, expected_type)
156 for expected_type in expected
157 )
160class ValidationError(_Error):
161 """
162 An instance was invalid under a provided schema.
163 """
165 _word_for_schema_in_error_message = "schema"
166 _word_for_instance_in_error_message = "instance"
169class SchemaError(_Error):
170 """
171 A schema was invalid under its corresponding metaschema.
172 """
174 _word_for_schema_in_error_message = "metaschema"
175 _word_for_instance_in_error_message = "schema"
178@attr.s(hash=True)
179class RefResolutionError(Exception):
180 """
181 A ref could not be resolved.
182 """
184 _cause = attr.ib()
186 def __str__(self):
187 return str(self._cause)
190class UndefinedTypeCheck(Exception):
191 """
192 A type checker was asked to check a type it did not have registered.
193 """
195 def __init__(self, type):
196 self.type = type
198 def __str__(self):
199 return f"Type {self.type!r} is unknown to this type checker"
202class UnknownType(Exception):
203 """
204 A validator was asked to validate an instance against an unknown type.
205 """
207 def __init__(self, type, instance, schema):
208 self.type = type
209 self.instance = instance
210 self.schema = schema
212 def __str__(self):
213 prefix = 16 * " "
215 return dedent(
216 f"""\
217 Unknown type {self.type!r} for validator with schema:
218 {indent(pformat(self.schema, width=72), prefix).lstrip()}
220 While checking instance:
221 {indent(pformat(self.instance, width=72), prefix).lstrip()}
222 """.rstrip(),
223 )
226class FormatError(Exception):
227 """
228 Validating a format failed.
229 """
231 def __init__(self, message, cause=None):
232 super(FormatError, self).__init__(message, cause)
233 self.message = message
234 self.cause = self.__cause__ = cause
236 def __str__(self):
237 return self.message
240class ErrorTree:
241 """
242 ErrorTrees make it easier to check which validations failed.
243 """
245 _instance = _unset
247 def __init__(self, errors=()):
248 self.errors = {}
249 self._contents = defaultdict(self.__class__)
251 for error in errors:
252 container = self
253 for element in error.path:
254 container = container[element]
255 container.errors[error.validator] = error
257 container._instance = error.instance
259 def __contains__(self, index):
260 """
261 Check whether ``instance[index]`` has any errors.
262 """
264 return index in self._contents
266 def __getitem__(self, index):
267 """
268 Retrieve the child tree one level down at the given ``index``.
270 If the index is not in the instance that this tree corresponds
271 to and is not known by this tree, whatever error would be raised
272 by ``instance.__getitem__`` will be propagated (usually this is
273 some subclass of `LookupError`.
274 """
276 if self._instance is not _unset and index not in self:
277 self._instance[index]
278 return self._contents[index]
280 def __setitem__(self, index, value):
281 """
282 Add an error to the tree at the given ``index``.
283 """
284 self._contents[index] = value
286 def __iter__(self):
287 """
288 Iterate (non-recursively) over the indices in the instance with errors.
289 """
291 return iter(self._contents)
293 def __len__(self):
294 """
295 Return the `total_errors`.
296 """
297 return self.total_errors
299 def __repr__(self):
300 total = len(self)
301 errors = "error" if total == 1 else "errors"
302 return f"<{self.__class__.__name__} ({total} total {errors})>"
304 @property
305 def total_errors(self):
306 """
307 The total number of errors in the entire tree, including children.
308 """
310 child_errors = sum(len(tree) for _, tree in self._contents.items())
311 return len(self.errors) + child_errors
314def by_relevance(weak=WEAK_MATCHES, strong=STRONG_MATCHES):
315 """
316 Create a key function that can be used to sort errors by relevance.
318 Arguments:
319 weak (set):
320 a collection of validation keywords to consider to be
321 "weak". If there are two errors at the same level of the
322 instance and one is in the set of weak validation keywords,
323 the other error will take priority. By default, :kw:`anyOf`
324 and :kw:`oneOf` are considered weak keywords and will be
325 superseded by other same-level validation errors.
327 strong (set):
328 a collection of validation keywords to consider to be
329 "strong"
330 """
331 def relevance(error):
332 validator = error.validator
333 return (
334 -len(error.path),
335 validator not in weak,
336 validator in strong,
337 not error._matches_type(),
338 )
339 return relevance
342relevance = by_relevance()
345def best_match(errors, key=relevance):
346 """
347 Try to find an error that appears to be the best match among given errors.
349 In general, errors that are higher up in the instance (i.e. for which
350 `ValidationError.path` is shorter) are considered better matches,
351 since they indicate "more" is wrong with the instance.
353 If the resulting match is either :kw:`oneOf` or :kw:`anyOf`, the
354 *opposite* assumption is made -- i.e. the deepest error is picked,
355 since these keywords only need to match once, and any other errors
356 may not be relevant.
358 Arguments:
359 errors (collections.abc.Iterable):
361 the errors to select from. Do not provide a mixture of
362 errors from different validation attempts (i.e. from
363 different instances or schemas), since it won't produce
364 sensical output.
366 key (collections.abc.Callable):
368 the key to use when sorting errors. See `relevance` and
369 transitively `by_relevance` for more details (the default is
370 to sort with the defaults of that function). Changing the
371 default is only useful if you want to change the function
372 that rates errors but still want the error context descent
373 done by this function.
375 Returns:
376 the best matching error, or ``None`` if the iterable was empty
378 .. note::
380 This function is a heuristic. Its return value may change for a given
381 set of inputs from version to version if better heuristics are added.
382 """
383 errors = iter(errors)
384 best = next(errors, None)
385 if best is None:
386 return
387 best = max(itertools.chain([best], errors), key=key)
389 while best.context:
390 # Calculate the minimum via nsmallest, because we don't recurse if
391 # all nested errors have the same relevance (i.e. if min == max == all)
392 smallest = heapq.nsmallest(2, best.context, key=key)
393 if len(smallest) == 2 and key(smallest[0]) == key(smallest[1]):
394 return best
395 best = smallest[0]
396 return best