Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions changelog/4416.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pdb: support keyword arguments with ``pdb.set_trace``

It handles ``header`` similar to Python 3.7 does it, and forwards any
other keyword arguments to the ``Pdb`` constructor.

This allows for ``__import__("pdb").set_trace(skip=["foo.*"])``.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this changes the signature of pdb.set_trace() I don't think we should do this (we also have to manually adjust our code every time cpython adds a parameter to this function which seems non-ideal)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the changelog text is incorrect, it should talk about pytest.set_trace

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pytest monkeypatches pdb.set_trace:

import pdb
import pytest


def test():
    assert pdb.set_trace.__module__ == '_pytest.debugging'
$ pytest t.py -q
.                                                                        [100%]
1 passed in 0.00 seconds

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #4416 (comment).

We're not changing the signature, but just pass through args and kwargs.
pdb.set_trace looks like it might never handle args (*, header=None in py37), but just in case and/or if you want to hack something into this yourself.
This PR handles header added in py37, but in general (through the terminal writer), and forwards anything else to Pdb.__init__.
pdb++ makes use of this, and any custom pdbcls could do so then.

I do not see a compatibility issue really, but it makes it just a bit more forward-compatible - also a new kwarg (like "header") might cause an error then when Pdb.__init__ does not handle it.

For this, we could try it with **kwargs first, and then without them on TypeError, logging a warning.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@asottile right now, there is not a good way to provide those arguments to Pdb directly. If you could read into the original request #4416, I think it could help to give a better understanding on the context of the change.

Right now we are constrained to build the Pdb class in the predefined way, with no arguments. However with this patch we would be able to initalize Pdb, or any other implementation (I am keen on TerminalPdb from ipython, or pycharm's pdb class) with the custom arguments to support debugging asyncio.

I would highly encourage to try debugging asyncio code with pytest.set_trace() with no skip support, and you will find that all the control changes from the ioloop and back are almost impossible to follow due to all the await syntax.

This will ease a lot my development of fully asyncio libraries/applications, as many times you find bugs through pytest, and you need to debug them in that specific fixtured environment.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any news on this folks? @asottile do you still strongly object to this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would help to get @RonnyPfannschmidt's opinion on this as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd put myself at non blocking opposed go ahead!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh, I can see @RonnyPfannschmidt literally on some fence already.. ;)

I'm also a bit on @asottile's side here in general, but do not really see a good way around this. Maybe we could use inspect to have expected/proper TypeErrors?

Also, this removes the break kwarg that could previously have been used to do pdb.set_trace(break=False) - although it appears to come only from internal use without refactoring it properly (54d3cd5), i.e. an API break as it stands now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the thing that convinces me that this is alright is that an interactive call, so the user knows he is running its own debugger tracing function. If this was an API call I would also be against it, as you don't know what debugger the person using your API is.

But let's see @RonnyPfannschmidt's opinion, if he is also against this as @asottile is, then I'm afraid we will need to close this PR.

28 changes: 18 additions & 10 deletions src/_pytest/debugging.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,18 +77,21 @@ class pytestPDB(object):
_saved = []

@classmethod
def set_trace(cls, set_break=True):
""" invoke PDB set_trace debugging, dropping any IO capturing. """
def _init_pdb(cls, *args, **kwargs):
""" Initialize PDB debugging, dropping any IO capturing. """
import _pytest.config

frame = sys._getframe().f_back
if cls._pluginmanager is not None:
capman = cls._pluginmanager.getplugin("capturemanager")
if capman:
capman.suspend_global_capture(in_=True)
tw = _pytest.config.create_terminal_writer(cls._config)
tw.line()
if capman and capman.is_globally_capturing():
# Handle header similar to pdb.set_trace in py37+.
header = kwargs.pop("header", None)
if header is not None:
tw.sep(">", header)
elif capman and capman.is_globally_capturing():
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
else:
tw.sep(">", "PDB set_trace")
Expand Down Expand Up @@ -129,13 +132,18 @@ def setup(self, f, tb):
self._pytest_capman.suspend_global_capture(in_=True)
return ret

_pdb = _PdbWrapper()
_pdb = _PdbWrapper(**kwargs)
cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config, pdb=_pdb)
else:
_pdb = cls._pdb_cls()
_pdb = cls._pdb_cls(**kwargs)
return _pdb

if set_break:
_pdb.set_trace(frame)
@classmethod
def set_trace(cls, *args, **kwargs):
"""Invoke debugging via ``Pdb.set_trace``, dropping any IO capturing."""
frame = sys._getframe().f_back
_pdb = cls._init_pdb(*args, **kwargs)
_pdb.set_trace(frame)


class PdbInvoke(object):
Expand All @@ -161,9 +169,9 @@ def pytest_pyfunc_call(self, pyfuncitem):


def _test_pytest_function(pyfuncitem):
pytestPDB.set_trace(set_break=False)
_pdb = pytestPDB._init_pdb()
testfunction = pyfuncitem.obj
pyfuncitem.obj = pdb.runcall
pyfuncitem.obj = _pdb.runcall
if pyfuncitem._isyieldedfunction():
arg_list = list(pyfuncitem._args)
arg_list.insert(0, testfunction)
Expand Down
31 changes: 30 additions & 1 deletion testing/test_pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,28 @@ def test_1():
assert "hello17" in rest # out is captured
self.flush(child)

def test_pdb_set_trace_kwargs(self, testdir):
p1 = testdir.makepyfile(
"""
import pytest
def test_1():
i = 0
print("hello17")
pytest.set_trace(header="== my_header ==")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@asottile
This currently fails, i.e. we do not support py37's signature.

x = 3
"""
)
child = testdir.spawn_pytest(str(p1))
child.expect("== my_header ==")
assert "PDB set_trace" not in child.before.decode()
child.expect("Pdb")
child.sendeof()
rest = child.read().decode("utf-8")
assert "1 failed" in rest
assert "def test_1" in rest
assert "hello17" in rest # out is captured
self.flush(child)

def test_pdb_set_trace_interception(self, testdir):
p1 = testdir.makepyfile(
"""
Expand Down Expand Up @@ -634,6 +656,12 @@ def test_pdb_custom_cls_with_settrace(self, testdir, monkeypatch):
testdir.makepyfile(
custom_pdb="""
class CustomPdb(object):
def __init__(self, *args, **kwargs):
skip = kwargs.pop("skip")
assert skip == ["foo.*"]
print("__init__")
super(CustomPdb, self).__init__(*args, **kwargs)

def set_trace(*args, **kwargs):
print('custom set_trace>')
"""
Expand All @@ -643,12 +671,13 @@ def set_trace(*args, **kwargs):
import pytest

def test_foo():
pytest.set_trace()
pytest.set_trace(skip=['foo.*'])
"""
)
monkeypatch.setenv("PYTHONPATH", str(testdir.tmpdir))
child = testdir.spawn_pytest("--pdbcls=custom_pdb:CustomPdb %s" % str(p1))

child.expect("__init__")
child.expect("custom set_trace>")
self.flush(child)

Expand Down