util.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. """Utilities for assertion debugging"""
  2. from __future__ import absolute_import, division, print_function
  3. import pprint
  4. import _pytest._code
  5. import py
  6. import six
  7. from ..compat import Sequence
  8. u = six.text_type
  9. # The _reprcompare attribute on the util module is used by the new assertion
  10. # interpretation code and assertion rewriter to detect this plugin was
  11. # loaded and in turn call the hooks defined here as part of the
  12. # DebugInterpreter.
  13. _reprcompare = None
  14. # the re-encoding is needed for python2 repr
  15. # with non-ascii characters (see issue 877 and 1379)
  16. def ecu(s):
  17. try:
  18. return u(s, "utf-8", "replace")
  19. except TypeError:
  20. return s
  21. def format_explanation(explanation):
  22. """This formats an explanation
  23. Normally all embedded newlines are escaped, however there are
  24. three exceptions: \n{, \n} and \n~. The first two are intended
  25. cover nested explanations, see function and attribute explanations
  26. for examples (.visit_Call(), visit_Attribute()). The last one is
  27. for when one explanation needs to span multiple lines, e.g. when
  28. displaying diffs.
  29. """
  30. explanation = ecu(explanation)
  31. lines = _split_explanation(explanation)
  32. result = _format_lines(lines)
  33. return u("\n").join(result)
  34. def _split_explanation(explanation):
  35. """Return a list of individual lines in the explanation
  36. This will return a list of lines split on '\n{', '\n}' and '\n~'.
  37. Any other newlines will be escaped and appear in the line as the
  38. literal '\n' characters.
  39. """
  40. raw_lines = (explanation or u("")).split("\n")
  41. lines = [raw_lines[0]]
  42. for values in raw_lines[1:]:
  43. if values and values[0] in ["{", "}", "~", ">"]:
  44. lines.append(values)
  45. else:
  46. lines[-1] += "\\n" + values
  47. return lines
  48. def _format_lines(lines):
  49. """Format the individual lines
  50. This will replace the '{', '}' and '~' characters of our mini
  51. formatting language with the proper 'where ...', 'and ...' and ' +
  52. ...' text, taking care of indentation along the way.
  53. Return a list of formatted lines.
  54. """
  55. result = lines[:1]
  56. stack = [0]
  57. stackcnt = [0]
  58. for line in lines[1:]:
  59. if line.startswith("{"):
  60. if stackcnt[-1]:
  61. s = u("and ")
  62. else:
  63. s = u("where ")
  64. stack.append(len(result))
  65. stackcnt[-1] += 1
  66. stackcnt.append(0)
  67. result.append(u(" +") + u(" ") * (len(stack) - 1) + s + line[1:])
  68. elif line.startswith("}"):
  69. stack.pop()
  70. stackcnt.pop()
  71. result[stack[-1]] += line[1:]
  72. else:
  73. assert line[0] in ["~", ">"]
  74. stack[-1] += 1
  75. indent = len(stack) if line.startswith("~") else len(stack) - 1
  76. result.append(u(" ") * indent + line[1:])
  77. assert len(stack) == 1
  78. return result
  79. # Provide basestring in python3
  80. try:
  81. basestring = basestring
  82. except NameError:
  83. basestring = str
  84. def assertrepr_compare(config, op, left, right):
  85. """Return specialised explanations for some operators/operands"""
  86. width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op
  87. left_repr = py.io.saferepr(left, maxsize=int(width // 2))
  88. right_repr = py.io.saferepr(right, maxsize=width - len(left_repr))
  89. summary = u("%s %s %s") % (ecu(left_repr), op, ecu(right_repr))
  90. def issequence(x):
  91. return isinstance(x, Sequence) and not isinstance(x, basestring)
  92. def istext(x):
  93. return isinstance(x, basestring)
  94. def isdict(x):
  95. return isinstance(x, dict)
  96. def isset(x):
  97. return isinstance(x, (set, frozenset))
  98. def isiterable(obj):
  99. try:
  100. iter(obj)
  101. return not istext(obj)
  102. except TypeError:
  103. return False
  104. verbose = config.getoption("verbose")
  105. explanation = None
  106. try:
  107. if op == "==":
  108. if istext(left) and istext(right):
  109. explanation = _diff_text(left, right, verbose)
  110. else:
  111. if issequence(left) and issequence(right):
  112. explanation = _compare_eq_sequence(left, right, verbose)
  113. elif isset(left) and isset(right):
  114. explanation = _compare_eq_set(left, right, verbose)
  115. elif isdict(left) and isdict(right):
  116. explanation = _compare_eq_dict(left, right, verbose)
  117. if isiterable(left) and isiterable(right):
  118. expl = _compare_eq_iterable(left, right, verbose)
  119. if explanation is not None:
  120. explanation.extend(expl)
  121. else:
  122. explanation = expl
  123. elif op == "not in":
  124. if istext(left) and istext(right):
  125. explanation = _notin_text(left, right, verbose)
  126. except Exception:
  127. explanation = [
  128. u(
  129. "(pytest_assertion plugin: representation of details failed. "
  130. "Probably an object has a faulty __repr__.)"
  131. ),
  132. u(_pytest._code.ExceptionInfo()),
  133. ]
  134. if not explanation:
  135. return None
  136. return [summary] + explanation
  137. def _diff_text(left, right, verbose=False):
  138. """Return the explanation for the diff between text or bytes
  139. Unless --verbose is used this will skip leading and trailing
  140. characters which are identical to keep the diff minimal.
  141. If the input are bytes they will be safely converted to text.
  142. """
  143. from difflib import ndiff
  144. explanation = []
  145. def escape_for_readable_diff(binary_text):
  146. """
  147. Ensures that the internal string is always valid unicode, converting any bytes safely to valid unicode.
  148. This is done using repr() which then needs post-processing to fix the encompassing quotes and un-escape
  149. newlines and carriage returns (#429).
  150. """
  151. r = six.text_type(repr(binary_text)[1:-1])
  152. r = r.replace(r"\n", "\n")
  153. r = r.replace(r"\r", "\r")
  154. return r
  155. if isinstance(left, bytes):
  156. left = escape_for_readable_diff(left)
  157. if isinstance(right, bytes):
  158. right = escape_for_readable_diff(right)
  159. if not verbose:
  160. i = 0 # just in case left or right has zero length
  161. for i in range(min(len(left), len(right))):
  162. if left[i] != right[i]:
  163. break
  164. if i > 42:
  165. i -= 10 # Provide some context
  166. explanation = [
  167. u("Skipping %s identical leading " "characters in diff, use -v to show")
  168. % i
  169. ]
  170. left = left[i:]
  171. right = right[i:]
  172. if len(left) == len(right):
  173. for i in range(len(left)):
  174. if left[-i] != right[-i]:
  175. break
  176. if i > 42:
  177. i -= 10 # Provide some context
  178. explanation += [
  179. u(
  180. "Skipping %s identical trailing "
  181. "characters in diff, use -v to show"
  182. )
  183. % i
  184. ]
  185. left = left[:-i]
  186. right = right[:-i]
  187. keepends = True
  188. if left.isspace() or right.isspace():
  189. left = repr(str(left))
  190. right = repr(str(right))
  191. explanation += [u"Strings contain only whitespace, escaping them using repr()"]
  192. explanation += [
  193. line.strip("\n")
  194. for line in ndiff(left.splitlines(keepends), right.splitlines(keepends))
  195. ]
  196. return explanation
  197. def _compare_eq_iterable(left, right, verbose=False):
  198. if not verbose:
  199. return [u("Use -v to get the full diff")]
  200. # dynamic import to speedup pytest
  201. import difflib
  202. try:
  203. left_formatting = pprint.pformat(left).splitlines()
  204. right_formatting = pprint.pformat(right).splitlines()
  205. explanation = [u("Full diff:")]
  206. except Exception:
  207. # hack: PrettyPrinter.pformat() in python 2 fails when formatting items that can't be sorted(), ie, calling
  208. # sorted() on a list would raise. See issue #718.
  209. # As a workaround, the full diff is generated by using the repr() string of each item of each container.
  210. left_formatting = sorted(repr(x) for x in left)
  211. right_formatting = sorted(repr(x) for x in right)
  212. explanation = [u("Full diff (fallback to calling repr on each item):")]
  213. explanation.extend(
  214. line.strip() for line in difflib.ndiff(left_formatting, right_formatting)
  215. )
  216. return explanation
  217. def _compare_eq_sequence(left, right, verbose=False):
  218. explanation = []
  219. for i in range(min(len(left), len(right))):
  220. if left[i] != right[i]:
  221. explanation += [u("At index %s diff: %r != %r") % (i, left[i], right[i])]
  222. break
  223. if len(left) > len(right):
  224. explanation += [
  225. u("Left contains more items, first extra item: %s")
  226. % py.io.saferepr(left[len(right)])
  227. ]
  228. elif len(left) < len(right):
  229. explanation += [
  230. u("Right contains more items, first extra item: %s")
  231. % py.io.saferepr(right[len(left)])
  232. ]
  233. return explanation
  234. def _compare_eq_set(left, right, verbose=False):
  235. explanation = []
  236. diff_left = left - right
  237. diff_right = right - left
  238. if diff_left:
  239. explanation.append(u("Extra items in the left set:"))
  240. for item in diff_left:
  241. explanation.append(py.io.saferepr(item))
  242. if diff_right:
  243. explanation.append(u("Extra items in the right set:"))
  244. for item in diff_right:
  245. explanation.append(py.io.saferepr(item))
  246. return explanation
  247. def _compare_eq_dict(left, right, verbose=False):
  248. explanation = []
  249. common = set(left).intersection(set(right))
  250. same = {k: left[k] for k in common if left[k] == right[k]}
  251. if same and verbose < 2:
  252. explanation += [u("Omitting %s identical items, use -vv to show") % len(same)]
  253. elif same:
  254. explanation += [u("Common items:")]
  255. explanation += pprint.pformat(same).splitlines()
  256. diff = {k for k in common if left[k] != right[k]}
  257. if diff:
  258. explanation += [u("Differing items:")]
  259. for k in diff:
  260. explanation += [
  261. py.io.saferepr({k: left[k]}) + " != " + py.io.saferepr({k: right[k]})
  262. ]
  263. extra_left = set(left) - set(right)
  264. if extra_left:
  265. explanation.append(u("Left contains more items:"))
  266. explanation.extend(
  267. pprint.pformat({k: left[k] for k in extra_left}).splitlines()
  268. )
  269. extra_right = set(right) - set(left)
  270. if extra_right:
  271. explanation.append(u("Right contains more items:"))
  272. explanation.extend(
  273. pprint.pformat({k: right[k] for k in extra_right}).splitlines()
  274. )
  275. return explanation
  276. def _notin_text(term, text, verbose=False):
  277. index = text.find(term)
  278. head = text[:index]
  279. tail = text[index + len(term) :]
  280. correct_text = head + tail
  281. diff = _diff_text(correct_text, text, verbose)
  282. newdiff = [u("%s is contained here:") % py.io.saferepr(term, maxsize=42)]
  283. for line in diff:
  284. if line.startswith(u("Skipping")):
  285. continue
  286. if line.startswith(u("- ")):
  287. continue
  288. if line.startswith(u("+ ")):
  289. newdiff.append(u(" ") + line[2:])
  290. else:
  291. newdiff.append(line)
  292. return newdiff