Skip to content

Commit 3c66c7d

Browse files
committed
Merge branch 'master' into merge
Conflicts: .github/workflows/main.yml src/_pytest/_code/code.py src/_pytest/_io/__init__.py testing/conftest.py testing/test_assertion.py testing/test_terminal.py
2 parents 8b3035e + e6ea9ed commit 3c66c7d

17 files changed

+590
-124
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ Simon Gomizelj
246246
Skylar Downes
247247
Srinivas Reddy Thatiparthy
248248
Stefan Farmbauer
249+
Stefan Scherfke
249250
Stefan Zimmermann
250251
Stefano Taschini
251252
Steffen Allner

changelog/6658.improvement.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Code is now highlighted in tracebacks when ``pygments`` is installed.
2+
3+
Users are encouraged to install ``pygments`` into their environment and provide feedback, because
4+
the plan is to make ``pygments`` a regular dependency in the future.

changelog/6673.breaking.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Reversed / fix meaning of "+/-" in error diffs. "-" means that sth. expected is missing in the result and "+" means that there are unexpected extras in the result.

doc/en/example/reportingdemo.rst

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ Here is a nice run of several failures and how ``pytest`` presents things:
8181
def test_eq_text(self):
8282
> assert "spam" == "eggs"
8383
E AssertionError: assert 'spam' == 'eggs'
84-
E - spam
85-
E + eggs
84+
E - eggs
85+
E + spam
8686
8787
failure_demo.py:45: AssertionError
8888
_____________ TestSpecialisedExplanations.test_eq_similar_text _____________
@@ -92,9 +92,9 @@ Here is a nice run of several failures and how ``pytest`` presents things:
9292
def test_eq_similar_text(self):
9393
> assert "foo 1 bar" == "foo 2 bar"
9494
E AssertionError: assert 'foo 1 bar' == 'foo 2 bar'
95-
E - foo 1 bar
95+
E - foo 2 bar
9696
E ? ^
97-
E + foo 2 bar
97+
E + foo 1 bar
9898
E ? ^
9999
100100
failure_demo.py:48: AssertionError
@@ -106,8 +106,8 @@ Here is a nice run of several failures and how ``pytest`` presents things:
106106
> assert "foo\nspam\nbar" == "foo\neggs\nbar"
107107
E AssertionError: assert 'foo\nspam\nbar' == 'foo\neggs\nbar'
108108
E foo
109-
E - spam
110-
E + eggs
109+
E - eggs
110+
E + spam
111111
E bar
112112
113113
failure_demo.py:51: AssertionError
@@ -122,9 +122,9 @@ Here is a nice run of several failures and how ``pytest`` presents things:
122122
E AssertionError: assert '111111111111...2222222222222' == '111111111111...2222222222222'
123123
E Skipping 90 identical leading characters in diff, use -v to show
124124
E Skipping 91 identical trailing characters in diff, use -v to show
125-
E - 1111111111a222222222
125+
E - 1111111111b222222222
126126
E ? ^
127-
E + 1111111111b222222222
127+
E + 1111111111a222222222
128128
E ? ^
129129
130130
failure_demo.py:56: AssertionError

