Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-124552 : Improve the accuracy of possible breakpoint check in bdb #124553

Merged
merged 6 commits into from
Oct 5, 2024
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
27 changes: 24 additions & 3 deletions Lib/bdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import fnmatch
import sys
import os
import weakref
from inspect import CO_GENERATOR, CO_COROUTINE, CO_ASYNC_GENERATOR

__all__ = ["BdbQuit", "Bdb", "Breakpoint"]
Expand Down Expand Up @@ -36,6 +37,7 @@ def __init__(self, skip=None):
self.frame_returning = None
self.trace_opcodes = False
self.enterframe = None
self.code_linenos = weakref.WeakKeyDictionary()

self._load_breaks()

Expand Down Expand Up @@ -155,6 +157,9 @@ def dispatch_return(self, frame, arg):
if self.stop_here(frame) or frame == self.returnframe:
# Ignore return events in generator except when stepping.
if self.stopframe and frame.f_code.co_flags & GENERATOR_AND_COROUTINE_FLAGS:
# It's possible to trigger a StopIteration exception in
# the caller so we must set the trace function in the caller
self._set_caller_tracefunc(frame)
Copy link
Member

Choose a reason for hiding this comment

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

how is this related to this PR?

Copy link
Member Author

Choose a reason for hiding this comment

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

There is a hidden bug which was exposed by the change of break_anywhere. There's a test case of bdb testing raising StopIteration in a generator. It passed because break_anywhere always return True when the function was defined in the same file, which always sets the trace function on the caller because it is in the same file. That coincidence hide the bug where returning from a generator should stop in the caller. There's a similar call in line 177 - that's for the normal return case from #118979 - it's also my fix to a similar case.

return self.trace_dispatch
try:
self.frame_returning = frame
Expand Down Expand Up @@ -273,9 +278,25 @@ def do_clear(self, arg):
raise NotImplementedError("subclass of bdb must implement do_clear()")

def break_anywhere(self, frame):
"""Return True if there is any breakpoint for frame's filename.
"""Return True if there is any breakpoint in that frame
"""
return self.canonic(frame.f_code.co_filename) in self.breaks
filename = self.canonic(frame.f_code.co_filename)
if filename not in self.breaks:
return False
for lineno in self.breaks[filename]:
if self._lineno_in_frame(lineno, frame):
return True
return False

def _lineno_in_frame(self, lineno, frame):
"""Return True if the line number is in the frame's code object.
"""
code = frame.f_code
if lineno < code.co_firstlineno:
return False
if code not in self.code_linenos:
self.code_linenos[code] = set(lineno for _, _, lineno in code.co_lines())
return lineno in self.code_linenos[code]

# Derived classes should override the user_* methods
# to gain control.
Expand Down Expand Up @@ -360,7 +381,7 @@ def set_next(self, frame):
def set_return(self, frame):
"""Stop when returning from the given frame."""
if frame.f_code.co_flags & GENERATOR_AND_COROUTINE_FLAGS:
self._set_stopinfo(frame, None, -1)
self._set_stopinfo(frame, frame, -1)
else:
self._set_stopinfo(frame.f_back, frame)

Expand Down
37 changes: 37 additions & 0 deletions Lib/test/test_pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,43 @@ def test_pdb_breakpoints_preserved_across_interactive_sessions():
(Pdb) continue
"""

def test_pdb_break_anywhere():
"""Test break_anywhere() method of Pdb.

>>> def outer():
... def inner():
... import pdb
... import sys
... p = pdb.Pdb(nosigint=True, readrc=False)
... p.set_trace()
... frame = sys._getframe()
... print(p.break_anywhere(frame)) # inner
... print(p.break_anywhere(frame.f_back)) # outer
... print(p.break_anywhere(frame.f_back.f_back)) # caller
... inner()

>>> def caller():
... outer()

>>> def test_function():
... caller()

>>> reset_Breakpoint()
>>> with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE
... 'b 3',
... 'c',
... ]):
... test_function()
> <doctest test.test_pdb.test_pdb_break_anywhere[0]>(6)inner()
-> p.set_trace()
(Pdb) b 3
Breakpoint 1 at <doctest test.test_pdb.test_pdb_break_anywhere[0]>:3
(Pdb) c
True
False
False
"""

def test_pdb_pp_repr_exc():
"""Test that do_p/do_pp do not swallow exceptions.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Improve the accuracy of :mod:`bdb`'s check for the possibility of breakpoint in a frame. This makes it possible to disable unnecessary events in functions.
Loading