From a925e072e08da377946e498d999876e762c2f904 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 13 Aug 2026 02:53:47 +0200 Subject: [PATCH 1/2] Lazy load pygments and pbd imports --- src/_pytest/_io/terminalwriter.py | 21 ++++++--- src/_pytest/debugging.py | 73 ++++++++++++++++++++++++++----- 2 files changed, 78 insertions(+), 16 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 9191b4edace..954a23b102c 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. @@ -207,13 +207,20 @@ def _write_source(self, lines: Sequence[str], indents: Sequence[str] = ()) -> No def _get_pygments_lexer(self, lexer: Literal["python", "diff"]) -> Lexer: 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) def _get_pygments_formatter(self) -> TerminalFormatter: + from pygments.formatters.terminal import TerminalFormatter + import pygments.util + from _pytest.config.exceptions import UsageError theme = os.getenv("PYTEST_THEME") @@ -239,6 +246,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/src/_pytest/debugging.py b/src/_pytest/debugging.py index b256f83c8bf..c5f03ceba44 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -9,6 +9,8 @@ from collections.abc import Generator import functools import importlib +import importlib.machinery +import importlib.util import sys import types from typing import Any @@ -62,18 +64,70 @@ def pytest_addoption(parser: Parser) -> None: ) -def pytest_configure(config: Config) -> None: - import pdb +class SetTracePatcher: + """Triggered on --pdb CLI opt and when a test fails. + + Importing pdb on 3.14 imports asyncio, ssl and socket + """ + + def __init__(self) -> None: + self.module: Any = None + self.original: Callable[..., None] | None = None + + def install(self) -> None: + module = sys.modules.get("pdb") + if module is not None: + self.patch(module) + else: + sys.meta_path.insert(0, self) + + def uninstall(self) -> None: + if self.module is not None: + self.module.set_trace = self.original + self.module = None + elif self in sys.meta_path: + sys.meta_path.remove(self) + + def find_spec( + self, + name: str, + path: Any = None, + target: types.ModuleType | None = None, + ) -> importlib.machinery.ModuleSpec | None: + if name != "pdb": + return None + + sys.meta_path.remove(self) + spec = importlib.util.find_spec(name) + if spec is None or spec.loader is None or isinstance(spec.loader, type): + sys.meta_path.insert(0, self) + return None + exec_module = spec.loader.exec_module + + def patching_exec_module(module: types.ModuleType) -> None: + exec_module(module) + self.patch(module) + + spec.loader.exec_module = patching_exec_module # type: ignore[method-assign] + return spec + + def patch(self, module: Any) -> None: + self.module = module + self.original = module.set_trace + module.set_trace = pytestPDB.set_trace + + +def pytest_configure(config: Config) -> None: if config.getvalue("trace"): config.pluginmanager.register(PdbTrace(), "pdbtrace") if config.getvalue("usepdb"): config.pluginmanager.register(PdbInvoke(), "pdbinvoke") - pytestPDB._saved.append( - (pdb.set_trace, pytestPDB._pluginmanager, pytestPDB._config) - ) - pdb.set_trace = pytestPDB.set_trace + patcher = SetTracePatcher() + patcher.install() + + pytestPDB._saved.append((patcher, pytestPDB._pluginmanager, pytestPDB._config)) pytestPDB._pluginmanager = config.pluginmanager pytestPDB._config = config @@ -81,10 +135,11 @@ def pytest_configure(config: Config) -> None: # pytest_configure was not (if another plugin raises UsageError). def fin() -> None: ( - pdb.set_trace, + saved_patcher, pytestPDB._pluginmanager, pytestPDB._config, ) = pytestPDB._saved.pop() + saved_patcher.uninstall() config.add_cleanup(fin) @@ -94,9 +149,7 @@ class pytestPDB: _pluginmanager: PytestPluginManager | None = None _config: Config | None = None - _saved: list[ - tuple[Callable[..., None], PytestPluginManager | None, Config | None] - ] = [] + _saved: list[tuple[SetTracePatcher, PytestPluginManager | None, Config | None]] = [] _recursive_debug = 0 _wrapped_pdb_cls: tuple[type[Any], type[Any]] | None = None From 39a1789f994d8e365b3caad6ca6b083f1a13f96a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 13 Aug 2026 18:16:15 +0200 Subject: [PATCH 2/2] Revert lazy pdb import Keep this PR to pygments only. The pdb part needs a sys.meta_path finder, which deserves its own discussion. Co-Authored-By: Claude Opus 5 --- src/_pytest/debugging.py | 73 ++++++---------------------------------- 1 file changed, 10 insertions(+), 63 deletions(-) diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index c5f03ceba44..b256f83c8bf 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -9,8 +9,6 @@ from collections.abc import Generator import functools import importlib -import importlib.machinery -import importlib.util import sys import types from typing import Any @@ -64,70 +62,18 @@ def pytest_addoption(parser: Parser) -> None: ) -class SetTracePatcher: - """Triggered on --pdb CLI opt and when a test fails. - - Importing pdb on 3.14 imports asyncio, ssl and socket - """ - - def __init__(self) -> None: - self.module: Any = None - self.original: Callable[..., None] | None = None - - def install(self) -> None: - module = sys.modules.get("pdb") - if module is not None: - self.patch(module) - else: - sys.meta_path.insert(0, self) - - def uninstall(self) -> None: - if self.module is not None: - self.module.set_trace = self.original - self.module = None - elif self in sys.meta_path: - sys.meta_path.remove(self) - - def find_spec( - self, - name: str, - path: Any = None, - target: types.ModuleType | None = None, - ) -> importlib.machinery.ModuleSpec | None: - if name != "pdb": - return None - - sys.meta_path.remove(self) - spec = importlib.util.find_spec(name) - if spec is None or spec.loader is None or isinstance(spec.loader, type): - sys.meta_path.insert(0, self) - return None - - exec_module = spec.loader.exec_module - - def patching_exec_module(module: types.ModuleType) -> None: - exec_module(module) - self.patch(module) - - spec.loader.exec_module = patching_exec_module # type: ignore[method-assign] - return spec - - def patch(self, module: Any) -> None: - self.module = module - self.original = module.set_trace - module.set_trace = pytestPDB.set_trace - - def pytest_configure(config: Config) -> None: + import pdb + if config.getvalue("trace"): config.pluginmanager.register(PdbTrace(), "pdbtrace") if config.getvalue("usepdb"): config.pluginmanager.register(PdbInvoke(), "pdbinvoke") - patcher = SetTracePatcher() - patcher.install() - - pytestPDB._saved.append((patcher, pytestPDB._pluginmanager, pytestPDB._config)) + pytestPDB._saved.append( + (pdb.set_trace, pytestPDB._pluginmanager, pytestPDB._config) + ) + pdb.set_trace = pytestPDB.set_trace pytestPDB._pluginmanager = config.pluginmanager pytestPDB._config = config @@ -135,11 +81,10 @@ def pytest_configure(config: Config) -> None: # pytest_configure was not (if another plugin raises UsageError). def fin() -> None: ( - saved_patcher, + pdb.set_trace, pytestPDB._pluginmanager, pytestPDB._config, ) = pytestPDB._saved.pop() - saved_patcher.uninstall() config.add_cleanup(fin) @@ -149,7 +94,9 @@ class pytestPDB: _pluginmanager: PytestPluginManager | None = None _config: Config | None = None - _saved: list[tuple[SetTracePatcher, PytestPluginManager | None, Config | None]] = [] + _saved: list[ + tuple[Callable[..., None], PytestPluginManager | None, Config | None] + ] = [] _recursive_debug = 0 _wrapped_pdb_cls: tuple[type[Any], type[Any]] | None = None