From b311a24fb92c2d1db683503d897e0489887d2647 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Fri, 28 Aug 2026 23:08:35 +0200 Subject: [PATCH 1/3] refactor: use importlib for dynamic imports --- CHANGELOG.md | 2 ++ src/_pytask/debugging.py | 4 ++-- src/_pytask/warnings_utils.py | 3 ++- tests/test_debugging.py | 26 ++++++++++++++++++++++++++ tests/test_warnings.py | 21 +++++++++++++++++++++ 5 files changed, 53 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a802bb5..ce3c149c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ releases are available on [PyPI](https://pypi.org/project/pytask) and ## Unreleased +- Uses `importlib.import_module` for dynamic imports of custom PDB classes and + warning categories. - [#970](https://github.com/pytask-dev/pytask/pull/970) prevents tee-sys capture from replaying output that was already displayed when capture stops before its buffer is read. diff --git a/src/_pytask/debugging.py b/src/_pytask/debugging.py index 1484584e..6a2e3771 100644 --- a/src/_pytask/debugging.py +++ b/src/_pytask/debugging.py @@ -7,6 +7,7 @@ from __future__ import annotations import functools +import importlib import pdb # noqa: T100 import sys from typing import TYPE_CHECKING @@ -173,8 +174,7 @@ def _import_pdb_cls( modname, classname = usepdb_cls try: - __import__(modname) - mod = sys.modules[modname] + mod = importlib.import_module(modname) # Handle --pdbcls=pdb:pdb.Pdb (useful e.g. with pdbpp or pdbp). parts = classname.split(".") diff --git a/src/_pytask/warnings_utils.py b/src/_pytask/warnings_utils.py index 5970596b..7be47007 100644 --- a/src/_pytask/warnings_utils.py +++ b/src/_pytask/warnings_utils.py @@ -3,6 +3,7 @@ from __future__ import annotations import functools +import importlib import re import textwrap import warnings @@ -124,7 +125,7 @@ def _resolve_warning_category(category: str) -> type[Warning]: klass = category else: module, _, klass = category.rpartition(".") - m = __import__(module, None, None, [klass]) + m = importlib.import_module(module) cat = getattr(m, klass) if not issubclass(cat, Warning): msg = f"{cat} is not a Warning subclass" diff --git a/tests/test_debugging.py b/tests/test_debugging.py index b36f0366..804a4145 100644 --- a/tests/test_debugging.py +++ b/tests/test_debugging.py @@ -1,14 +1,17 @@ from __future__ import annotations import os +import pdb # noqa: T100 import re import sys import textwrap from contextlib import ExitStack as does_not_raise # noqa: N813 +from types import SimpleNamespace import click import pytest +from _pytask.debugging import PytaskPDB from _pytask.debugging import _pdbcls_callback from pytask import ExitCode from pytask import cli @@ -44,6 +47,29 @@ def test_capture_callback(value, expected, expectation): assert result == expected +def test_import_pdb_cls_uses_import_module(monkeypatch): + class CustomPdb(pdb.Pdb): + pass + + module = SimpleNamespace(CustomPdb=CustomPdb) + imported = [] + + def import_module(name): + imported.append(name) + return module + + monkeypatch.setattr("importlib.import_module", import_module) + monkeypatch.setattr( + PytaskPDB, "_config", {"pdbcls": ("package.debuggers", "CustomPdb")} + ) + monkeypatch.setattr(PytaskPDB, "_wrapped_pdb_cls", None) + + wrapped = PytaskPDB._import_pdb_cls(None, None) # type: ignore[arg-type] + + assert imported == ["package.debuggers"] + assert issubclass(wrapped, CustomPdb) + + def _flush(child): if child.isalive(): child.read() diff --git a/tests/test_warnings.py b/tests/test_warnings.py index 6e551507..9ccea707 100644 --- a/tests/test_warnings.py +++ b/tests/test_warnings.py @@ -2,15 +2,36 @@ import sys import textwrap +from types import SimpleNamespace import pytest +from _pytask.warnings_utils import _resolve_warning_category from pytask import ExitCode from pytask import build from pytask import cli from tests.conftest import run_in_subprocess +def test_resolve_warning_category_uses_import_module(monkeypatch): + class CustomWarning(Warning): + pass + + module = SimpleNamespace(CustomWarning=CustomWarning) + imported = [] + + def import_module(name): + imported.append(name) + return module + + monkeypatch.setattr("importlib.import_module", import_module) + + result = _resolve_warning_category("package.warnings.CustomWarning") + + assert imported == ["package.warnings"] + assert result is CustomWarning + + @pytest.mark.parametrize( "disable_warnings", [pytest.param(True, marks=pytest.mark.filterwarnings("ignore:warning!!!")), False], From feff55e0e5124cbe02a2cd99d224e9d9b5bd6b16 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Fri, 28 Aug 2026 23:09:31 +0200 Subject: [PATCH 2/3] docs: link dynamic imports changelog entry --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce3c149c..e336c27b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,9 @@ releases are available on [PyPI](https://pypi.org/project/pytask) and ## Unreleased -- Uses `importlib.import_module` for dynamic imports of custom PDB classes and - warning categories. +- [#972](https://github.com/pytask-dev/pytask/pull/972) uses + `importlib.import_module` for dynamic imports of custom PDB classes and warning + categories. - [#970](https://github.com/pytask-dev/pytask/pull/970) prevents tee-sys capture from replaying output that was already displayed when capture stops before its buffer is read. From b12a003b5a750938eead93e0916ddc817bb7c5bd Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Fri, 28 Aug 2026 23:11:30 +0200 Subject: [PATCH 3/3] chore: avoid overlap with pdb help change --- CHANGELOG.md | 6 +++--- tests/test_debugging.py | 46 ++++++++++++++++++++--------------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e336c27b..c7c7be8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,12 @@ releases are available on [PyPI](https://pypi.org/project/pytask) and ## Unreleased -- [#972](https://github.com/pytask-dev/pytask/pull/972) uses - `importlib.import_module` for dynamic imports of custom PDB classes and warning - categories. - [#970](https://github.com/pytask-dev/pytask/pull/970) prevents tee-sys capture from replaying output that was already displayed when capture stops before its buffer is read. +- [#972](https://github.com/pytask-dev/pytask/pull/972) uses + `importlib.import_module` for dynamic imports of custom PDB classes and warning + categories. - [#968](https://github.com/pytask-dev/pytask/pull/968) improves the performance of file descriptor capture for tasks that produce no output. - Documents the pytest provenance of the adapted debugging, marker, and warning diff --git a/tests/test_debugging.py b/tests/test_debugging.py index 804a4145..f03851fd 100644 --- a/tests/test_debugging.py +++ b/tests/test_debugging.py @@ -47,29 +47,6 @@ def test_capture_callback(value, expected, expectation): assert result == expected -def test_import_pdb_cls_uses_import_module(monkeypatch): - class CustomPdb(pdb.Pdb): - pass - - module = SimpleNamespace(CustomPdb=CustomPdb) - imported = [] - - def import_module(name): - imported.append(name) - return module - - monkeypatch.setattr("importlib.import_module", import_module) - monkeypatch.setattr( - PytaskPDB, "_config", {"pdbcls": ("package.debuggers", "CustomPdb")} - ) - monkeypatch.setattr(PytaskPDB, "_wrapped_pdb_cls", None) - - wrapped = PytaskPDB._import_pdb_cls(None, None) # type: ignore[arg-type] - - assert imported == ["package.debuggers"] - assert issubclass(wrapped, CustomPdb) - - def _flush(child): if child.isalive(): child.read() @@ -96,6 +73,29 @@ def task_example(): _flush(child) +def test_import_pdb_cls_uses_import_module(monkeypatch): + class CustomPdb(pdb.Pdb): + pass + + module = SimpleNamespace(CustomPdb=CustomPdb) + imported = [] + + def import_module(name): + imported.append(name) + return module + + monkeypatch.setattr("importlib.import_module", import_module) + monkeypatch.setattr( + PytaskPDB, "_config", {"pdbcls": ("package.debuggers", "CustomPdb")} + ) + monkeypatch.setattr(PytaskPDB, "_wrapped_pdb_cls", None) + + wrapped = PytaskPDB._import_pdb_cls(None, None) # type: ignore[arg-type] + + assert imported == ["package.debuggers"] + assert issubclass(wrapped, CustomPdb) + + @pytest.mark.skipif(not IS_PEXPECT_INSTALLED, reason="pexpect is not installed.") @pytest.mark.skipif(sys.platform == "win32", reason="pexpect cannot spawn on Windows.") def test_post_mortem_on_error_w_kwargs(tmp_path):