Coverage for /usr/local/lib/python3.7/site-packages/_pytest/assertion/util.py : 18%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""Utilities for assertion debugging"""
2import collections.abc
3import pprint
4from typing import AbstractSet
5from typing import Any
6from typing import Callable
7from typing import Iterable
8from typing import List
9from typing import Mapping
10from typing import Optional
11from typing import Sequence
12from typing import Tuple
14import _pytest._code
15from _pytest import outcomes
16from _pytest._io.saferepr import safeformat
17from _pytest._io.saferepr import saferepr
18from _pytest.compat import ATTRS_EQ_FIELD
20# The _reprcompare attribute on the util module is used by the new assertion
21# interpretation code and assertion rewriter to detect this plugin was
22# loaded and in turn call the hooks defined here as part of the
23# DebugInterpreter.
24_reprcompare = None # type: Optional[Callable[[str, object, object], Optional[str]]]
26# Works similarly as _reprcompare attribute. Is populated with the hook call
27# when pytest_runtest_setup is called.
28_assertion_pass = None # type: Optional[Callable[[int, str, str], None]]
31class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter):
32 """PrettyPrinter that always dispatches (regardless of width)."""
34 def _format(self, object, stream, indent, allowance, context, level):
35 p = self._dispatch.get(type(object).__repr__, None)
37 objid = id(object)
38 if objid in context or p is None:
39 return super()._format(object, stream, indent, allowance, context, level)
41 context[objid] = 1
42 p(self, object, stream, indent, allowance, context, level + 1)
43 del context[objid]
46def _pformat_dispatch(object, indent=1, width=80, depth=None, *, compact=False):
47 return AlwaysDispatchingPrettyPrinter(
48 indent=1, width=80, depth=None, compact=False
49 ).pformat(object)
52def format_explanation(explanation: str) -> str:
53 """This formats an explanation
55 Normally all embedded newlines are escaped, however there are
56 three exceptions: \n{, \n} and \n~. The first two are intended
57 cover nested explanations, see function and attribute explanations
58 for examples (.visit_Call(), visit_Attribute()). The last one is
59 for when one explanation needs to span multiple lines, e.g. when
60 displaying diffs.
61 """
62 lines = _split_explanation(explanation)
63 result = _format_lines(lines)
64 return "\n".join(result)
67def _split_explanation(explanation: str) -> List[str]:
68 """Return a list of individual lines in the explanation
70 This will return a list of lines split on '\n{', '\n}' and '\n~'.
71 Any other newlines will be escaped and appear in the line as the
72 literal '\n' characters.
73 """
74 raw_lines = (explanation or "").split("\n")
75 lines = [raw_lines[0]]
76 for values in raw_lines[1:]:
77 if values and values[0] in ["{", "}", "~", ">"]:
78 lines.append(values)
79 else:
80 lines[-1] += "\\n" + values
81 return lines
84def _format_lines(lines: Sequence[str]) -> List[str]:
85 """Format the individual lines
87 This will replace the '{', '}' and '~' characters of our mini
88 formatting language with the proper 'where ...', 'and ...' and ' +
89 ...' text, taking care of indentation along the way.
91 Return a list of formatted lines.
92 """
93 result = list(lines[:1])
94 stack = [0]
95 stackcnt = [0]
96 for line in lines[1:]:
97 if line.startswith("{"):
98 if stackcnt[-1]:
99 s = "and "
100 else:
101 s = "where "
102 stack.append(len(result))
103 stackcnt[-1] += 1
104 stackcnt.append(0)
105 result.append(" +" + " " * (len(stack) - 1) + s + line[1:])
106 elif line.startswith("}"):
107 stack.pop()
108 stackcnt.pop()
109 result[stack[-1]] += line[1:]
110 else:
111 assert line[0] in ["~", ">"]
112 stack[-1] += 1
113 indent = len(stack) if line.startswith("~") else len(stack) - 1
114 result.append(" " * indent + line[1:])
115 assert len(stack) == 1
116 return result
119def issequence(x: Any) -> bool:
120 return isinstance(x, collections.abc.Sequence) and not isinstance(x, str)
123def istext(x: Any) -> bool:
124 return isinstance(x, str)
127def isdict(x: Any) -> bool:
128 return isinstance(x, dict)
131def isset(x: Any) -> bool:
132 return isinstance(x, (set, frozenset))
135def isdatacls(obj: Any) -> bool:
136 return getattr(obj, "__dataclass_fields__", None) is not None
139def isattrs(obj: Any) -> bool:
140 return getattr(obj, "__attrs_attrs__", None) is not None
143def isiterable(obj: Any) -> bool:
144 try:
145 iter(obj)
146 return not istext(obj)
147 except TypeError:
148 return False
151def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[str]]:
152 """Return specialised explanations for some operators/operands"""
153 verbose = config.getoption("verbose")
154 if verbose > 1:
155 left_repr = safeformat(left)
156 right_repr = safeformat(right)
157 else:
158 # XXX: "15 chars indentation" is wrong
159 # ("E AssertionError: assert "); should use term width.
160 maxsize = (
161 80 - 15 - len(op) - 2
162 ) // 2 # 15 chars indentation, 1 space around op
163 left_repr = saferepr(left, maxsize=maxsize)
164 right_repr = saferepr(right, maxsize=maxsize)
166 summary = "{} {} {}".format(left_repr, op, right_repr)
168 explanation = None
169 try:
170 if op == "==":
171 if istext(left) and istext(right):
172 explanation = _diff_text(left, right, verbose)
173 else:
174 if issequence(left) and issequence(right):
175 explanation = _compare_eq_sequence(left, right, verbose)
176 elif isset(left) and isset(right):
177 explanation = _compare_eq_set(left, right, verbose)
178 elif isdict(left) and isdict(right):
179 explanation = _compare_eq_dict(left, right, verbose)
180 elif type(left) == type(right) and (isdatacls(left) or isattrs(left)):
181 type_fn = (isdatacls, isattrs)
182 explanation = _compare_eq_cls(left, right, verbose, type_fn)
183 elif verbose > 0:
184 explanation = _compare_eq_verbose(left, right)
185 if isiterable(left) and isiterable(right):
186 expl = _compare_eq_iterable(left, right, verbose)
187 if explanation is not None:
188 explanation.extend(expl)
189 else:
190 explanation = expl
191 elif op == "not in":
192 if istext(left) and istext(right):
193 explanation = _notin_text(left, right, verbose)
194 except outcomes.Exit:
195 raise
196 except Exception:
197 explanation = [
198 "(pytest_assertion plugin: representation of details failed. "
199 "Probably an object has a faulty __repr__.)",
200 str(_pytest._code.ExceptionInfo.from_current()),
201 ]
203 if not explanation:
204 return None
206 return [summary] + explanation
209def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
210 """Return the explanation for the diff between text.
212 Unless --verbose is used this will skip leading and trailing
213 characters which are identical to keep the diff minimal.
214 """
215 from difflib import ndiff
217 explanation = [] # type: List[str]
219 if verbose < 1:
220 i = 0 # just in case left or right has zero length
221 for i in range(min(len(left), len(right))):
222 if left[i] != right[i]:
223 break
224 if i > 42:
225 i -= 10 # Provide some context
226 explanation = [
227 "Skipping %s identical leading characters in diff, use -v to show" % i
228 ]
229 left = left[i:]
230 right = right[i:]
231 if len(left) == len(right):
232 for i in range(len(left)):
233 if left[-i] != right[-i]:
234 break
235 if i > 42:
236 i -= 10 # Provide some context
237 explanation += [
238 "Skipping {} identical trailing "
239 "characters in diff, use -v to show".format(i)
240 ]
241 left = left[:-i]
242 right = right[:-i]
243 keepends = True
244 if left.isspace() or right.isspace():
245 left = repr(str(left))
246 right = repr(str(right))
247 explanation += ["Strings contain only whitespace, escaping them using repr()"]
248 explanation += [
249 line.strip("\n")
250 for line in ndiff(left.splitlines(keepends), right.splitlines(keepends))
251 ]
252 return explanation
255def _compare_eq_verbose(left: Any, right: Any) -> List[str]:
256 keepends = True
257 left_lines = repr(left).splitlines(keepends)
258 right_lines = repr(right).splitlines(keepends)
260 explanation = [] # type: List[str]
261 explanation += ["-" + line for line in left_lines]
262 explanation += ["+" + line for line in right_lines]
264 return explanation
267def _surrounding_parens_on_own_lines(lines: List[str]) -> None:
268 """Move opening/closing parenthesis/bracket to own lines."""
269 opening = lines[0][:1]
270 if opening in ["(", "[", "{"]:
271 lines[0] = " " + lines[0][1:]
272 lines[:] = [opening] + lines
273 closing = lines[-1][-1:]
274 if closing in [")", "]", "}"]:
275 lines[-1] = lines[-1][:-1] + ","
276 lines[:] = lines + [closing]
279def _compare_eq_iterable(
280 left: Iterable[Any], right: Iterable[Any], verbose: int = 0
281) -> List[str]:
282 if not verbose:
283 return ["Use -v to get the full diff"]
284 # dynamic import to speedup pytest
285 import difflib
287 left_formatting = pprint.pformat(left).splitlines()
288 right_formatting = pprint.pformat(right).splitlines()
290 # Re-format for different output lengths.
291 lines_left = len(left_formatting)
292 lines_right = len(right_formatting)
293 if lines_left != lines_right:
294 left_formatting = _pformat_dispatch(left).splitlines()
295 right_formatting = _pformat_dispatch(right).splitlines()
297 if lines_left > 1 or lines_right > 1:
298 _surrounding_parens_on_own_lines(left_formatting)
299 _surrounding_parens_on_own_lines(right_formatting)
301 explanation = ["Full diff:"]
302 explanation.extend(
303 line.rstrip() for line in difflib.ndiff(left_formatting, right_formatting)
304 )
305 return explanation
308def _compare_eq_sequence(
309 left: Sequence[Any], right: Sequence[Any], verbose: int = 0
310) -> List[str]:
311 comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes)
312 explanation = [] # type: List[str]
313 len_left = len(left)
314 len_right = len(right)
315 for i in range(min(len_left, len_right)):
316 if left[i] != right[i]:
317 if comparing_bytes:
318 # when comparing bytes, we want to see their ascii representation
319 # instead of their numeric values (#5260)
320 # using a slice gives us the ascii representation:
321 # >>> s = b'foo'
322 # >>> s[0]
323 # 102
324 # >>> s[0:1]
325 # b'f'
326 left_value = left[i : i + 1]
327 right_value = right[i : i + 1]
328 else:
329 left_value = left[i]
330 right_value = right[i]
332 explanation += [
333 "At index {} diff: {!r} != {!r}".format(i, left_value, right_value)
334 ]
335 break
337 if comparing_bytes:
338 # when comparing bytes, it doesn't help to show the "sides contain one or more items"
339 # longer explanation, so skip it
340 return explanation
342 len_diff = len_left - len_right
343 if len_diff:
344 if len_diff > 0:
345 dir_with_more = "Left"
346 extra = saferepr(left[len_right])
347 else:
348 len_diff = 0 - len_diff
349 dir_with_more = "Right"
350 extra = saferepr(right[len_left])
352 if len_diff == 1:
353 explanation += [
354 "{} contains one more item: {}".format(dir_with_more, extra)
355 ]
356 else:
357 explanation += [
358 "%s contains %d more items, first extra item: %s"
359 % (dir_with_more, len_diff, extra)
360 ]
361 return explanation
364def _compare_eq_set(
365 left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
366) -> List[str]:
367 explanation = []
368 diff_left = left - right
369 diff_right = right - left
370 if diff_left:
371 explanation.append("Extra items in the left set:")
372 for item in diff_left:
373 explanation.append(saferepr(item))
374 if diff_right:
375 explanation.append("Extra items in the right set:")
376 for item in diff_right:
377 explanation.append(saferepr(item))
378 return explanation
381def _compare_eq_dict(
382 left: Mapping[Any, Any], right: Mapping[Any, Any], verbose: int = 0
383) -> List[str]:
384 explanation = [] # type: List[str]
385 set_left = set(left)
386 set_right = set(right)
387 common = set_left.intersection(set_right)
388 same = {k: left[k] for k in common if left[k] == right[k]}
389 if same and verbose < 2:
390 explanation += ["Omitting %s identical items, use -vv to show" % len(same)]
391 elif same:
392 explanation += ["Common items:"]
393 explanation += pprint.pformat(same).splitlines()
394 diff = {k for k in common if left[k] != right[k]}
395 if diff:
396 explanation += ["Differing items:"]
397 for k in diff:
398 explanation += [saferepr({k: left[k]}) + " != " + saferepr({k: right[k]})]
399 extra_left = set_left - set_right
400 len_extra_left = len(extra_left)
401 if len_extra_left:
402 explanation.append(
403 "Left contains %d more item%s:"
404 % (len_extra_left, "" if len_extra_left == 1 else "s")
405 )
406 explanation.extend(
407 pprint.pformat({k: left[k] for k in extra_left}).splitlines()
408 )
409 extra_right = set_right - set_left
410 len_extra_right = len(extra_right)
411 if len_extra_right:
412 explanation.append(
413 "Right contains %d more item%s:"
414 % (len_extra_right, "" if len_extra_right == 1 else "s")
415 )
416 explanation.extend(
417 pprint.pformat({k: right[k] for k in extra_right}).splitlines()
418 )
419 return explanation
422def _compare_eq_cls(
423 left: Any,
424 right: Any,
425 verbose: int,
426 type_fns: Tuple[Callable[[Any], bool], Callable[[Any], bool]],
427) -> List[str]:
428 isdatacls, isattrs = type_fns
429 if isdatacls(left):
430 all_fields = left.__dataclass_fields__
431 fields_to_check = [field for field, info in all_fields.items() if info.compare]
432 elif isattrs(left):
433 all_fields = left.__attrs_attrs__
434 fields_to_check = [
435 field.name for field in all_fields if getattr(field, ATTRS_EQ_FIELD)
436 ]
438 same = []
439 diff = []
440 for field in fields_to_check:
441 if getattr(left, field) == getattr(right, field):
442 same.append(field)
443 else:
444 diff.append(field)
446 explanation = []
447 if same and verbose < 2:
448 explanation.append("Omitting %s identical items, use -vv to show" % len(same))
449 elif same:
450 explanation += ["Matching attributes:"]
451 explanation += pprint.pformat(same).splitlines()
452 if diff:
453 explanation += ["Differing attributes:"]
454 for field in diff:
455 explanation += [
456 ("%s: %r != %r") % (field, getattr(left, field), getattr(right, field))
457 ]
458 return explanation
461def _notin_text(term: str, text: str, verbose: int = 0) -> List[str]:
462 index = text.find(term)
463 head = text[:index]
464 tail = text[index + len(term) :]
465 correct_text = head + tail
466 diff = _diff_text(correct_text, text, verbose)
467 newdiff = ["%s is contained here:" % saferepr(term, maxsize=42)]
468 for line in diff:
469 if line.startswith("Skipping"):
470 continue
471 if line.startswith("- "):
472 continue
473 if line.startswith("+ "):
474 newdiff.append(" " + line[2:])
475 else:
476 newdiff.append(line)
477 return newdiff