diff --git a/changelog/14948.improvement.rst b/changelog/14948.improvement.rst new file mode 100644 index 00000000000..3e3f40a5367 --- /dev/null +++ b/changelog/14948.improvement.rst @@ -0,0 +1,6 @@ +``pygments`` is no longer imported when the ``_pytest._io.TerminalWriter`` +module is loaded. It is now imported lazily, only when source code is actually +syntax-highlighted (e.g. when rendering a traceback with color output enabled). +This avoids importing the relatively heavy ``pygments`` package on runs that +don't need it (notably runs without markup, such as most CI executions), +slightly reducing pytest's startup time. diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 9191b4edace..265b7b512bc 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -9,17 +9,17 @@ from typing import final from typing import Literal from typing import TextIO - -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 typing import TYPE_CHECKING from ..compat import assert_never from .wcwidth import wcswidth +if TYPE_CHECKING: + from pygments.formatters.terminal import TerminalFormatter + from pygments.lexer import Lexer + + # This code was initially copied from py 1.8.1, file _io/terminalwriter.py. @@ -206,6 +206,9 @@ def _write_source(self, lines: Sequence[str], indents: Sequence[str] = ()) -> No self.line(indent + new_line) def _get_pygments_lexer(self, lexer: Literal["python", "diff"]) -> Lexer: + from pygments.lexers.diff import DiffLexer + from pygments.lexers.python import PythonLexer + if lexer == "python": return PythonLexer() elif lexer == "diff": @@ -214,6 +217,9 @@ def _get_pygments_lexer(self, lexer: Literal["python", "diff"]) -> Lexer: assert_never(lexer) def _get_pygments_formatter(self) -> TerminalFormatter: + import pygments + from pygments.formatters.terminal import TerminalFormatter + from _pytest.config.exceptions import UsageError theme = os.getenv("PYTEST_THEME") @@ -239,6 +245,8 @@ def _highlight( if not source or not self.hasmarkup or not self.code_highlight: return source + import pygments + pygments_lexer = self._get_pygments_lexer(lexer) pygments_formatter = self._get_pygments_formatter() diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 30208084ab2..7c38af17329 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -2909,6 +2909,29 @@ def test_foo(): ) +def test_terminalwriter_import_does_not_import_pygments() -> None: + """Importing the module must not eagerly import pygments. + + pygments is imported lazily only when source is actually highlighted, + so pytest startup (including runs without markup) should not pay the + cost of importing it. + """ + import subprocess + + code = ( + "import sys; " + "import _pytest._io.terminalwriter; " + "print('pygments' in sys.modules)" + ) + result = subprocess.run( + [sys.executable, "-c", code], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + assert result.stdout.strip() == "False" + + def test_raw_skip_reason_skipped() -> None: report = SimpleNamespace() report.skipped = True