From e61e81a7b53496ea53f1f9958a2be5468190b8ef Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 1 Nov 2018 19:40:38 +0100 Subject: [PATCH] Make debugging's pytest_configure re-entrant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is relevant when using runpytest in-process. Fixes: E def test_1(testdir): E testdir.runpytest() E > __import__('pdb').set_trace() E E ../../test_trace_after_runpytest.py:3: E …/Vcs/pytest/src/_pytest/debugging.py:81: in set_trace E tw = _pytest.config.create_terminal_writer(cls._config) E E config = None, args = (), kwargs = {}, tw = E E def create_terminal_writer(config, *args, **kwargs): E """Create a TerminalWriter instance configured according to the options E in the config object. Every code which requires a TerminalWriter object E and has access to a config object should use this function. E """ E tw = py.io.TerminalWriter(*args, **kwargs) E > if config.option.color == "yes": E E AttributeError: 'NoneType' object has no attribute 'option' --- changelog/4287.bugfix.rst | 1 + src/_pytest/debugging.py | 22 +++++++++++++++------- testing/test_pdb.py | 27 +++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 7 deletions(-) create mode 100644 changelog/4287.bugfix.rst diff --git a/changelog/4287.bugfix.rst b/changelog/4287.bugfix.rst new file mode 100644 index 000000000..5ba5fbb7a --- /dev/null +++ b/changelog/4287.bugfix.rst @@ -0,0 +1 @@ +Fix nested usage of debugging plugin (pdb), e.g. with pytester's ``testdir.runpytest``. diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 5a9729d5b..0baa84afa 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -47,17 +47,24 @@ def pytest_configure(config): if config.getvalue("usepdb"): config.pluginmanager.register(PdbInvoke(), "pdbinvoke") - old = (pdb.set_trace, pytestPDB._pluginmanager) - - def fin(): - pdb.set_trace, pytestPDB._pluginmanager = old - pytestPDB._config = None - pytestPDB._pdb_cls = pdb.Pdb - + pytestPDB._saved.append( + (pdb.set_trace, pytestPDB._pluginmanager, pytestPDB._config, pytestPDB._pdb_cls) + ) pdb.set_trace = pytestPDB.set_trace pytestPDB._pluginmanager = config.pluginmanager pytestPDB._config = config pytestPDB._pdb_cls = pdb_cls + + # NOTE: not using pytest_unconfigure, since it might get called although + # pytest_configure was not (if another plugin raises UsageError). + def fin(): + ( + pdb.set_trace, + pytestPDB._pluginmanager, + pytestPDB._config, + pytestPDB._pdb_cls, + ) = pytestPDB._saved.pop() + config._cleanup.append(fin) @@ -67,6 +74,7 @@ class pytestPDB(object): _pluginmanager = None _config = None _pdb_cls = pdb.Pdb + _saved = [] @classmethod def set_trace(cls, set_break=True): diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 56fe5fc7a..ec1af5b56 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -784,3 +784,30 @@ class TestTraceOption: assert "1 passed" in rest assert "reading from stdin while output" not in rest TestPDB.flush(child) + + +def test_trace_after_runpytest(testdir): + """Test that debugging's pytest_configure is re-entrant.""" + p1 = testdir.makepyfile( + """ + from _pytest.debugging import pytestPDB + + def test_outer(testdir): + from _pytest.debugging import pytestPDB + + assert len(pytestPDB._saved) == 1 + + testdir.runpytest("-k test_inner") + + __import__('pdb').set_trace() + + def test_inner(testdir): + assert len(pytestPDB._saved) == 2 + """ + ) + child = testdir.spawn_pytest("-p pytester %s -k test_outer" % p1) + child.expect(r"\(Pdb") + child.sendline("c") + rest = child.read().decode("utf8") + TestPDB.flush(child) + assert child.exitstatus == 0, rest