Skip to content

Commit a3d55a6

Browse files
authored
Make pygments mandatory and fix string highlighting (#13189)
* Make pygments dependency required Closes #7683 * Also highlight comparisons between strings Fixes #13175
1 parent de1a488 commit a3d55a6

File tree

6 files changed

+52
-45
lines changed

6 files changed

+52
-45
lines changed

changelog/13175.bugfix.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The diff is now also highlighted correctly when comparing two strings.

changelog/7683.improvement.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The formerly optional ``pygments`` dependency is now required, causing output always to be source-highlighted (unless disabled via the ``--code-highlight=no`` CLI option).

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,14 @@ dependencies = [
5151
"iniconfig",
5252
"packaging",
5353
"pluggy>=1.5,<2",
54+
"pygments>=2.7.2",
5455
"tomli>=1; python_version<'3.11'",
5556
]
5657
optional-dependencies.dev = [
5758
"argcomplete",
5859
"attrs>=19.2",
5960
"hypothesis>=3.56",
6061
"mock",
61-
"pygments>=2.7.2",
6262
"requests",
6363
"setuptools",
6464
"xmlschema",

src/_pytest/_io/terminalwriter.py

+17-37
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@
99
from typing import final
1010
from typing import Literal
1111
from typing import TextIO
12-
from typing import TYPE_CHECKING
12+
13+
import pygments
14+
from pygments.formatters.terminal import TerminalFormatter
15+
from pygments.lexer import Lexer
16+
from pygments.lexers.diff import DiffLexer
17+
from pygments.lexers.python import PythonLexer
1318

1419
from ..compat import assert_never
1520
from .wcwidth import wcswidth
1621

1722

18-
if TYPE_CHECKING:
19-
from pygments.formatter import Formatter
20-
from pygments.lexer import Lexer
21-
22-
2323
# This code was initially copied from py 1.8.1, file _io/terminalwriter.py.
2424

2525

@@ -201,37 +201,22 @@ def _write_source(self, lines: Sequence[str], indents: Sequence[str] = ()) -> No
201201
for indent, new_line in zip(indents, new_lines):
202202
self.line(indent + new_line)
203203

204-
def _get_pygments_lexer(self, lexer: Literal["python", "diff"]) -> Lexer | None:
205-
try:
206-
if lexer == "python":
207-
from pygments.lexers.python import PythonLexer
208-
209-
return PythonLexer()
210-
elif lexer == "diff":
211-
from pygments.lexers.diff import DiffLexer
212-
213-
return DiffLexer()
214-
else:
215-
assert_never(lexer)
216-
except ModuleNotFoundError:
217-
return None
218-
219-
def _get_pygments_formatter(self) -> Formatter | None:
220-
try:
221-
import pygments.util
222-
except ModuleNotFoundError:
223-
return None
204+
def _get_pygments_lexer(self, lexer: Literal["python", "diff"]) -> Lexer:
205+
if lexer == "python":
206+
return PythonLexer()
207+
elif lexer == "diff":
208+
return DiffLexer()
209+
else:
210+
assert_never(lexer)
224211

212+
def _get_pygments_formatter(self) -> TerminalFormatter:
225213
from _pytest.config.exceptions import UsageError
226214

227215
theme = os.getenv("PYTEST_THEME")
228216
theme_mode = os.getenv("PYTEST_THEME_MODE", "dark")
229217

230218
try:
231-
from pygments.formatters.terminal import TerminalFormatter
232-
233219
return TerminalFormatter(bg=theme_mode, style=theme)
234-
235220
except pygments.util.ClassNotFound as e:
236221
raise UsageError(
237222
f"PYTEST_THEME environment variable has an invalid value: '{theme}'. "
@@ -251,16 +236,11 @@ def _highlight(
251236
return source
252237

253238
pygments_lexer = self._get_pygments_lexer(lexer)
254-
if pygments_lexer is None:
255-
return source
256-
257239
pygments_formatter = self._get_pygments_formatter()
258-
if pygments_formatter is None:
259-
return source
260-
261-
from pygments import highlight
262240

263-
highlighted: str = highlight(source, pygments_lexer, pygments_formatter)
241+
highlighted: str = pygments.highlight(
242+
source, pygments_lexer, pygments_formatter
243+
)
264244
# pygments terminal formatter may add a newline when there wasn't one.
265245
# We don't want this, remove.
266246
if highlighted[-1] == "\n" and source[-1] != "\n":

src/_pytest/assertion/util.py

+22-7
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ def __call__(self, source: str, lexer: Literal["diff", "python"] = "python") ->
4343
"""Apply highlighting to the given source."""
4444

4545

46+
def dummy_highlighter(source: str, lexer: Literal["diff", "python"] = "python") -> str:
47+
"""Dummy highlighter that returns the text unprocessed.
48+
49+
Needed for _notin_text, as the diff gets post-processed to only show the "+" part.
50+
"""
51+
return source
52+
53+
4654
def format_explanation(explanation: str) -> str:
4755
r"""Format an explanation.
4856
@@ -242,7 +250,7 @@ def _compare_eq_any(
242250
) -> list[str]:
243251
explanation = []
244252
if istext(left) and istext(right):
245-
explanation = _diff_text(left, right, verbose)
253+
explanation = _diff_text(left, right, highlighter, verbose)
246254
else:
247255
from _pytest.python_api import ApproxBase
248256

@@ -274,7 +282,9 @@ def _compare_eq_any(
274282
return explanation
275283

276284

277-
def _diff_text(left: str, right: str, verbose: int = 0) -> list[str]:
285+
def _diff_text(
286+
left: str, right: str, highlighter: _HighlightFunc, verbose: int = 0
287+
) -> list[str]:
278288
"""Return the explanation for the diff between text.
279289
280290
Unless --verbose is used this will skip leading and trailing
@@ -315,10 +325,15 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> list[str]:
315325
explanation += ["Strings contain only whitespace, escaping them using repr()"]
316326
# "right" is the expected base against which we compare "left",
317327
# see https://github.com/pytest-dev/pytest/issues/3333
318-
explanation += [
319-
line.strip("\n")
320-
for line in ndiff(right.splitlines(keepends), left.splitlines(keepends))
321-
]
328+
explanation.extend(
329+
highlighter(
330+
"\n".join(
331+
line.strip("\n")
332+
for line in ndiff(right.splitlines(keepends), left.splitlines(keepends))
333+
),
334+
lexer="diff",
335+
).splitlines()
336+
)
322337
return explanation
323338

324339

@@ -586,7 +601,7 @@ def _notin_text(term: str, text: str, verbose: int = 0) -> list[str]:
586601
head = text[:index]
587602
tail = text[index + len(term) :]
588603
correct_text = head + tail
589-
diff = _diff_text(text, correct_text, verbose)
604+
diff = _diff_text(text, correct_text, dummy_highlighter, verbose)
590605
newdiff = [f"{saferepr(term, maxsize=42)} is contained here:"]
591606
for line in diff:
592607
if line.startswith("Skipping"):

testing/test_assertion.py

+10
Original file line numberDiff line numberDiff line change
@@ -2019,6 +2019,16 @@ def test():
20192019
"{bold}{red}E {light-green}+ 'number-is-5': 5,{hl-reset}{endline}{reset}",
20202020
],
20212021
),
2022+
(
2023+
"""
2024+
def test():
2025+
assert "abcd" == "abce"
2026+
""",
2027+
[
2028+
"{bold}{red}E {reset}{light-red}- abce{hl-reset}{endline}{reset}",
2029+
"{bold}{red}E {light-green}+ abcd{hl-reset}{endline}{reset}",
2030+
],
2031+
),
20222032
),
20232033
)
20242034
def test_comparisons_handle_colors(

0 commit comments

Comments
 (0)