diff --git a/changelog/4855.feature.rst b/changelog/4855.feature.rst new file mode 100644 index 000000000..274d3991f --- /dev/null +++ b/changelog/4855.feature.rst @@ -0,0 +1,4 @@ +The ``--pdbcls`` option handles classes via module attributes now (e.g. +``pdb:pdb.Pdb`` with `pdb++`_), and its validation was improved. + +.. _pdb++: https://pypi.org/project/pdbpp/ diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 6b401aa0b..271a590a1 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -3,6 +3,7 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +import argparse import pdb import sys from doctest import UnexpectedException @@ -11,6 +12,31 @@ from _pytest import outcomes from _pytest.config import hookimpl +def _validate_usepdb_cls(value): + 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) + ) + + def pytest_addoption(parser): group = parser.getgroup("general") group._addoption( @@ -23,6 +49,7 @@ def pytest_addoption(parser): "--pdbcls", dest="usepdb_cls", metavar="modulename:classname", + type=_validate_usepdb_cls, help="start a custom interactive Python debugger on errors. " "For example: --pdbcls=IPython.terminal.debugger:TerminalPdb", ) @@ -35,11 +62,8 @@ def pytest_addoption(parser): def pytest_configure(config): - if config.getvalue("usepdb_cls"): - modname, classname = config.getvalue("usepdb_cls").split(":") - __import__(modname) - pdb_cls = getattr(sys.modules[modname], classname) - else: + pdb_cls = config.getvalue("usepdb_cls") + if not pdb_cls: pdb_cls = pdb.Pdb if config.getvalue("trace"): diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 43d640614..f9a050b10 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -2,12 +2,14 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +import argparse import os import platform import sys import _pytest._code import pytest +from _pytest.debugging import _validate_usepdb_cls try: breakpoint @@ -688,6 +690,23 @@ class TestPDB(object): result.stdout.fnmatch_lines(["*NameError*xxx*", "*1 error*"]) assert custom_pdb_calls == ["init", "reset", "interaction"] + def test_pdb_custom_cls_invalid(self, testdir): + result = testdir.runpytest_inprocess("--pdbcls=invalid") + result.stderr.fnmatch_lines( + [ + "*: error: argument --pdbcls: 'invalid' is not in the format 'modname:classname'" + ] + ) + + def test_pdb_validate_usepdb_cls(self, testdir): + assert _validate_usepdb_cls("os.path:dirname.__name__") == "dirname" + + with pytest.raises( + argparse.ArgumentTypeError, + match=r"^could not get pdb class for 'pdb:DoesNotExist': .*'DoesNotExist'", + ): + _validate_usepdb_cls("pdb:DoesNotExist") + def test_pdb_custom_cls_without_pdb(self, testdir, custom_pdb_calls): p1 = testdir.makepyfile("""xxx """) result = testdir.runpytest_inprocess("--pdbcls=_pytest:_CustomPdb", p1)