diff --git a/changelog/5039.bugfix.rst b/changelog/5039.bugfix.rst new file mode 100644 index 00000000000..4e173f64e96 --- /dev/null +++ b/changelog/5039.bugfix.rst @@ -0,0 +1 @@ +Fix regression with ``--pdbcls``, which stopped working with local modules in 4.0.0. diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index cb1c964c35b..3bfd5465fb2 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -10,31 +10,18 @@ from _pytest import outcomes from _pytest.config import hookimpl +from _pytest.config.exceptions import UsageError def _validate_usepdb_cls(value): + """Validate syntax of --pdbcls option.""" try: modname, classname = value.split(":") except ValueError: raise argparse.ArgumentTypeError( "{!r} is not in the format 'modname:classname'".format(value) ) - - try: - __import__(modname) - mod = sys.modules[modname] - - # Handle --pdbcls=pdb:pdb.Pdb (useful e.g. with pdbpp). - parts = classname.split(".") - pdb_cls = getattr(mod, parts[0]) - for part in parts[1:]: - pdb_cls = getattr(pdb_cls, part) - - return pdb_cls - except Exception as exc: - raise argparse.ArgumentTypeError( - "could not get pdb class for {!r}: {}".format(value, exc) - ) + return (modname, classname) def pytest_addoption(parser): @@ -61,9 +48,28 @@ def pytest_addoption(parser): ) +def _import_pdbcls(modname, classname): + try: + __import__(modname) + mod = sys.modules[modname] + + # Handle --pdbcls=pdb:pdb.Pdb (useful e.g. with pdbpp). + parts = classname.split(".") + pdb_cls = getattr(mod, parts[0]) + for part in parts[1:]: + pdb_cls = getattr(pdb_cls, part) + + return pdb_cls + except Exception as exc: + value = ":".join((modname, classname)) + raise UsageError("--pdbcls: could not import {!r}: {}".format(value, exc)) + + def pytest_configure(config): pdb_cls = config.getvalue("usepdb_cls") - if not pdb_cls: + if pdb_cls: + pdb_cls = _import_pdbcls(*pdb_cls) + else: pdb_cls = pdb.Pdb if config.getvalue("trace"): diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 531846e8e13..61f33950de9 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -2,7 +2,6 @@ from __future__ import division from __future__ import print_function -import argparse import os import platform import sys @@ -803,13 +802,12 @@ def test_pdb_custom_cls_invalid(self, testdir): ) def test_pdb_validate_usepdb_cls(self, testdir): - assert _validate_usepdb_cls("os.path:dirname.__name__") == "dirname" + assert _validate_usepdb_cls("os.path:dirname.__name__") == ( + "os.path", + "dirname.__name__", + ) - with pytest.raises( - argparse.ArgumentTypeError, - match=r"^could not get pdb class for 'pdb:DoesNotExist': .*'DoesNotExist'", - ): - _validate_usepdb_cls("pdb:DoesNotExist") + assert _validate_usepdb_cls("pdb:DoesNotExist") == ("pdb", "DoesNotExist") def test_pdb_custom_cls_without_pdb(self, testdir, custom_pdb_calls): p1 = testdir.makepyfile("""xxx """) @@ -1121,3 +1119,35 @@ def test_inner({fixture}): assert child.exitstatus == 0 assert "= 1 passed in " in rest assert "> PDB continue (IO-capturing resumed for fixture %s) >" % (fixture) in rest + + +def test_pdbcls_via_local_module(testdir): + """It should be imported in pytest_configure or later only.""" + p1 = testdir.makepyfile( + """ + def test(): + print("before_settrace") + __import__("pdb").set_trace() + """, + mypdb=""" + class Wrapped: + class MyPdb: + def set_trace(self, *args): + print("mypdb_called", args) + """, + ) + result = testdir.runpytest( + str(p1), "--pdbcls=really.invalid:Value", syspathinsert=True + ) + result.stderr.fnmatch_lines( + [ + "ERROR: --pdbcls: could not import 'really.invalid:Value': No module named *really*" + ] + ) + assert result.ret == 4 + + result = testdir.runpytest( + str(p1), "--pdbcls=mypdb:Wrapped.MyPdb", syspathinsert=True + ) + assert result.ret == 0 + result.stdout.fnmatch_lines(["*mypdb_called*", "* 1 passed in *"])