errors.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  1. # -*-: coding utf-8 -*-
  2. """ This module contains the error-related constants and classes. """
  3. from __future__ import absolute_import
  4. from collections import defaultdict, namedtuple, MutableMapping
  5. from copy import copy, deepcopy
  6. from functools import wraps
  7. from pprint import pformat
  8. from cerberus.platform import PYTHON_VERSION
  9. from cerberus.utils import compare_paths_lt, quote_string
  10. ErrorDefinition = namedtuple('ErrorDefinition', 'code, rule')
  11. """
  12. This class is used to define possible errors. Each distinguishable error is
  13. defined by a *unique* error ``code`` as integer and the ``rule`` that can
  14. cause it as string.
  15. The instances' names do not contain a common prefix as they are supposed to be
  16. referenced within the module namespace, e.g. ``errors.CUSTOM``.
  17. """
  18. # custom
  19. CUSTOM = ErrorDefinition(0x00, None)
  20. # existence
  21. DOCUMENT_MISSING = ErrorDefinition(0x01, None) # issues/141
  22. DOCUMENT_MISSING = "document is missing"
  23. REQUIRED_FIELD = ErrorDefinition(0x02, 'required')
  24. UNKNOWN_FIELD = ErrorDefinition(0x03, None)
  25. DEPENDENCIES_FIELD = ErrorDefinition(0x04, 'dependencies')
  26. DEPENDENCIES_FIELD_VALUE = ErrorDefinition(0x05, 'dependencies')
  27. EXCLUDES_FIELD = ErrorDefinition(0x06, 'excludes')
  28. # shape
  29. DOCUMENT_FORMAT = ErrorDefinition(0x21, None) # issues/141
  30. DOCUMENT_FORMAT = "'{0}' is not a document, must be a dict"
  31. EMPTY_NOT_ALLOWED = ErrorDefinition(0x22, 'empty')
  32. NOT_NULLABLE = ErrorDefinition(0x23, 'nullable')
  33. BAD_TYPE = ErrorDefinition(0x24, 'type')
  34. BAD_TYPE_FOR_SCHEMA = ErrorDefinition(0x25, 'schema')
  35. ITEMS_LENGTH = ErrorDefinition(0x26, 'items')
  36. MIN_LENGTH = ErrorDefinition(0x27, 'minlength')
  37. MAX_LENGTH = ErrorDefinition(0x28, 'maxlength')
  38. # color
  39. REGEX_MISMATCH = ErrorDefinition(0x41, 'regex')
  40. MIN_VALUE = ErrorDefinition(0x42, 'min')
  41. MAX_VALUE = ErrorDefinition(0x43, 'max')
  42. UNALLOWED_VALUE = ErrorDefinition(0x44, 'allowed')
  43. UNALLOWED_VALUES = ErrorDefinition(0x45, 'allowed')
  44. FORBIDDEN_VALUE = ErrorDefinition(0x46, 'forbidden')
  45. FORBIDDEN_VALUES = ErrorDefinition(0x47, 'forbidden')
  46. # other
  47. NORMALIZATION = ErrorDefinition(0x60, None)
  48. COERCION_FAILED = ErrorDefinition(0x61, 'coerce')
  49. RENAMING_FAILED = ErrorDefinition(0x62, 'rename_handler')
  50. READONLY_FIELD = ErrorDefinition(0x63, 'readonly')
  51. SETTING_DEFAULT_FAILED = ErrorDefinition(0x64, 'default_setter')
  52. # groups
  53. ERROR_GROUP = ErrorDefinition(0x80, None)
  54. MAPPING_SCHEMA = ErrorDefinition(0x81, 'schema')
  55. SEQUENCE_SCHEMA = ErrorDefinition(0x82, 'schema')
  56. KEYSCHEMA = ErrorDefinition(0x83, 'keyschema')
  57. VALUESCHEMA = ErrorDefinition(0x84, 'valueschema')
  58. BAD_ITEMS = ErrorDefinition(0x8f, 'items')
  59. LOGICAL = ErrorDefinition(0x90, None)
  60. NONEOF = ErrorDefinition(0x91, 'noneof')
  61. ONEOF = ErrorDefinition(0x92, 'oneof')
  62. ANYOF = ErrorDefinition(0x93, 'anyof')
  63. ALLOF = ErrorDefinition(0x94, 'allof')
  64. """ SchemaError messages """
  65. SCHEMA_ERROR_DEFINITION_TYPE = \
  66. "schema definition for field '{0}' must be a dict"
  67. SCHEMA_ERROR_MISSING = "validation schema missing"
  68. """ Error representations """
  69. class ValidationError(object):
  70. """ A simple class to store and query basic error information. """
  71. def __init__(self, document_path, schema_path, code, rule, constraint,
  72. value, info):
  73. self.document_path = document_path
  74. """ The path to the field within the document that caused the error.
  75. Type: :class:`tuple` """
  76. self.schema_path = schema_path
  77. """ The path to the rule within the schema that caused the error.
  78. Type: :class:`tuple` """
  79. self.code = code
  80. """ The error's identifier code. Type: :class:`int` """
  81. self.rule = rule
  82. """ The rule that failed. Type: `string` """
  83. self.constraint = constraint
  84. """ The constraint that failed. """
  85. self.value = value
  86. """ The value that failed. """
  87. self.info = info
  88. """ May hold additional information about the error.
  89. Type: :class:`tuple` """
  90. def __eq__(self, other):
  91. """ Assumes the errors relate to the same document and schema. """
  92. return hash(self) == hash(other)
  93. def __hash__(self):
  94. """ Expects that all other properties are transitively determined. """
  95. return hash(self.document_path) ^ hash(self.schema_path) \
  96. ^ hash(self.code)
  97. def __lt__(self, other):
  98. if self.document_path != other.document_path:
  99. return compare_paths_lt(self.document_path, other.document_path)
  100. else:
  101. return compare_paths_lt(self.schema_path, other.schema_path)
  102. def __repr__(self):
  103. return "{class_name} @ {memptr} ( " \
  104. "document_path={document_path}," \
  105. "schema_path={schema_path}," \
  106. "code={code}," \
  107. "constraint={constraint}," \
  108. "value={value}," \
  109. "info={info} )"\
  110. .format(class_name=self.__class__.__name__, memptr=hex(id(self)), # noqa: E501
  111. document_path=self.document_path,
  112. schema_path=self.schema_path,
  113. code=hex(self.code),
  114. constraint=quote_string(self.constraint),
  115. value=quote_string(self.value),
  116. info=self.info)
  117. @property
  118. def child_errors(self):
  119. """
  120. A list that contains the individual errors of a bulk validation error.
  121. """
  122. return self.info[0] if self.is_group_error else None
  123. @property
  124. def definitions_errors(self):
  125. """ Dictionary with errors of an *of-rule mapped to the index of the
  126. definition it occurred in. Returns :obj:`None` if not applicable.
  127. """
  128. if not self.is_logic_error:
  129. return None
  130. result = defaultdict(list)
  131. for error in self.child_errors:
  132. i = error.schema_path[len(self.schema_path)]
  133. result[i].append(error)
  134. return result
  135. @property
  136. def field(self):
  137. """ Field of the contextual mapping, possibly :obj:`None`. """
  138. if self.document_path:
  139. return self.document_path[-1]
  140. else:
  141. return None
  142. @property
  143. def is_group_error(self):
  144. """ ``True`` for errors of bulk validations. """
  145. return bool(self.code & ERROR_GROUP.code)
  146. @property
  147. def is_logic_error(self):
  148. """ ``True`` for validation errors against different schemas with
  149. *of-rules. """
  150. return bool(self.code & LOGICAL.code - ERROR_GROUP.code)
  151. @property
  152. def is_normalization_error(self):
  153. """ ``True`` for normalization errors. """
  154. return bool(self.code & NORMALIZATION.code)
  155. class ErrorList(list):
  156. """ A list for :class:`~cerberus.errors.ValidationError` instances that
  157. can be queried with the ``in`` keyword for a particular
  158. :class:`~cerberus.errors.ErrorDefinition`. """
  159. def __contains__(self, error_definition):
  160. for code in (x.code for x in self):
  161. if code == error_definition.code:
  162. return True
  163. return False
  164. class ErrorTreeNode(MutableMapping):
  165. __slots__ = ('descendants', 'errors', 'parent_node', 'path', 'tree_root')
  166. def __init__(self, path, parent_node):
  167. self.parent_node = parent_node
  168. self.tree_root = self.parent_node.tree_root
  169. self.path = path[:self.parent_node.depth + 1]
  170. self.errors = ErrorList()
  171. self.descendants = {}
  172. def __add__(self, error):
  173. self.add(error)
  174. return self
  175. def __contains__(self, item):
  176. if isinstance(item, ErrorDefinition):
  177. return item in self.errors
  178. else:
  179. return item in self.descendants
  180. def __delitem__(self, key):
  181. del self.descendants[key]
  182. def __iter__(self):
  183. return iter(self.errors)
  184. def __getitem__(self, item):
  185. if isinstance(item, ErrorDefinition):
  186. for error in self.errors:
  187. if item.code == error.code:
  188. return error
  189. else:
  190. return self.descendants.get(item)
  191. def __len__(self):
  192. return len(self.errors)
  193. def __repr__(self):
  194. return self.__str__()
  195. def __setitem__(self, key, value):
  196. self.descendants[key] = value
  197. def __str__(self):
  198. return str(self.errors) + ',' + str(self.descendants)
  199. @property
  200. def depth(self):
  201. return len(self.path)
  202. @property
  203. def tree_type(self):
  204. return self.tree_root.tree_type
  205. def add(self, error):
  206. error_path = self._path_of_(error)
  207. key = error_path[self.depth]
  208. if key not in self.descendants:
  209. self[key] = ErrorTreeNode(error_path, self)
  210. if len(error_path) == self.depth + 1:
  211. self[key].errors.append(error)
  212. self[key].errors.sort()
  213. if error.is_group_error:
  214. for child_error in error.child_errors:
  215. self.tree_root += child_error
  216. else:
  217. self[key] += error
  218. def _path_of_(self, error):
  219. return getattr(error, self.tree_type + '_path')
  220. class ErrorTree(ErrorTreeNode):
  221. """ Base class for :class:`~cerberus.errors.DocumentErrorTree` and
  222. :class:`~cerberus.errors.SchemaErrorTree`. """
  223. def __init__(self, errors=[]):
  224. self.parent_node = None
  225. self.tree_root = self
  226. self.path = ()
  227. self.errors = ErrorList()
  228. self.descendants = {}
  229. for error in errors:
  230. self += error
  231. def add(self, error):
  232. """ Add an error to the tree.
  233. :param error: :class:`~cerberus.errors.ValidationError`
  234. """
  235. if not self._path_of_(error):
  236. self.errors.append(error)
  237. self.errors.sort()
  238. else:
  239. super(ErrorTree, self).add(error)
  240. def fetch_errors_from(self, path):
  241. """ Returns all errors for a particular path.
  242. :param path: :class:`tuple` of :term:`hashable` s.
  243. :rtype: :class:`~cerberus.errors.ErrorList`
  244. """
  245. node = self.fetch_node_from(path)
  246. if node is not None:
  247. return node.errors
  248. else:
  249. return ErrorList()
  250. def fetch_node_from(self, path):
  251. """ Returns a node for a path.
  252. :param path: Tuple of :term:`hashable` s.
  253. :rtype: :class:`~cerberus.errors.ErrorTreeNode` or :obj:`None`
  254. """
  255. context = self
  256. for key in path:
  257. context = context[key]
  258. if context is None:
  259. break
  260. return context
  261. class DocumentErrorTree(ErrorTree):
  262. """ Implements a dict-like class to query errors by indexes following the
  263. structure of a validated document. """
  264. tree_type = 'document'
  265. class SchemaErrorTree(ErrorTree):
  266. """ Implements a dict-like class to query errors by indexes following the
  267. structure of the used schema. """
  268. tree_type = 'schema'
  269. class BaseErrorHandler(object):
  270. """ Base class for all error handlers.
  271. Subclasses are identified as error-handlers with an instance-test. """
  272. def __init__(self, *args, **kwargs):
  273. """ Optionally initialize a new instance. """
  274. pass
  275. def __call__(self, errors):
  276. """ Returns errors in a handler-specific format.
  277. :param errors: An object containing the errors.
  278. :type errors: :term:`iterable` of
  279. :class:`~cerberus.errors.ValidationError` instances or a
  280. :class:`~cerberus.Validator` instance
  281. """
  282. raise NotImplementedError
  283. def __iter__(self):
  284. """ Be a superhero and implement an iterator over errors. """
  285. raise NotImplementedError
  286. def add(self, error):
  287. """ Add an error to the errors' container object of a handler.
  288. :param error: The error to add.
  289. :type error: :class:`~cerberus.errors.ValidationError`
  290. """
  291. raise NotImplementedError
  292. def emit(self, error):
  293. """ Optionally emits an error in the handler's format to a stream.
  294. Or light a LED, or even shut down a power plant.
  295. :param error: The error to emit.
  296. :type error: :class:`~cerberus.errors.ValidationError`
  297. """
  298. pass
  299. def end(self, validator):
  300. """ Gets called when a validation ends.
  301. :param validator: The calling validator.
  302. :type validator: :class:`~cerberus.Validator` """
  303. pass
  304. def extend(self, errors):
  305. """ Adds all errors to the handler's container object.
  306. :param errors: The errors to add.
  307. :type errors: :term:`iterable` of
  308. :class:`~cerberus.errors.ValidationError` instances
  309. """
  310. for error in errors:
  311. self.add(error)
  312. def start(self, validator):
  313. """ Gets called when a validation starts.
  314. :param validator: The calling validator.
  315. :type validator: :class:`~cerberus.Validator`
  316. """
  317. pass
  318. class ToyErrorHandler(BaseErrorHandler):
  319. def __call__(self, *args, **kwargs):
  320. raise RuntimeError('This is not supposed to happen.')
  321. def clear(self):
  322. pass
  323. def encode_unicode(f):
  324. """Cerberus error messages expect regular binary strings.
  325. If unicode is used in a ValidationError message can't be printed.
  326. This decorator ensures that if legacy Python is used unicode
  327. strings are encoded before passing to a function.
  328. """
  329. @wraps(f)
  330. def wrapped(obj, error):
  331. def _encode(value):
  332. """Helper encoding unicode strings into binary utf-8"""
  333. if isinstance(value, unicode): # noqa: F821
  334. return value.encode('utf-8')
  335. return value
  336. error = copy(error)
  337. error.document_path = _encode(error.document_path)
  338. error.schema_path = _encode(error.schema_path)
  339. error.constraint = _encode(error.constraint)
  340. error.value = _encode(error.value)
  341. error.info = _encode(error.info)
  342. return f(obj, error)
  343. return wrapped if PYTHON_VERSION < 3 else f
  344. class BasicErrorHandler(BaseErrorHandler):
  345. """ Models cerberus' legacy. Returns a :class:`dict`. When mangled
  346. through :class:`str` a pretty-formatted representation of that
  347. tree is returned.
  348. """
  349. messages = {0x00: "{0}",
  350. 0x01: "document is missing",
  351. 0x02: "required field",
  352. 0x03: "unknown field",
  353. 0x04: "field '{0}' is required",
  354. 0x05: "depends on these values: {constraint}",
  355. 0x06: "{0} must not be present with '{field}'",
  356. 0x21: "'{0}' is not a document, must be a dict",
  357. 0x22: "empty values not allowed",
  358. 0x23: "null value not allowed",
  359. 0x24: "must be of {constraint} type",
  360. 0x25: "must be of dict type",
  361. 0x26: "length of list should be {constraint}, it is {0}",
  362. 0x27: "min length is {constraint}",
  363. 0x28: "max length is {constraint}",
  364. 0x41: "value does not match regex '{constraint}'",
  365. 0x42: "min value is {constraint}",
  366. 0x43: "max value is {constraint}",
  367. 0x44: "unallowed value {value}",
  368. 0x45: "unallowed values {0}",
  369. 0x46: "unallowed value {value}",
  370. 0x47: "unallowed values {0}",
  371. 0x61: "field '{field}' cannot be coerced: {0}",
  372. 0x62: "field '{field}' cannot be renamed: {0}",
  373. 0x63: "field is read-only",
  374. 0x64: "default value for '{field}' cannot be set: {0}",
  375. 0x81: "mapping doesn't validate subschema: {0}",
  376. 0x82: "one or more sequence-items don't validate: {0}",
  377. 0x83: "one or more keys of a mapping don't validate: {0}",
  378. 0x84: "one or more values in a mapping don't validate: {0}",
  379. 0x85: "one or more sequence-items don't validate: {0}",
  380. 0x91: "one or more definitions validate",
  381. 0x92: "none or more than one rule validate",
  382. 0x93: "no definitions validate",
  383. 0x94: "one or more definitions don't validate"
  384. }
  385. def __init__(self, tree=None):
  386. self.tree = {} if tree is None else tree
  387. def __call__(self, errors=None):
  388. if errors is not None:
  389. self.clear()
  390. self.extend(errors)
  391. return self.pretty_tree
  392. def __str__(self):
  393. return pformat(self.pretty_tree)
  394. @property
  395. def pretty_tree(self):
  396. pretty = deepcopy(self.tree)
  397. for field in pretty:
  398. self._purge_empty_dicts(pretty[field])
  399. return pretty
  400. @encode_unicode
  401. def add(self, error):
  402. # Make sure the original error is not altered with
  403. # error paths specific to the handler.
  404. error = deepcopy(error)
  405. self._rewrite_error_path(error)
  406. if error.is_logic_error:
  407. self._insert_logic_error(error)
  408. elif error.is_group_error:
  409. self._insert_group_error(error)
  410. elif error.code in self.messages:
  411. self._insert_error(error.document_path,
  412. self._format_message(error.field, error))
  413. def clear(self):
  414. self.tree = {}
  415. def start(self, validator):
  416. self.clear()
  417. def _format_message(self, field, error):
  418. return self.messages[error.code].format(
  419. *error.info, constraint=error.constraint,
  420. field=field, value=error.value)
  421. def _insert_error(self, path, node):
  422. """ Adds an error or sub-tree to :attr:tree.
  423. :param path: Path to the error.
  424. :type path: Tuple of strings and integers.
  425. :param node: An error message or a sub-tree.
  426. :type node: String or dictionary.
  427. """
  428. field = path[0]
  429. if len(path) == 1:
  430. if field in self.tree:
  431. subtree = self.tree[field].pop()
  432. self.tree[field] += [node, subtree]
  433. else:
  434. self.tree[field] = [node, {}]
  435. elif len(path) >= 1:
  436. if field not in self.tree:
  437. self.tree[field] = [{}]
  438. subtree = self.tree[field][-1]
  439. if subtree:
  440. new = self.__class__(tree=copy(subtree))
  441. else:
  442. new = self.__class__()
  443. new._insert_error(path[1:], node)
  444. subtree.update(new.tree)
  445. def _insert_group_error(self, error):
  446. for child_error in error.child_errors:
  447. if child_error.is_logic_error:
  448. self._insert_logic_error(child_error)
  449. elif child_error.is_group_error:
  450. self._insert_group_error(child_error)
  451. else:
  452. self._insert_error(child_error.document_path,
  453. self._format_message(child_error.field,
  454. child_error))
  455. def _insert_logic_error(self, error):
  456. field = error.field
  457. self._insert_error(error.document_path,
  458. self._format_message(field, error))
  459. for definition_errors in error.definitions_errors.values():
  460. for child_error in definition_errors:
  461. if child_error.is_logic_error:
  462. self._insert_logic_error(child_error)
  463. elif child_error.is_group_error:
  464. self._insert_group_error(child_error)
  465. else:
  466. self._insert_error(child_error.document_path,
  467. self._format_message(field, child_error))
  468. def _purge_empty_dicts(self, error_list):
  469. subtree = error_list[-1]
  470. if not error_list[-1]:
  471. error_list.pop()
  472. else:
  473. for key in subtree:
  474. self._purge_empty_dicts(subtree[key])
  475. def _rewrite_error_path(self, error, offset=0):
  476. """
  477. Recursively rewrites the error path to correctly represent logic errors
  478. """
  479. if error.is_logic_error:
  480. self._rewrite_logic_error_path(error, offset)
  481. elif error.is_group_error:
  482. self._rewrite_group_error_path(error, offset)
  483. def _rewrite_group_error_path(self, error, offset=0):
  484. child_start = len(error.document_path) - offset
  485. for child_error in error.child_errors:
  486. relative_path = child_error.document_path[child_start:]
  487. child_error.document_path = error.document_path + relative_path
  488. self._rewrite_error_path(child_error, offset)
  489. def _rewrite_logic_error_path(self, error, offset=0):
  490. child_start = len(error.document_path) - offset
  491. for i, definition_errors in error.definitions_errors.items():
  492. if not definition_errors:
  493. continue
  494. nodename = '%s definition %s' % (error.rule, i)
  495. path = error.document_path + (nodename,)
  496. for child_error in definition_errors:
  497. rel_path = child_error.document_path[child_start:]
  498. child_error.document_path = path + rel_path
  499. self._rewrite_error_path(child_error, offset + 1)
  500. class SchemaErrorHandler(BasicErrorHandler):
  501. messages = BasicErrorHandler.messages.copy()
  502. messages[0x03] = "unknown rule"