src/_pytest/_code/code.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,18 +1066,31 @@ def __init__(
10661066
@staticmethod
10671067
def _color_error_lines(tw: TerminalWriter, lines: Sequence[str]) -> None:
10681068
bold_before = False
1069+
seen_indent_lines = [] # type: List[str]
1070+
seen_source_lines = [] # type: List[str]
10691071
for line in lines:
10701072
if line.startswith("E "):
1073+
if seen_source_lines:
1074+
tw._write_source(seen_source_lines, seen_indent_lines)
1075+
seen_indent_lines = []
1076+
seen_source_lines = []
1077+
10711078
if bold_before:
10721079
markup = tw.markup("E ", bold=True, red=True)
10731080
markup += tw.markup(line[4:])
10741081
else:
10751082
markup = tw.markup(line, bold=True, red=True)
10761083
bold_before = True
1084+
tw.line(markup)
10771085
else:
10781086
bold_before = False
1079-
markup = line
1080-
tw.line(markup)
1087+
seen_indent_lines.append(line[:4])
1088+
seen_source_lines.append(line[4:])
1089+
1090+
if seen_source_lines:
1091+
tw._write_source(seen_source_lines, seen_indent_lines)
1092+
seen_indent_lines = []
1093+
seen_source_lines = []
10811094

10821095
def toterminal(self, tw: TerminalWriter) -> None:
10831096
if self.style == "short":

src/_pytest/_io/__init__.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from typing import List
2+
from typing import Sequence
3+
14
import py.io
25

36

@@ -14,3 +17,36 @@ def fullwidth(self):
1417
@fullwidth.setter
1518
def fullwidth(self, value):
1619
self._terminal_width = value
20+
21+
def _write_source(self, lines: List[str], indents: Sequence[str] = ()) -> None:
22+
"""Write lines of source code possibly highlighted.
23+
24+
Keeping this private for now because the API is clunky. We should discuss how
25+
to evolve the terminal writer so we can have more precise color support, for example
26+
being able to write part of a line in one color and the rest in another, and so on.
27+
"""
28+
if indents and len(indents) != len(lines):
29+
raise ValueError(
30+
"indents size ({}) should have same size as lines ({})".format(
31+
len(indents), len(lines)
32+
)
33+
)
34+
if not indents:
35+
indents = [""] * len(lines)
36+
source = "\n".join(lines)
37+
new_lines = self._highlight(source).splitlines()
38+
for indent, new_line in zip(indents, new_lines):
39+
self.line(indent + new_line)
40+
41+
def _highlight(self, source):
42+
"""Highlight the given source code according to the "code_highlight" option"""
43+
if not self.hasmarkup:
44+
return source
45+
try:
46+
from pygments.formatters.terminal import TerminalFormatter
47+
from pygments.lexers.python import PythonLexer
48+
from pygments import highlight
49+
except ImportError:
50+
return source
51+
else:
52+
return highlight(source, PythonLexer(), TerminalFormatter(bg="dark"))

src/_pytest/_io/saferepr.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,5 +99,5 @@ def _format(self, object, stream, indent, allowance, context, level):
9999

100100
def _pformat_dispatch(object, indent=1, width=80, depth=None, *, compact=False):
101101
return AlwaysDispatchingPrettyPrinter(
102-
indent=1, width=80, depth=None, compact=False
102+
indent=indent, width=width, depth=depth, compact=compact
103103
).pformat(object)

src/_pytest/assertion/util.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -226,9 +226,11 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
226226
left = repr(str(left))
227227
right = repr(str(right))
228228
explanation += ["Strings contain only whitespace, escaping them using repr()"]
229+
# "right" is the expected base against which we compare "left",
230+
# see https://github.com/pytest-dev/pytest/issues/3333
229231
explanation += [
230232
line.strip("\n")
231-
for line in ndiff(left.splitlines(keepends), right.splitlines(keepends))
233+
for line in ndiff(right.splitlines(keepends), left.splitlines(keepends))
232234
]
233235
return explanation
234236

@@ -239,8 +241,8 @@ def _compare_eq_verbose(left: Any, right: Any) -> List[str]:
239241
right_lines = repr(right).splitlines(keepends)
240242

241243
explanation = [] # type: List[str]
242-
explanation += ["-" + line for line in left_lines]
243-
explanation += ["+" + line for line in right_lines]
244+
explanation += ["+" + line for line in left_lines]
245+
explanation += ["-" + line for line in right_lines]
244246

245247
return explanation
246248

@@ -283,8 +285,10 @@ def _compare_eq_iterable(
283285
_surrounding_parens_on_own_lines(right_formatting)
284286

285287
explanation = ["Full diff:"]
288+
# "right" is the expected base against which we compare "left",
289+
# see https://github.com/pytest-dev/pytest/issues/3333
286290
explanation.extend(
287-
line.rstrip() for line in difflib.ndiff(left_formatting, right_formatting)
291+
line.rstrip() for line in difflib.ndiff(right_formatting, left_formatting)
288292
)
289293
return explanation
290294

@@ -319,8 +323,9 @@ def _compare_eq_sequence(
319323
break
320324

321325
if comparing_bytes:
322-
# when comparing bytes, it doesn't help to show the "sides contain one or more items"
323-
# longer explanation, so skip it
326+
# when comparing bytes, it doesn't help to show the "sides contain one or more
327+
# items" longer explanation, so skip it
328+
324329
return explanation
325330

326331
len_diff = len_left - len_right
@@ -447,7 +452,7 @@ def _notin_text(term: str, text: str, verbose: int = 0) -> List[str]:
447452
head = text[:index]
448453
tail = text[index + len(term) :]
449454
correct_text = head + tail
450-
diff = _diff_text(correct_text, text, verbose)
455+
diff = _diff_text(text, correct_text, verbose)
451456
newdiff = ["%s is contained here:" % saferepr(term, maxsize=42)]
452457
for line in diff:
453458
if line.startswith("Skipping"):

testing/acceptance_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1282,8 +1282,8 @@ def test():
12821282
" def check():",
12831283
"> assert 1 == 2",
12841284
"E assert 1 == 2",
1285-
"E -1",
1286-
"E +2",
1285+
"E +1",
1286+
"E -2",
12871287
"",
12881288
"pdb.py:2: AssertionError",
12891289
"*= 1 failed in *",

testing/code/test_terminal_writer.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import re
2+
from io import StringIO
3+
4+
import pytest
5+
from _pytest._io import TerminalWriter
6+
7+
8+
@pytest.mark.parametrize(
9+
"has_markup, expected",
10+
[
11+
pytest.param(
12+
True, "{kw}assert{hl-reset} {number}0{hl-reset}\n", id="with markup"
13+
),
14+
pytest.param(False, "assert 0\n", id="no markup"),
15+
],
16+
)
17+
def test_code_highlight(has_markup, expected, color_mapping):
18+
f = StringIO()
19+
tw = TerminalWriter(f)
20+
tw.hasmarkup = has_markup
21+
tw._write_source(["assert 0"])
22+
assert f.getvalue().splitlines(keepends=True) == color_mapping.format([expected])
23+
24+
with pytest.raises(
25+
ValueError,
26+
match=re.escape("indents size (2) should have same size as lines (1)"),
27+
):
28+
tw._write_source(["assert 0"], [" ", " "])

0 commit comments

Comments
 (0)