Skip to content

Commit 9d6dabc

Browse files
committed
Parse on ReprEntry
1 parent 0c902ec commit 9d6dabc

File tree

5 files changed

+112
-129
lines changed

5 files changed

+112
-129
lines changed

src/_pytest/_code/code.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import py
3030

3131
import _pytest
32-
from _pytest._io import ParsedTraceback
3332
from _pytest._io import TerminalWriter
3433
from _pytest._io.saferepr import safeformat
3534
from _pytest._io.saferepr import saferepr
@@ -1039,26 +1038,31 @@ def __init__(
10391038
self.style = style
10401039

10411040
def toterminal(self, tw: TerminalWriter) -> None:
1041+
def is_fail(line):
1042+
return line.startswith("{} ".format(FormattedExcinfo.fail_marker))
10421043

1043-
traceback_lines = ParsedTraceback.from_lines(self.lines)
1044-
source = "\n".join(traceback_lines.sources + traceback_lines.errors) + "\n"
1044+
indents_and_source_lines = [
1045+
(x[:4], x[4:]) for x in self.lines if not is_fail(x)
1046+
]
1047+
indents, source_lines = list(zip(*indents_and_source_lines))
1048+
1049+
def write_tb_lines():
1050+
tw._write_source(source_lines, indents)
1051+
for line in (x for x in self.lines if is_fail(x)):
1052+
tw.line(line, bold=True, red=True)
10451053

10461054
if self.style == "short":
10471055
assert self.reprfileloc is not None
10481056
self.reprfileloc.toterminal(tw)
1049-
tw.write_source(source)
1050-
for line in traceback_lines.explanations:
1051-
tw.line(line, bold=True, red=True)
1057+
write_tb_lines()
10521058
if self.reprlocals:
10531059
self.reprlocals.toterminal(tw, indent=" " * 8)
10541060
return
10551061

10561062
if self.reprfuncargs:
10571063
self.reprfuncargs.toterminal(tw)
10581064

1059-
tw.write_source(source)
1060-
for line in traceback_lines.explanations:
1061-
tw.line(line, bold=True, red=True)
1065+
write_tb_lines()
10621066

10631067
if self.reprlocals:
10641068
tw.line("")

src/_pytest/_io/__init__.py

Lines changed: 14 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
1-
from enum import auto
2-
from enum import Enum
1+
from typing import List
32
from typing import Sequence
4-
from typing import Tuple
53

6-
import attr
74
from py.io import TerminalWriter as BaseTerminalWriter # noqa: F401
85

96

107
class TerminalWriter(BaseTerminalWriter):
11-
def write_source(self, source: str) -> None:
8+
def _write_source(self, lines: List[str], indents: Sequence[str] = ()) -> None:
129
"""Write lines of source code possibly highlighted."""
13-
self.write(self._highlight(source))
10+
if indents and len(indents) != len(lines):
11+
raise ValueError(
12+
"indents size ({}) should have same size as lines ({})".format(
13+
len(indents), len(lines)
14+
)
15+
)
16+
if not indents:
17+
indents = [""] * len(lines)
18+
source = "\n".join(lines)
19+
new_lines = self._highlight(source).splitlines()
20+
for indent, new_line in zip(indents, new_lines):
21+
self.line(indent + new_line)
1422

1523
def _highlight(self, source):
1624
"""Highlight the given source code according to the "code_highlight" option"""
@@ -24,44 +32,3 @@ def _highlight(self, source):
2432
return source
2533
else:
2634
return highlight(source, PythonLexer(), TerminalFormatter(bg="dark"))
27-
28-
29-
@attr.s(frozen=True)
30-
class ParsedTraceback:
31-
sources = attr.ib() # type: Tuple[str, ...]
32-
errors = attr.ib() # type: Tuple[str, ...]
33-
explanations = attr.ib() # type: Tuple[str, ...]
34-
35-
ERROR_PREFIX = "> "
36-
EXPLANATION_PREFIX = "E "
37-
38-
class State(Enum):
39-
source = auto()
40-
error = auto()
41-
explanation = auto()
42-
43-
@classmethod
44-
def from_lines(cls, lines: Sequence[str]) -> "ParsedTraceback":
45-
sources = []
46-
errors = []
47-
explanations = []
48-
49-
state = cls.State.source
50-
51-
for line in lines:
52-
if state == cls.State.source and line.startswith(cls.ERROR_PREFIX):
53-
state = cls.State.error
54-
if line.startswith(cls.EXPLANATION_PREFIX):
55-
state = cls.State.explanation
56-
57-
if state == cls.State.source:
58-
sources.append(line)
59-
elif state == cls.State.error:
60-
errors.append(line)
61-
else:
62-
assert state == cls.State.explanation, "unknown state {!r}".format(
63-
state
64-
)
65-
explanations.append(line)
66-
67-
return ParsedTraceback(tuple(sources), tuple(errors), tuple(explanations))

testing/code/test_code.py

Lines changed: 0 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from _pytest._code import ExceptionInfo
88
from _pytest._code import Frame
99
from _pytest._code.code import ReprFuncArgs
10-
from _pytest._io import ParsedTraceback
1110

1211

1312
def test_ne() -> None:
@@ -181,59 +180,3 @@ def test_not_raise_exception_with_mixed_encoding(self, tw_mock) -> None:
181180
tw_mock.lines[0]
182181
== r"unicode_string = São Paulo, utf8_string = b'S\xc3\xa3o Paulo'"
183182
)
184-
185-
186-
class TestParsedTraceback:
187-
def test_simple(self):
188-
lines = [
189-
" def test():\n",
190-
" print('hello')\n",
191-
" foo = y()\n",
192-
"> assert foo == z()\n",
193-
"E assert 1 == 2\n",
194-
"E + where 2 = z()\n",
195-
]
196-
traceback_lines = ParsedTraceback.from_lines(lines)
197-
assert traceback_lines.sources == (
198-
" def test():\n",
199-
" print('hello')\n",
200-
" foo = y()\n",
201-
)
202-
assert traceback_lines.errors == ("> assert foo == z()\n",)
203-
assert traceback_lines.explanations == (
204-
"E assert 1 == 2\n",
205-
"E + where 2 = z()\n",
206-
)
207-
208-
def test_with_continuations(self):
209-
lines = [
210-
" def test():\n",
211-
" foo = \\\n",
212-
" y()\n",
213-
"> assert foo == \\\n",
214-
" z()\n",
215-
"E assert 1 == 2\n",
216-
"E + where 2 = z()\n",
217-
]
218-
result = ParsedTraceback.from_lines(lines)
219-
assert result.sources == (
220-
" def test():\n",
221-
" foo = \\\n",
222-
" y()\n",
223-
)
224-
assert result.errors == ("> assert foo == \\\n", " z()\n",)
225-
assert result.explanations == (
226-
"E assert 1 == 2\n",
227-
"E + where 2 = z()\n",
228-
)
229-
230-
def test_no_flow_lines(self):
231-
lines = [
232-
" assert 0\n",
233-
"E assert 0\n",
234-
]
235-
236-
result = ParsedTraceback.from_lines(lines)
237-
assert result.sources == (" assert 0\n",)
238-
assert result.errors == ()
239-
assert result.explanations == ("E assert 0\n",)

