Skip to content
21 changes: 17 additions & 4 deletions Lib/bdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,12 @@ def dispatch_line(self, frame):
self.user_line(). Raise BdbQuit if self.quitting is set.
Return self.trace_dispatch to continue tracing in this scope.
"""
if self.stop_here(frame) or self.break_here(frame):
# GH-136057
# For line events, we don't want to stop at the same line where
# we issue the previous next/step command.
Copy link
Member

Choose a reason for hiding this comment

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

If we stop, it's not necessarily due to a next/step, right? Could it also be a breakpoint or until, etc?

So maybe the comment can say:

If we are stopping due to a next/step command, we don't want to stop on the same line on which
the command was issued.

Copy link
Member Author

Choose a reason for hiding this comment

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

So I'm thinking. If we are currently at line 10, and the user does a until 10, should we stop at the current line? And if we stopped by a breakpoint, then we do continue, should we stop at the same line? Those are kind of undefined behaviors, unlike step and next which explicitly said that the debugger should stop at the "next" line.

if (self.stop_here(frame) or self.break_here(frame)) and not (
self.cmdframe == frame and self.cmdlineno == frame.f_lineno
):
self.user_line(frame)
self.restart_events()
if self.quitting: raise BdbQuit
Expand Down Expand Up @@ -535,7 +540,8 @@ def _set_trace_opcodes(self, trace_opcodes):
if self.monitoring_tracer:
self.monitoring_tracer.update_local_events()

def _set_stopinfo(self, stopframe, returnframe, stoplineno=0, opcode=False):
def _set_stopinfo(self, stopframe, returnframe, stoplineno=0, opcode=False,
cmdframe=None, cmdlineno=None):
"""Set the attributes for stopping.

If stoplineno is greater than or equal to 0, then stop at line
Expand All @@ -548,6 +554,11 @@ def _set_stopinfo(self, stopframe, returnframe, stoplineno=0, opcode=False):
# stoplineno >= 0 means: stop at line >= the stoplineno
# stoplineno -1 means: don't stop at all
self.stoplineno = stoplineno
# cmdframe/cmdlineno is the frame/line number when the user issues
# step/next commands. We don't want to stop at the same line for
# those commands.
self.cmdframe = cmdframe
self.cmdlineno = cmdlineno
self._set_trace_opcodes(opcode)

def _set_caller_tracefunc(self, current_frame):
Expand All @@ -573,15 +584,17 @@ def set_until(self, frame, lineno=None):

def set_step(self):
"""Stop after one line of code."""
self._set_stopinfo(None, None)
# set_step() could be called from signal handler so enterframe might be None
self._set_stopinfo(None, None, cmdframe=self.enterframe,
cmdlineno=getattr(self.enterframe, 'f_lineno', None))

def set_stepinstr(self):
"""Stop before the next instruction."""
self._set_stopinfo(None, None, opcode=True)

def set_next(self, frame):
"""Stop on the next line in or below the given frame."""
self._set_stopinfo(frame, None)
self._set_stopinfo(frame, None, cmdframe=frame, cmdlineno=frame.f_lineno)

def set_return(self, frame):
"""Stop when returning from the given frame."""
Expand Down
31 changes: 31 additions & 0 deletions Lib/test/test_pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -3232,6 +3232,37 @@ def test_pdb_issue_gh_127321():
"""


def test_pdb_issue_gh_136057():
"""See GH-136057
"step" and "next" commands should be able to get over list comprehensions
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't it be that next steps over the list comp but step executes every iteration?

Copy link
Member Author

Choose a reason for hiding this comment

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

Not according to our documentation

Execute the current line, stop at the first possible occasion (either in a function that is called or on the next line in the current function).

I mean we can have different interpretation of the phrase "first possible occasion". Technically we can do this at bytecode level - but that's probably not we really want. I think we should stick to the explanation in the parenthesis.

>>> def test_function():
... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace()
... lst = [i for i in range(10)]
... for i in lst: pass

>>> with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE
... 'next',
... 'next',
... 'step',
... 'continue',
... ]):
... test_function()
> <doctest test.test_pdb.test_pdb_issue_gh_136057[0]>(2)test_function()
-> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace()
(Pdb) next
> <doctest test.test_pdb.test_pdb_issue_gh_136057[0]>(3)test_function()
-> lst = [i for i in range(10)]
(Pdb) next
> <doctest test.test_pdb.test_pdb_issue_gh_136057[0]>(4)test_function()
-> for i in lst: pass
(Pdb) step
--Return--
> <doctest test.test_pdb.test_pdb_issue_gh_136057[0]>(4)test_function()->None
-> for i in lst: pass
(Pdb) continue
"""


def test_pdb_issue_gh_80731():
"""See GH-80731

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed the bug in :mod:`pdb` and :mod:`bdb` where ``next`` and ``step`` can't go over the line if a loop exists in the line.
Loading