exceptions.py 15 KB

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