summaryrefslogtreecommitdiffstats
path: root/third_party/python/jsonschema/jsonschema/exceptions.py
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/python/jsonschema/jsonschema/exceptions.py')
-rw-r--r--third_party/python/jsonschema/jsonschema/exceptions.py396
1 files changed, 396 insertions, 0 deletions
diff --git a/third_party/python/jsonschema/jsonschema/exceptions.py b/third_party/python/jsonschema/jsonschema/exceptions.py
new file mode 100644
index 0000000000..87db3df3a6
--- /dev/null
+++ b/third_party/python/jsonschema/jsonschema/exceptions.py
@@ -0,0 +1,396 @@
+"""
+Validation errors, and some surrounding helpers.
+"""
+from __future__ import annotations
+
+from collections import defaultdict, deque
+from pprint import pformat
+from textwrap import dedent, indent
+import heapq
+import itertools
+
+import attr
+
+from jsonschema import _utils
+
+WEAK_MATCHES: frozenset[str] = frozenset(["anyOf", "oneOf"])
+STRONG_MATCHES: frozenset[str] = frozenset()
+
+_unset = _utils.Unset()
+
+
+class _Error(Exception):
+ def __init__(
+ self,
+ message,
+ validator=_unset,
+ path=(),
+ cause=None,
+ context=(),
+ validator_value=_unset,
+ instance=_unset,
+ schema=_unset,
+ schema_path=(),
+ parent=None,
+ type_checker=_unset,
+ ):
+ super(_Error, self).__init__(
+ message,
+ validator,
+ path,
+ cause,
+ context,
+ validator_value,
+ instance,
+ schema,
+ schema_path,
+ parent,
+ )
+ self.message = message
+ self.path = self.relative_path = deque(path)
+ self.schema_path = self.relative_schema_path = deque(schema_path)
+ self.context = list(context)
+ self.cause = self.__cause__ = cause
+ self.validator = validator
+ self.validator_value = validator_value
+ self.instance = instance
+ self.schema = schema
+ self.parent = parent
+ self._type_checker = type_checker
+
+ for error in context:
+ error.parent = self
+
+ def __repr__(self):
+ return f"<{self.__class__.__name__}: {self.message!r}>"
+
+ def __str__(self):
+ essential_for_verbose = (
+ self.validator, self.validator_value, self.instance, self.schema,
+ )
+ if any(m is _unset for m in essential_for_verbose):
+ return self.message
+
+ schema_path = _utils.format_as_index(
+ container=self._word_for_schema_in_error_message,
+ indices=list(self.relative_schema_path)[:-1],
+ )
+ instance_path = _utils.format_as_index(
+ container=self._word_for_instance_in_error_message,
+ indices=self.relative_path,
+ )
+ prefix = 16 * " "
+
+ return dedent(
+ f"""\
+ {self.message}
+
+ Failed validating {self.validator!r} in {schema_path}:
+ {indent(pformat(self.schema, width=72), prefix).lstrip()}
+
+ On {instance_path}:
+ {indent(pformat(self.instance, width=72), prefix).lstrip()}
+ """.rstrip(),
+ )
+
+ @classmethod
+ def create_from(cls, other):
+ return cls(**other._contents())
+
+ @property
+ def absolute_path(self):
+ parent = self.parent
+ if parent is None:
+ return self.relative_path
+
+ path = deque(self.relative_path)
+ path.extendleft(reversed(parent.absolute_path))
+ return path
+
+ @property
+ def absolute_schema_path(self):
+ parent = self.parent
+ if parent is None:
+ return self.relative_schema_path
+
+ path = deque(self.relative_schema_path)
+ path.extendleft(reversed(parent.absolute_schema_path))
+ return path
+
+ @property
+ def json_path(self):
+ path = "$"
+ for elem in self.absolute_path:
+ if isinstance(elem, int):
+ path += "[" + str(elem) + "]"
+ else:
+ path += "." + elem
+ return path
+
+ def _set(self, type_checker=None, **kwargs):
+ if type_checker is not None and self._type_checker is _unset:
+ self._type_checker = type_checker
+
+ for k, v in kwargs.items():
+ if getattr(self, k) is _unset:
+ setattr(self, k, v)
+
+ def _contents(self):
+ attrs = (
+ "message", "cause", "context", "validator", "validator_value",
+ "path", "schema_path", "instance", "schema", "parent",
+ )
+ return dict((attr, getattr(self, attr)) for attr in attrs)
+
+ def _matches_type(self):
+ try:
+ expected = self.schema["type"]
+ except (KeyError, TypeError):
+ return False
+
+ if isinstance(expected, str):
+ return self._type_checker.is_type(self.instance, expected)
+
+ return any(
+ self._type_checker.is_type(self.instance, expected_type)
+ for expected_type in expected
+ )
+
+
+class ValidationError(_Error):
+ """
+ An instance was invalid under a provided schema.
+ """
+
+ _word_for_schema_in_error_message = "schema"
+ _word_for_instance_in_error_message = "instance"
+
+
+class SchemaError(_Error):
+ """
+ A schema was invalid under its corresponding metaschema.
+ """
+
+ _word_for_schema_in_error_message = "metaschema"
+ _word_for_instance_in_error_message = "schema"
+
+
+@attr.s(hash=True)
+class RefResolutionError(Exception):
+ """
+ A ref could not be resolved.
+ """
+
+ _cause = attr.ib()
+
+ def __str__(self):
+ return str(self._cause)
+
+
+class UndefinedTypeCheck(Exception):
+ """
+ A type checker was asked to check a type it did not have registered.
+ """
+
+ def __init__(self, type):
+ self.type = type
+
+ def __str__(self):
+ return f"Type {self.type!r} is unknown to this type checker"
+
+
+class UnknownType(Exception):
+ """
+ A validator was asked to validate an instance against an unknown type.
+ """
+
+ def __init__(self, type, instance, schema):
+ self.type = type
+ self.instance = instance
+ self.schema = schema
+
+ def __str__(self):
+ prefix = 16 * " "
+
+ return dedent(
+ f"""\
+ Unknown type {self.type!r} for validator with schema:
+ {indent(pformat(self.schema, width=72), prefix).lstrip()}
+
+ While checking instance:
+ {indent(pformat(self.instance, width=72), prefix).lstrip()}
+ """.rstrip(),
+ )
+
+
+class FormatError(Exception):
+ """
+ Validating a format failed.
+ """
+
+ def __init__(self, message, cause=None):
+ super(FormatError, self).__init__(message, cause)
+ self.message = message
+ self.cause = self.__cause__ = cause
+
+ def __str__(self):
+ return self.message
+
+
+class ErrorTree:
+ """
+ ErrorTrees make it easier to check which validations failed.
+ """
+
+ _instance = _unset
+
+ def __init__(self, errors=()):
+ self.errors = {}
+ self._contents = defaultdict(self.__class__)
+
+ for error in errors:
+ container = self
+ for element in error.path:
+ container = container[element]
+ container.errors[error.validator] = error
+
+ container._instance = error.instance
+
+ def __contains__(self, index):
+ """
+ Check whether ``instance[index]`` has any errors.
+ """
+
+ return index in self._contents
+
+ def __getitem__(self, index):
+ """
+ Retrieve the child tree one level down at the given ``index``.
+
+ If the index is not in the instance that this tree corresponds
+ to and is not known by this tree, whatever error would be raised
+ by ``instance.__getitem__`` will be propagated (usually this is
+ some subclass of `LookupError`.
+ """
+
+ if self._instance is not _unset and index not in self:
+ self._instance[index]
+ return self._contents[index]
+
+ def __setitem__(self, index, value):
+ """
+ Add an error to the tree at the given ``index``.
+ """
+ self._contents[index] = value
+
+ def __iter__(self):
+ """
+ Iterate (non-recursively) over the indices in the instance with errors.
+ """
+
+ return iter(self._contents)
+
+ def __len__(self):
+ """
+ Return the `total_errors`.
+ """
+ return self.total_errors
+
+ def __repr__(self):
+ total = len(self)
+ errors = "error" if total == 1 else "errors"
+ return f"<{self.__class__.__name__} ({total} total {errors})>"
+
+ @property
+ def total_errors(self):
+ """
+ The total number of errors in the entire tree, including children.
+ """
+
+ child_errors = sum(len(tree) for _, tree in self._contents.items())
+ return len(self.errors) + child_errors
+
+
+def by_relevance(weak=WEAK_MATCHES, strong=STRONG_MATCHES):
+ """
+ Create a key function that can be used to sort errors by relevance.
+
+ Arguments:
+ weak (set):
+ a collection of validation keywords to consider to be
+ "weak". If there are two errors at the same level of the
+ instance and one is in the set of weak validation keywords,
+ the other error will take priority. By default, :kw:`anyOf`
+ and :kw:`oneOf` are considered weak keywords and will be
+ superseded by other same-level validation errors.
+
+ strong (set):
+ a collection of validation keywords to consider to be
+ "strong"
+ """
+ def relevance(error):
+ validator = error.validator
+ return (
+ -len(error.path),
+ validator not in weak,
+ validator in strong,
+ not error._matches_type(),
+ )
+ return relevance
+
+
+relevance = by_relevance()
+
+
+def best_match(errors, key=relevance):
+ """
+ Try to find an error that appears to be the best match among given errors.
+
+ In general, errors that are higher up in the instance (i.e. for which
+ `ValidationError.path` is shorter) are considered better matches,
+ since they indicate "more" is wrong with the instance.
+
+ If the resulting match is either :kw:`oneOf` or :kw:`anyOf`, the
+ *opposite* assumption is made -- i.e. the deepest error is picked,
+ since these keywords only need to match once, and any other errors
+ may not be relevant.
+
+ Arguments:
+ errors (collections.abc.Iterable):
+
+ the errors to select from. Do not provide a mixture of
+ errors from different validation attempts (i.e. from
+ different instances or schemas), since it won't produce
+ sensical output.
+
+ key (collections.abc.Callable):
+
+ the key to use when sorting errors. See `relevance` and
+ transitively `by_relevance` for more details (the default is
+ to sort with the defaults of that function). Changing the
+ default is only useful if you want to change the function
+ that rates errors but still want the error context descent
+ done by this function.
+
+ Returns:
+ the best matching error, or ``None`` if the iterable was empty
+
+ .. note::
+
+ This function is a heuristic. Its return value may change for a given
+ set of inputs from version to version if better heuristics are added.
+ """
+ errors = iter(errors)
+ best = next(errors, None)
+ if best is None:
+ return
+ best = max(itertools.chain([best], errors), key=key)
+
+ while best.context:
+ # Calculate the minimum via nsmallest, because we don't recurse if
+ # all nested errors have the same relevance (i.e. if min == max == all)
+ smallest = heapq.nsmallest(2, best.context, key=key)
+ if len(smallest) == 2 and key(smallest[0]) == key(smallest[1]):
+ return best
+ best = smallest[0]
+ return best