testing/code/test_terminal_writer.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import re
2+
from io import StringIO
3+
4+
import pytest
5+
from _pytest._io import TerminalWriter
6+
7+
8+
# TODO: move this and the other two related attributes from test_terminal.py into conftest as a
9+
# fixture
10+
COLORS = {
11+
"red": "\x1b[31m",
12+
"green": "\x1b[32m",
13+
"yellow": "\x1b[33m",
14+
"bold": "\x1b[1m",
15+
"reset": "\x1b[0m",
16+
"kw": "\x1b[94m",
17+
"hl-reset": "\x1b[39;49;00m",
18+
"function": "\x1b[92m",
19+
"number": "\x1b[94m",
20+
"str": "\x1b[33m",
21+
"print": "\x1b[96m",
22+
}
23+
24+
25+
@pytest.mark.parametrize(
26+
"has_markup, expected",
27+
[
28+
pytest.param(
29+
True, "{kw}assert{hl-reset} {number}0{hl-reset}\n", id="with markup"
30+
),
31+
pytest.param(False, "assert 0\n", id="no markup"),
32+
],
33+
)
34+
def test_code_highlight(has_markup, expected):
35+
f = StringIO()
36+
tw = TerminalWriter(f)
37+
tw.hasmarkup = has_markup
38+
tw._write_source(["assert 0"])
39+
assert f.getvalue() == expected.format(**COLORS)
40+
41+
with pytest.raises(
42+
ValueError,
43+
match=re.escape("indents size (2) should have same size as lines (1)"),
44+
):
45+
tw._write_source(["assert 0"], [" ", " "])

