You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

275 lines
6.9 KiB

4 years ago
  1. from collections import defaultdict, deque
  2. import itertools
  3. import pprint
  4. import textwrap
  5. from jsonschema import _utils
  6. from jsonschema.compat import PY3, iteritems
  7. WEAK_MATCHES = frozenset(["anyOf", "oneOf"])
  8. STRONG_MATCHES = frozenset()
  9. _unset = _utils.Unset()
  10. class _Error(Exception):
  11. def __init__(
  12. self,
  13. message,
  14. validator=_unset,
  15. path=(),
  16. cause=None,
  17. context=(),
  18. validator_value=_unset,
  19. instance=_unset,
  20. schema=_unset,
  21. schema_path=(),
  22. parent=None,
  23. ):
  24. super(_Error, self).__init__(
  25. message,
  26. validator,
  27. path,
  28. cause,
  29. context,
  30. validator_value,
  31. instance,
  32. schema,
  33. schema_path,
  34. parent,
  35. )
  36. self.message = message
  37. self.path = self.relative_path = deque(path)
  38. self.schema_path = self.relative_schema_path = deque(schema_path)
  39. self.context = list(context)
  40. self.cause = self.__cause__ = cause
  41. self.validator = validator
  42. self.validator_value = validator_value
  43. self.instance = instance
  44. self.schema = schema
  45. self.parent = parent
  46. for error in context:
  47. error.parent = self
  48. def __repr__(self):
  49. return "<%s: %r>" % (self.__class__.__name__, self.message)
  50. def __unicode__(self):
  51. essential_for_verbose = (
  52. self.validator, self.validator_value, self.instance, self.schema,
  53. )
  54. if any(m is _unset for m in essential_for_verbose):
  55. return self.message
  56. pschema = pprint.pformat(self.schema, width=72)
  57. pinstance = pprint.pformat(self.instance, width=72)
  58. return self.message + textwrap.dedent("""
  59. Failed validating %r in schema%s:
  60. %s
  61. On instance%s:
  62. %s
  63. """.rstrip()
  64. ) % (
  65. self.validator,
  66. _utils.format_as_index(list(self.relative_schema_path)[:-1]),
  67. _utils.indent(pschema),
  68. _utils.format_as_index(self.relative_path),
  69. _utils.indent(pinstance),
  70. )
  71. if PY3:
  72. __str__ = __unicode__
  73. else:
  74. def __str__(self):
  75. return unicode(self).encode("utf-8")
  76. @classmethod
  77. def create_from(cls, other):
  78. return cls(**other._contents())
  79. @property
  80. def absolute_path(self):
  81. parent = self.parent
  82. if parent is None:
  83. return self.relative_path
  84. path = deque(self.relative_path)
  85. path.extendleft(reversed(parent.absolute_path))
  86. return path
  87. @property
  88. def absolute_schema_path(self):
  89. parent = self.parent
  90. if parent is None:
  91. return self.relative_schema_path
  92. path = deque(self.relative_schema_path)
  93. path.extendleft(reversed(parent.absolute_schema_path))
  94. return path
  95. def _set(self, **kwargs):
  96. for k, v in iteritems(kwargs):
  97. if getattr(self, k) is _unset:
  98. setattr(self, k, v)
  99. def _contents(self):
  100. attrs = (
  101. "message", "cause", "context", "validator", "validator_value",
  102. "path", "schema_path", "instance", "schema", "parent",
  103. )
  104. return dict((attr, getattr(self, attr)) for attr in attrs)
  105. class ValidationError(_Error):
  106. pass
  107. class SchemaError(_Error):
  108. pass
  109. class RefResolutionError(Exception):
  110. pass
  111. class UnknownType(Exception):
  112. def __init__(self, type, instance, schema):
  113. self.type = type
  114. self.instance = instance
  115. self.schema = schema
  116. def __unicode__(self):
  117. pschema = pprint.pformat(self.schema, width=72)
  118. pinstance = pprint.pformat(self.instance, width=72)
  119. return textwrap.dedent("""
  120. Unknown type %r for validator with schema:
  121. %s
  122. While checking instance:
  123. %s
  124. """.rstrip()
  125. ) % (self.type, _utils.indent(pschema), _utils.indent(pinstance))
  126. if PY3:
  127. __str__ = __unicode__
  128. else:
  129. def __str__(self):
  130. return unicode(self).encode("utf-8")
  131. class FormatError(Exception):
  132. def __init__(self, message, cause=None):
  133. super(FormatError, self).__init__(message, cause)
  134. self.message = message
  135. self.cause = self.__cause__ = cause
  136. def __unicode__(self):
  137. return self.message
  138. if PY3:
  139. __str__ = __unicode__
  140. else:
  141. def __str__(self):
  142. return self.message.encode("utf-8")
  143. class ErrorTree(object):
  144. """
  145. ErrorTrees make it easier to check which validations failed.
  146. """
  147. _instance = _unset
  148. def __init__(self, errors=()):
  149. self.errors = {}
  150. self._contents = defaultdict(self.__class__)
  151. for error in errors:
  152. container = self
  153. for element in error.path:
  154. container = container[element]
  155. container.errors[error.validator] = error
  156. container._instance = error.instance
  157. def __contains__(self, index):
  158. """
  159. Check whether ``instance[index]`` has any errors.
  160. """
  161. return index in self._contents
  162. def __getitem__(self, index):
  163. """
  164. Retrieve the child tree one level down at the given ``index``.
  165. If the index is not in the instance that this tree corresponds to and
  166. is not known by this tree, whatever error would be raised by
  167. ``instance.__getitem__`` will be propagated (usually this is some
  168. subclass of :class:`LookupError`.
  169. """
  170. if self._instance is not _unset and index not in self:
  171. self._instance[index]
  172. return self._contents[index]
  173. def __setitem__(self, index, value):
  174. self._contents[index] = value
  175. def __iter__(self):
  176. """
  177. Iterate (non-recursively) over the indices in the instance with errors.
  178. """
  179. return iter(self._contents)
  180. def __len__(self):
  181. """
  182. Same as :attr:`total_errors`.
  183. """
  184. return self.total_errors
  185. def __repr__(self):
  186. return "<%s (%s total errors)>" % (self.__class__.__name__, len(self))
  187. @property
  188. def total_errors(self):
  189. """
  190. The total number of errors in the entire tree, including children.
  191. """
  192. child_errors = sum(len(tree) for _, tree in iteritems(self._contents))
  193. return len(self.errors) + child_errors
  194. def by_relevance(weak=WEAK_MATCHES, strong=STRONG_MATCHES):
  195. def relevance(error):
  196. validator = error.validator
  197. return -len(error.path), validator not in weak, validator in strong
  198. return relevance
  199. relevance = by_relevance()
  200. def best_match(errors, key=relevance):
  201. errors = iter(errors)
  202. best = next(errors, None)
  203. if best is None:
  204. return
  205. best = max(itertools.chain([best], errors), key=key)
  206. while best.context:
  207. best = min(best.context, key=key)
  208. return best