diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a802bb5..c7c7be8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ releases are available on [PyPI](https://pypi.org/project/pytask) and - [#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/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..f03851fd 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 @@ -70,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): 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],