From a4ea66cb1fcbb8a7723c764d67a817a0afdee36d Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 26 Jul 2017 17:18:29 +0200 Subject: [PATCH 1/2] pdb: resume capturing after `continue` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After `pdb.set_trace()` capturing is turned off. This patch resumes it after using the `continue` (or `c` / `cont`) command. Store _pytest_capman on the class, for pdbpp's do_debug hack to keep it. Without this, `debug …` would fail like this: /usr/lib/python3.6/cmd.py:217: in onecmd return func(arg) .venv/lib/python3.6/site-packages/pdb.py:608: in do_debug return orig_do_debug(self, arg) /usr/lib/python3.6/pdb.py:1099: in do_debug sys.call_tracing(p.run, (arg, globals, locals)) /usr/lib/python3.6/bdb.py:434: in run exec(cmd, globals, locals) /usr/lib/python3.6/bdb.py:51: in trace_dispatch return self.dispatch_line(frame) /usr/lib/python3.6/bdb.py:69: in dispatch_line self.user_line(frame) /usr/lib/python3.6/pdb.py:261: in user_line self.interaction(frame, None) .venv/lib/python3.6/site-packages/pdb.py:203: in interaction self.setup(frame, traceback) E AttributeError: 'PytestPdb' object has no attribute '_pytest_capman' - add pytest_leave_pdb hook - fixes test_pdb_interaction_capturing_twice: would fail on master now, but works here --- changelog/2619.feature.rst | 1 + src/_pytest/debugging.py | 38 +++++++++++++++++++++++++++++++++++++- src/_pytest/hookspec.py | 10 ++++++++++ testing/test_pdb.py | 28 ++++++++++++++++++++++------ 4 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 changelog/2619.feature.rst diff --git a/changelog/2619.feature.rst b/changelog/2619.feature.rst new file mode 100644 index 00000000000..df8137a669c --- /dev/null +++ b/changelog/2619.feature.rst @@ -0,0 +1 @@ +Resume capturing output after ``continue`` with ``__import__("pdb").set_trace()``. diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index cc9bf5c2a0f..da35688b9a7 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -96,8 +96,44 @@ def set_trace(cls, set_break=True): tw.line() tw.sep(">", "PDB set_trace (IO-capturing turned off)") cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config) + + class _PdbWrapper(cls._pdb_cls, object): + _pytest_capman = capman + _continued = False + + def do_continue(self, arg): + ret = super(_PdbWrapper, self).do_continue(arg) + if self._pytest_capman: + tw = _pytest.config.create_terminal_writer(cls._config) + tw.line() + tw.sep(">", "PDB continue (IO-capturing resumed)") + self._pytest_capman.resume_global_capture() + cls._pluginmanager.hook.pytest_leave_pdb(config=cls._config) + self._continued = True + return ret + + do_c = do_cont = do_continue + + def setup(self, f, tb): + """Suspend on setup(). + + Needed after do_continue resumed, and entering another + breakpoint again. + """ + ret = super(_PdbWrapper, self).setup(f, tb) + if not ret and self._continued: + # pdb.setup() returns True if the command wants to exit + # from the interaction: do not suspend capturing then. + if self._pytest_capman: + self._pytest_capman.suspend_global_capture(in_=True) + return ret + + _pdb = _PdbWrapper() + else: + _pdb = cls._pdb_cls() + if set_break: - cls._pdb_cls().set_trace(frame) + _pdb.set_trace(frame) class PdbInvoke(object): diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 533806964d5..ae289f0a3d0 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -609,3 +609,13 @@ def pytest_enter_pdb(config): :param _pytest.config.Config config: pytest config object """ + + +def pytest_leave_pdb(config): + """ called when leaving pdb (e.g. with continue after pdb.set_trace()). + + Can be used by plugins to take special action just after the python + debugger leaves interactive mode. + + :param _pytest.config.Config config: pytest config object + """ diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 57a6cb9a300..19f95959caa 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -158,6 +158,7 @@ def test_not_called_due_to_quit(): assert "= 1 failed in" in rest assert "def test_1" not in rest assert "Exit: Quitting debugger" in rest + assert "PDB continue (IO-capturing resumed)" not in rest self.flush(child) @staticmethod @@ -489,18 +490,23 @@ def test_1(): """ ) child = testdir.spawn_pytest(str(p1)) + child.expect(r"PDB set_trace \(IO-capturing turned off\)") child.expect("test_1") child.expect("x = 3") child.expect("Pdb") child.sendline("c") + child.expect(r"PDB continue \(IO-capturing resumed\)") + child.expect(r"PDB set_trace \(IO-capturing turned off\)") child.expect("x = 4") child.expect("Pdb") child.sendeof() + child.expect("_ test_1 _") + child.expect("def test_1") + child.expect("Captured stdout call") rest = child.read().decode("utf8") - assert "1 failed" in rest - assert "def test_1" in rest assert "hello17" in rest # out is captured assert "hello18" in rest # out is captured + assert "1 failed" in rest self.flush(child) def test_pdb_used_outside_test(self, testdir): @@ -541,15 +547,19 @@ def test_pdb_collection_failure_is_shown(self, testdir): ["E NameError: *xxx*", "*! *Exit: Quitting debugger !*"] # due to EOF ) - def test_enter_pdb_hook_is_called(self, testdir): + def test_enter_leave_pdb_hooks_are_called(self, testdir): testdir.makeconftest( """ + def pytest_configure(config): + config.testing_verification = 'configured' + def pytest_enter_pdb(config): assert config.testing_verification == 'configured' print('enter_pdb_hook') - def pytest_configure(config): - config.testing_verification = 'configured' + def pytest_leave_pdb(config): + assert config.testing_verification == 'configured' + print('leave_pdb_hook') """ ) p1 = testdir.makepyfile( @@ -558,11 +568,17 @@ def pytest_configure(config): def test_foo(): pytest.set_trace() + assert 0 """ ) child = testdir.spawn_pytest(str(p1)) child.expect("enter_pdb_hook") - child.send("c\n") + child.sendline("c") + child.expect(r"PDB continue \(IO-capturing resumed\)") + child.expect("Captured stdout call") + rest = child.read().decode("utf8") + assert "leave_pdb_hook" in rest + assert "1 failed" in rest child.sendeof() self.flush(child) From ede3a4e850e8d2d3dedbb90eca84bc80a6f6bc27 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 24 Oct 2018 23:27:14 +0200 Subject: [PATCH 2/2] pytest_{enter,leave}_pdb: pass through pdb instance --- changelog/2619.feature.rst | 3 +++ src/_pytest/debugging.py | 6 ++++-- src/_pytest/hookspec.py | 6 ++++-- testing/test_pdb.py | 14 ++++++++++++-- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/changelog/2619.feature.rst b/changelog/2619.feature.rst index df8137a669c..d2ce9c5ed38 100644 --- a/changelog/2619.feature.rst +++ b/changelog/2619.feature.rst @@ -1 +1,4 @@ Resume capturing output after ``continue`` with ``__import__("pdb").set_trace()``. + +This also adds a new ``pytest_leave_pdb`` hook, and passes in ``pdb`` to the +existing ``pytest_enter_pdb`` hook. diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index da35688b9a7..5542fef78fe 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -95,7 +95,6 @@ def set_trace(cls, set_break=True): tw = _pytest.config.create_terminal_writer(cls._config) tw.line() tw.sep(">", "PDB set_trace (IO-capturing turned off)") - cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config) class _PdbWrapper(cls._pdb_cls, object): _pytest_capman = capman @@ -108,7 +107,9 @@ def do_continue(self, arg): tw.line() tw.sep(">", "PDB continue (IO-capturing resumed)") self._pytest_capman.resume_global_capture() - cls._pluginmanager.hook.pytest_leave_pdb(config=cls._config) + cls._pluginmanager.hook.pytest_leave_pdb( + config=cls._config, pdb=self + ) self._continued = True return ret @@ -129,6 +130,7 @@ def setup(self, f, tb): return ret _pdb = _PdbWrapper() + cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config, pdb=_pdb) else: _pdb = cls._pdb_cls() diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index ae289f0a3d0..6e04557208b 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -603,19 +603,21 @@ def pytest_exception_interact(node, call, report): """ -def pytest_enter_pdb(config): +def pytest_enter_pdb(config, pdb): """ called upon pdb.set_trace(), can be used by plugins to take special action just before the python debugger enters in interactive mode. :param _pytest.config.Config config: pytest config object + :param pdb.Pdb pdb: Pdb instance """ -def pytest_leave_pdb(config): +def pytest_leave_pdb(config, pdb): """ called when leaving pdb (e.g. with continue after pdb.set_trace()). Can be used by plugins to take special action just after the python debugger leaves interactive mode. :param _pytest.config.Config config: pytest config object + :param pdb.Pdb pdb: Pdb instance """ diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 19f95959caa..3f0f744101e 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -550,16 +550,26 @@ def test_pdb_collection_failure_is_shown(self, testdir): def test_enter_leave_pdb_hooks_are_called(self, testdir): testdir.makeconftest( """ + mypdb = None + def pytest_configure(config): config.testing_verification = 'configured' - def pytest_enter_pdb(config): + def pytest_enter_pdb(config, pdb): assert config.testing_verification == 'configured' print('enter_pdb_hook') - def pytest_leave_pdb(config): + global mypdb + mypdb = pdb + mypdb.set_attribute = "bar" + + def pytest_leave_pdb(config, pdb): assert config.testing_verification == 'configured' print('leave_pdb_hook') + + global mypdb + assert mypdb is pdb + assert mypdb.set_attribute == "bar" """ ) p1 = testdir.makepyfile(