From 3dc7d3807bdf556ed26f7393cffc2d46c8a0b0cc Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 10 Nov 2025 23:35:30 +0200 Subject: [PATCH 1/2] debugging: import unittest lazily Fix #13917 --- src/_pytest/debugging.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index de1b2688f76..75a37a867cb 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -11,7 +11,6 @@ import sys import types from typing import Any -import unittest from _pytest import outcomes from _pytest._code import ExceptionInfo @@ -296,7 +295,8 @@ def pytest_exception_interact( sys.stdout.write(err) assert call.excinfo is not None - if not isinstance(call.excinfo.value, unittest.SkipTest): + unittest = sys.modules.get("unittest") + if unittest is None or not isinstance(call.excinfo.value, unittest.SkipTest): _enter_pdb(node, call.excinfo, report) def pytest_internalerror(self, excinfo: ExceptionInfo[BaseException]) -> None: From 15ab2c7940f0d22d2a3f48146b55547a6979e6e7 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 10 Nov 2025 23:41:33 +0200 Subject: [PATCH 2/2] runner: don't treat `unittest.SkipTest` as an interactive exception Instead of special casing in the debugging plugin, it seems more correct to just not treat `unittest.SkipTest` as an interactive exception, similarly to pytest's own `Skipped`. The `pytest_make_collect_report` hook in runner.py already treats them the same. This fixes the issue more generally. --- changelog/13917.bugfix.rst | 1 + src/_pytest/debugging.py | 5 +---- src/_pytest/runner.py | 5 ++++- 3 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 changelog/13917.bugfix.rst diff --git a/changelog/13917.bugfix.rst b/changelog/13917.bugfix.rst new file mode 100644 index 00000000000..d2cf90c2894 --- /dev/null +++ b/changelog/13917.bugfix.rst @@ -0,0 +1 @@ +:class:`unittest.SkipTest` is no longer considered an interactive exception, i.e. :hook:`pytest_exception_interact` is no longer called for it. diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 75a37a867cb..2ee540cc996 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -294,10 +294,7 @@ def pytest_exception_interact( sys.stdout.write(out) sys.stdout.write(err) assert call.excinfo is not None - - unittest = sys.modules.get("unittest") - if unittest is None or not isinstance(call.excinfo.value, unittest.SkipTest): - _enter_pdb(node, call.excinfo, report) + _enter_pdb(node, call.excinfo, report) def pytest_internalerror(self, excinfo: ExceptionInfo[BaseException]) -> None: exc_or_tb = _postmortem_exc_or_tb(excinfo) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 9c20ff9e638..d1090aace89 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -271,7 +271,10 @@ def check_interactive_exception(call: CallInfo[object], report: BaseReport) -> b if hasattr(report, "wasxfail"): # Exception was expected. return False - if isinstance(call.excinfo.value, Skipped | bdb.BdbQuit): + unittest = sys.modules.get("unittest") + if isinstance(call.excinfo.value, Skipped | bdb.BdbQuit) or ( + unittest is not None and isinstance(call.excinfo.value, unittest.SkipTest) + ): # Special control flow exception. return False return True