Skip to content

Commit

Permalink
Make pygments mandatory and fix string highlighting (#13189)
Browse files Browse the repository at this point in the history
* Make pygments dependency required

Closes #7683

* Also highlight comparisons between strings

Fixes #13175
  • Loading branch information
The-Compiler authored Feb 8, 2025
1 parent de1a488 commit a3d55a6
Show file tree
Hide file tree
Showing 6 changed files with 52 additions and 45 deletions.
1 change: 1 addition & 0 deletions changelog/13175.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The diff is now also highlighted correctly when comparing two strings.
1 change: 1 addition & 0 deletions changelog/7683.improvement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +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).
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,14 @@ dependencies = [
"iniconfig",
"packaging",
"pluggy>=1.5,<2",
"pygments>=2.7.2",
"tomli>=1; python_version<'3.11'",
]
optional-dependencies.dev = [
"argcomplete",
"attrs>=19.2",
"hypothesis>=3.56",
"mock",
"pygments>=2.7.2",
"requests",
"setuptools",
"xmlschema",
Expand Down
54 changes: 17 additions & 37 deletions src/_pytest/_io/terminalwriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@
from typing import final
from typing import Literal
from typing import TextIO
from typing import TYPE_CHECKING

import pygments
from pygments.formatters.terminal import TerminalFormatter
from pygments.lexer import Lexer
from pygments.lexers.diff import DiffLexer
from pygments.lexers.python import PythonLexer

from ..compat import assert_never
from .wcwidth import wcswidth


if TYPE_CHECKING:
from pygments.formatter import Formatter
from pygments.lexer import Lexer


# This code was initially copied from py 1.8.1, file _io/terminalwriter.py.


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

def _get_pygments_lexer(self, lexer: Literal["python", "diff"]) -> Lexer | None:
try:
if lexer == "python":
from pygments.lexers.python import PythonLexer

return PythonLexer()
elif lexer == "diff":
from pygments.lexers.diff import DiffLexer

return DiffLexer()
else:
assert_never(lexer)
except ModuleNotFoundError:
return None

def _get_pygments_formatter(self) -> Formatter | None:
try:
import pygments.util
except ModuleNotFoundError:
return None
def _get_pygments_lexer(self, lexer: Literal["python", "diff"]) -> Lexer:
if lexer == "python":
return PythonLexer()
elif lexer == "diff":
return DiffLexer()
else:
assert_never(lexer)

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

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

try:
from pygments.formatters.terminal import TerminalFormatter

return TerminalFormatter(bg=theme_mode, style=theme)

except pygments.util.ClassNotFound as e:
raise UsageError(
f"PYTEST_THEME environment variable has an invalid value: '{theme}'. "
Expand All @@ -251,16 +236,11 @@ def _highlight(
return source

pygments_lexer = self._get_pygments_lexer(lexer)
if pygments_lexer is None:
return source

pygments_formatter = self._get_pygments_formatter()
if pygments_formatter is None:
return source

from pygments import highlight

highlighted: str = highlight(source, pygments_lexer, pygments_formatter)
highlighted: str = pygments.highlight(
source, pygments_lexer, pygments_formatter
)
# pygments terminal formatter may add a newline when there wasn't one.
# We don't want this, remove.
if highlighted[-1] == "\n" and source[-1] != "\n":
Expand Down
29 changes: 22 additions & 7 deletions src/_pytest/assertion/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ def __call__(self, source: str, lexer: Literal["diff", "python"] = "python") ->
"""Apply highlighting to the given source."""


def dummy_highlighter(source: str, lexer: Literal["diff", "python"] = "python") -> str:
"""Dummy highlighter that returns the text unprocessed.
Needed for _notin_text, as the diff gets post-processed to only show the "+" part.
"""
return source


def format_explanation(explanation: str) -> str:
r"""Format an explanation.
Expand Down Expand Up @@ -242,7 +250,7 @@ def _compare_eq_any(
) -> list[str]:
explanation = []
if istext(left) and istext(right):
explanation = _diff_text(left, right, verbose)
explanation = _diff_text(left, right, highlighter, verbose)
else:
from _pytest.python_api import ApproxBase

Expand Down Expand Up @@ -274,7 +282,9 @@ def _compare_eq_any(
return explanation


def _diff_text(left: str, right: str, verbose: int = 0) -> list[str]:
def _diff_text(
left: str, right: str, highlighter: _HighlightFunc, verbose: int = 0
) -> list[str]:
"""Return the explanation for the diff between text.
Unless --verbose is used this will skip leading and trailing
Expand Down Expand Up @@ -315,10 +325,15 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> list[str]:
explanation += ["Strings contain only whitespace, escaping them using repr()"]
# "right" is the expected base against which we compare "left",
# see https://github.com/pytest-dev/pytest/issues/3333
explanation += [
line.strip("\n")
for line in ndiff(right.splitlines(keepends), left.splitlines(keepends))
]
explanation.extend(
highlighter(
"\n".join(
line.strip("\n")
for line in ndiff(right.splitlines(keepends), left.splitlines(keepends))
),
lexer="diff",
).splitlines()
)
return explanation


Expand Down Expand Up @@ -586,7 +601,7 @@ def _notin_text(term: str, text: str, verbose: int = 0) -> list[str]:
head = text[:index]
tail = text[index + len(term) :]
correct_text = head + tail
diff = _diff_text(text, correct_text, verbose)
diff = _diff_text(text, correct_text, dummy_highlighter, verbose)
newdiff = [f"{saferepr(term, maxsize=42)} is contained here:"]
for line in diff:
if line.startswith("Skipping"):
Expand Down
10 changes: 10 additions & 0 deletions testing/test_assertion.py
Original file line number Diff line number Diff line change
Expand Up @@ -2019,6 +2019,16 @@ def test():
"{bold}{red}E {light-green}+ 'number-is-5': 5,{hl-reset}{endline}{reset}",
],
),
(
"""
def test():
assert "abcd" == "abce"
""",
[
"{bold}{red}E {reset}{light-red}- abce{hl-reset}{endline}{reset}",
"{bold}{red}E {light-green}+ abcd{hl-reset}{endline}{reset}",
],
),
),
)
def test_comparisons_handle_colors(
Expand Down

0 comments on commit a3d55a6

Please sign in to comment.