testing/test_terminal.py

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
"hl-reset": "\x1b[39;49;00m",
3535
"function": "\x1b[92m",
3636
"number": "\x1b[94m",
37+
"str": "\x1b[33m",
38+
"print": "\x1b[96m",
3739
}
3840
RE_COLORS = {k: re.escape(v) for k, v in COLORS.items()}
3941

@@ -2025,21 +2027,43 @@ def test_via_exec(testdir: Testdir) -> None:
20252027
)
20262028

20272029

2028-
def test_code_highlight(testdir: Testdir) -> None:
2029-
testdir.makepyfile(
2030+
class TestCodeHighlight:
2031+
def test_code_highlight_simple(self, testdir: Testdir) -> None:
2032+
testdir.makepyfile(
2033+
"""
2034+
def test_foo():
2035+
assert 1 == 10
20302036
"""
2031-
def test_foo():
2032-
assert 1 == 10
2033-
"""
2034-
)
2035-
result = testdir.runpytest("--color=yes")
2036-
result.stdout.fnmatch_lines(
2037-
[
2038-
line.format(**COLORS).replace("[", "[[]")
2039-
for line in [
2040-
" {kw}def{hl-reset} {function}test_foo{hl-reset}():",
2041-
"> {kw}assert{hl-reset} {number}1{hl-reset} == {number}10{hl-reset}",
2042-
"{bold}{red}E assert 1 == 10{reset}",
2037+
)
2038+
result = testdir.runpytest("--color=yes")
2039+
result.stdout.fnmatch_lines(
2040+
[
2041+
line.format(**COLORS).replace("[", "[[]")
2042+
for line in [
2043+
" {kw}def{hl-reset} {function}test_foo{hl-reset}():",
2044+
"> {kw}assert{hl-reset} {number}1{hl-reset} == {number}10{hl-reset}",
2045+
"{bold}{red}E assert 1 == 10{reset}",
2046+
]
20432047
]
2044-
]
2045-
)
2048+
)
2049+
2050+
def test_code_highlight_continuation(self, testdir: Testdir) -> None:
2051+
testdir.makepyfile(
2052+
"""
2053+
def test_foo():
2054+
print('''
2055+
'''); assert 0
2056+
"""
2057+
)
2058+
result = testdir.runpytest("--color=yes")
2059+
result.stdout.fnmatch_lines(
2060+
[
2061+
line.format(**COLORS).replace("[", "[[]")
2062+
for line in [
2063+
" {kw}def{hl-reset} {function}test_foo{hl-reset}():",
2064+
" {print}print{hl-reset}({str}'''{hl-reset}{str}{hl-reset}",
2065+
"> {str} {hl-reset}{str}'''{hl-reset}); {kw}assert{hl-reset} {number}0{hl-reset}",
2066+
"{bold}{red}E assert 0{reset}",
2067+
]
2068+
]
2069+
)

0 commit comments

Comments
 (0)