Skip to content

gh-79871: IDLE - Fix and test debugger module #11451

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

Merged
merged 51 commits into from
Nov 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
bbb58e8
start setting up tests for the Idb module
tonybaloney Jan 6, 2019
525efbc
handle NoneType in frame.f_back (first frame in session) and call bdb…
tonybaloney Jan 6, 2019
83aeefa
add a test for Idb to ensure calling super. Add a test to check the h…
tonybaloney Jan 6, 2019
d01dcdb
add test for user_exception to validate passing of exc_info to gui in…
tonybaloney Jan 6, 2019
6a123ef
add tests for in_rpc_code to catch various frame file names
tonybaloney Jan 6, 2019
e5e882c
add some PEP8 compliant line breaks
tonybaloney Jan 6, 2019
35915da
setup tests for the debugger.Debugger class
tonybaloney Jan 7, 2019
dbb5c7e
add tests for run with and without an idb instance set on init
tonybaloney Jan 7, 2019
2929019
add tests for interaction, which are currently demonstrating a bug in…
tonybaloney Jan 7, 2019
bbe4300
Bdb expects an instance attribute called 'botframe', which is set by …
tonybaloney Jan 7, 2019
4957496
remove empty single line comments
tonybaloney Jan 7, 2019
fbf1cac
add tests for the basic operations
tonybaloney Jan 7, 2019
2b42337
finished tests for Debugger
tonybaloney Jan 7, 2019
56741f1
add a constructor for MockFrameType to condense the code
tonybaloney Jan 7, 2019
8d015ce
add tests for show_source and load_stack for the StackViewer class
tonybaloney Jan 7, 2019
4807556
remove line comment
tonybaloney Jan 7, 2019
88e5f91
📜🤖 Added by blurb_it.
blurb-it[bot] Jan 7, 2019
0df2512
fixed references to .assertEquals, moved code into setup and teardown…
tonybaloney Jan 10, 2019
dfe6db9
cleanupm stackviewer tests and use Tk instance
tonybaloney Jan 10, 2019
2b9a43a
untoggle the gui requirement
tonybaloney Jan 10, 2019
5ae4060
Change the single-line docstrings to comments
tonybaloney Apr 4, 2019
14b908f
Removed the 2 interaction tests because they cause the UI to wait for…
tonybaloney Apr 5, 2019
ae491f4
Fix the test assertions and use stacked frames when testing debugging…
tonybaloney Apr 5, 2019
bc28ced
Make the test vaguer so that if the coverage checks pass it doesn't i…
tonybaloney Apr 5, 2019
9333fcf
Clean up attributes in memory and split tests between those which nee…
tonybaloney Apr 5, 2019
046fe00
Merge branch 'main' into idlelib_tests
terryjreedy Nov 14, 2023
daa2015
Update Misc/NEWS.d/next/IDLE/2019-01-07-06-18-25.bpo-35668.JimxP5.rst
terryjreedy Nov 14, 2023
37afb53
Update 2019-01-07-06-18-25.bpo-35668.JimxP5.rst
terryjreedy Nov 14, 2023
f4c3c00
Merge remote-tracking branch 'upstream/main' into pr_11451
terryjreedy Nov 15, 2023
e9d0e4e
Add comment to stackviewer.
terryjreedy Nov 15, 2023
3b97b55
Add debugger module docstrings and revise those for Idb.
terryjreedy Nov 15, 2023
d989bfa
Fix test failures by using 'assert_called_once_with'.
terryjreedy Nov 15, 2023
22e8249
Remove duplicate test.
terryjreedy Nov 15, 2023
2611447
Merge remote-tracking branch 'upstream/main' into pr_11451
terryjreedy Nov 15, 2023
f9f1a22
Remove added 'botframe' initialization as very likely not needed.
terryjreedy Nov 16, 2023
a202f4f
Move function frame2message to module level.
terryjreedy Nov 16, 2023
597cbba
IDLE style is no newline to start docstring.
terryjreedy Nov 16, 2023
0a53cd1
Change 'doc string' to 'Doc string.'
terryjreedy Nov 16, 2023
114d686
Remove unneeded Ibd setups and teardowns.
terryjreedy Nov 16, 2023
1c1107d
Polish IdbTest.
terryjreedy Nov 16, 2023
ed58072
Make in_rpc_code a module function, like frame2message.
terryjreedy Nov 16, 2023
d541302
Add FunctionTest
terryjreedy Nov 16, 2023
df1936f
Refactor IdbTest.
terryjreedy Nov 16, 2023
fedfbc0
FunctionTest comment
terryjreedy Nov 16, 2023
a731e63
Use False, True for Debugger().interacting.
terryjreedy Nov 16, 2023
9eb6548
Modify Debugger tests
terryjreedy Nov 16, 2023
0454ea5
Import Mock and patch, refactor to run many tests without gui.
terryjreedy Nov 16, 2023
63008a5
Put no-gui tests first, fix breakpoints, work on gui tests.
terryjreedy Nov 19, 2023
fe14bb8
Tests pass.
terryjreedy Nov 19, 2023
fd7e259
Merge branch 'main' into idlelib_tests
terryjreedy Nov 19, 2023
5fe9868
ws
terryjreedy Nov 19, 2023
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
179 changes: 115 additions & 64 deletions Lib/idlelib/debugger.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
"""Debug user code with a GUI interface to a subclass of bdb.Bdb.

The Idb idb and Debugger gui instances each need a reference to each
other or to an rpc proxy for each other.

If IDLE is started with '-n', so that user code and idb both run in the
IDLE process, Debugger is called without an idb. Debugger.__init__
calls Idb with its incomplete self. Idb.__init__ stores gui and gui
then stores idb.

If IDLE is started normally, so that user code executes in a separate
process, debugger_r.start_remote_debugger is called, executing in the
IDLE process. It calls 'start the debugger' in the remote process,
which calls Idb with a gui proxy. Then Debugger is called in the IDLE
for more.
"""

import bdb
import os

Expand All @@ -10,66 +27,95 @@


class Idb(bdb.Bdb):
"Supply user_line and user_exception functions for Bdb."

def __init__(self, gui):
self.gui = gui # An instance of Debugger or proxy of remote.
bdb.Bdb.__init__(self)
self.gui = gui # An instance of Debugger or proxy thereof.
super().__init__()

def user_line(self, frame):
if self.in_rpc_code(frame):
"""Handle a user stopping or breaking at a line.

Convert frame to a string and send it to gui.
"""
if _in_rpc_code(frame):
self.set_step()
return
message = self.__frame2message(frame)
message = _frame2message(frame)
try:
self.gui.interaction(message, frame)
except TclError: # When closing debugger window with [x] in 3.x
pass

def user_exception(self, frame, info):
if self.in_rpc_code(frame):
def user_exception(self, frame, exc_info):
"""Handle an the occurrence of an exception."""
if _in_rpc_code(frame):
self.set_step()
return
message = self.__frame2message(frame)
self.gui.interaction(message, frame, info)

def in_rpc_code(self, frame):
if frame.f_code.co_filename.count('rpc.py'):
return True
else:
prev_frame = frame.f_back
prev_name = prev_frame.f_code.co_filename
if 'idlelib' in prev_name and 'debugger' in prev_name:
# catch both idlelib/debugger.py and idlelib/debugger_r.py
# on both Posix and Windows
return False
return self.in_rpc_code(prev_frame)

def __frame2message(self, frame):
code = frame.f_code
filename = code.co_filename
lineno = frame.f_lineno
basename = os.path.basename(filename)
message = f"{basename}:{lineno}"
if code.co_name != "?":
message = f"{message}: {code.co_name}()"
return message
message = _frame2message(frame)
self.gui.interaction(message, frame, exc_info)

def _in_rpc_code(frame):
"Determine if debugger is within RPC code."
if frame.f_code.co_filename.count('rpc.py'):
return True # Skip this frame.
else:
prev_frame = frame.f_back
if prev_frame is None:
return False
prev_name = prev_frame.f_code.co_filename
if 'idlelib' in prev_name and 'debugger' in prev_name:
# catch both idlelib/debugger.py and idlelib/debugger_r.py
# on both Posix and Windows
return False
return _in_rpc_code(prev_frame)

def _frame2message(frame):
"""Return a message string for frame."""
code = frame.f_code
filename = code.co_filename
lineno = frame.f_lineno
basename = os.path.basename(filename)
message = f"{basename}:{lineno}"
if code.co_name != "?":
message = f"{message}: {code.co_name}()"
return message


class Debugger:

vstack = vsource = vlocals = vglobals = None
"""The debugger interface.

This class handles the drawing of the debugger window and
the interactions with the underlying debugger session.
"""
vstack = None
vsource = None
vlocals = None
vglobals = None
stackviewer = None
localsviewer = None
globalsviewer = None

def __init__(self, pyshell, idb=None):
"""Instantiate and draw a debugger window.

:param pyshell: An instance of the PyShell Window
:type pyshell: :class:`idlelib.pyshell.PyShell`

:param idb: An instance of the IDLE debugger (optional)
:type idb: :class:`idlelib.debugger.Idb`
"""
if idb is None:
idb = Idb(self)
self.pyshell = pyshell
self.idb = idb # If passed, a proxy of remote instance.
self.frame = None
self.make_gui()
self.interacting = 0
self.interacting = False
self.nesting_level = 0

def run(self, *args):
"""Run the debugger."""
# Deal with the scenario where we've already got a program running
# in the debugger and we want to start another. If that is the case,
# our second 'run' was invoked from an event dispatched not from
Expand Down Expand Up @@ -104,12 +150,13 @@ def run(self, *args):
self.root.after(100, lambda: self.run(*args))
return
try:
self.interacting = 1
self.interacting = True
return self.idb.run(*args)
finally:
self.interacting = 0
self.interacting = False

def close(self, event=None):
"""Close the debugger and window."""
try:
self.quit()
except Exception:
Expand All @@ -127,6 +174,7 @@ def close(self, event=None):
self.top.destroy()

def make_gui(self):
"""Draw the debugger gui on the screen."""
pyshell = self.pyshell
self.flist = pyshell.flist
self.root = root = pyshell.root
Expand All @@ -135,11 +183,11 @@ def make_gui(self):
self.top.wm_iconname("Debug")
top.wm_protocol("WM_DELETE_WINDOW", self.close)
self.top.bind("<Escape>", self.close)
#

self.bframe = bframe = Frame(top)
self.bframe.pack(anchor="w")
self.buttons = bl = []
#

self.bcont = b = Button(bframe, text="Go", command=self.cont)
bl.append(b)
self.bstep = b = Button(bframe, text="Step", command=self.step)
Expand All @@ -150,14 +198,14 @@ def make_gui(self):
bl.append(b)
self.bret = b = Button(bframe, text="Quit", command=self.quit)
bl.append(b)
#

for b in bl:
b.configure(state="disabled")
b.pack(side="left")
#

self.cframe = cframe = Frame(bframe)
self.cframe.pack(side="left")
#

if not self.vstack:
self.__class__.vstack = BooleanVar(top)
self.vstack.set(1)
Expand All @@ -180,20 +228,20 @@ def make_gui(self):
self.bglobals = Checkbutton(cframe,
text="Globals", command=self.show_globals, variable=self.vglobals)
self.bglobals.grid(row=1, column=1)
#

self.status = Label(top, anchor="w")
self.status.pack(anchor="w")
self.error = Label(top, anchor="w")
self.error.pack(anchor="w", fill="x")
self.errorbg = self.error.cget("background")
#

self.fstack = Frame(top, height=1)
self.fstack.pack(expand=1, fill="both")
self.flocals = Frame(top)
self.flocals.pack(expand=1, fill="both")
self.fglobals = Frame(top, height=1)
self.fglobals.pack(expand=1, fill="both")
#

if self.vstack.get():
self.show_stack()
if self.vlocals.get():
Expand All @@ -204,7 +252,7 @@ def make_gui(self):
def interaction(self, message, frame, info=None):
self.frame = frame
self.status.configure(text=message)
#

if info:
type, value, tb = info
try:
Expand All @@ -223,28 +271,28 @@ def interaction(self, message, frame, info=None):
tb = None
bg = self.errorbg
self.error.configure(text=m1, background=bg)
#

sv = self.stackviewer
if sv:
stack, i = self.idb.get_stack(self.frame, tb)
sv.load_stack(stack, i)
#

self.show_variables(1)
#

if self.vsource.get():
self.sync_source_line()
#

for b in self.buttons:
b.configure(state="normal")
#

self.top.wakeup()
# Nested main loop: Tkinter's main loop is not reentrant, so use
# Tcl's vwait facility, which reenters the event loop until an
# event handler sets the variable we're waiting on
# event handler sets the variable we're waiting on.
self.nesting_level += 1
self.root.tk.call('vwait', '::idledebugwait')
self.nesting_level -= 1
#

for b in self.buttons:
b.configure(state="disabled")
self.status.configure(text="")
Expand Down Expand Up @@ -288,8 +336,6 @@ def quit(self):
def abort_loop(self):
self.root.tk.call('set', '::idledebugwait', '1')

stackviewer = None

def show_stack(self):
if not self.stackviewer and self.vstack.get():
self.stackviewer = sv = StackViewer(self.fstack, self.flist, self)
Expand All @@ -311,9 +357,6 @@ def show_frame(self, stackitem):
self.frame = stackitem[0] # lineno is stackitem[1]
self.show_variables()

localsviewer = None
globalsviewer = None

def show_locals(self):
lv = self.localsviewer
if self.vlocals.get():
Expand Down Expand Up @@ -354,26 +397,32 @@ def show_variables(self, force=0):
if gv:
gv.load_dict(gdict, force, self.pyshell.interp.rpcclt)

def set_breakpoint_here(self, filename, lineno):
def set_breakpoint(self, filename, lineno):
"""Set a filename-lineno breakpoint in the debugger.

Called from self.load_breakpoints and EW.setbreakpoint
"""
self.idb.set_break(filename, lineno)

def clear_breakpoint_here(self, filename, lineno):
def clear_breakpoint(self, filename, lineno):
self.idb.clear_break(filename, lineno)

def clear_file_breaks(self, filename):
self.idb.clear_all_file_breaks(filename)

def load_breakpoints(self):
"Load PyShellEditorWindow breakpoints into subprocess debugger"
"""Load PyShellEditorWindow breakpoints into subprocess debugger."""
for editwin in self.pyshell.flist.inversedict:
filename = editwin.io.filename
try:
for lineno in editwin.breakpoints:
self.set_breakpoint_here(filename, lineno)
self.set_breakpoint(filename, lineno)
except AttributeError:
continue


class StackViewer(ScrolledList):
"Code stack viewer for debugger GUI."

def __init__(self, master, flist, gui):
if macosx.isAquaTk():
Expand Down Expand Up @@ -414,25 +463,25 @@ def load_stack(self, stack, index=None):
self.select(index)

def popup_event(self, event):
"override base method"
"Override base method."
if self.stack:
return ScrolledList.popup_event(self, event)

def fill_menu(self):
"override base method"
"Override base method."
menu = self.menu
menu.add_command(label="Go to source line",
command=self.goto_source_line)
menu.add_command(label="Show stack frame",
command=self.show_stack_frame)

def on_select(self, index):
"override base method"
"Override base method."
if 0 <= index < len(self.stack):
self.gui.show_frame(self.stack[index])

def on_double(self, index):
"override base method"
"Override base method."
self.show_source(index)

def goto_source_line(self):
Expand All @@ -457,6 +506,7 @@ def show_source(self, index):


class NamespaceViewer:
"Global/local namespace viewer for debugger GUI."

def __init__(self, master, title, dict=None):
width = 0
Expand Down Expand Up @@ -544,6 +594,7 @@ def load_dict(self, dict, force=0, rpc_client=None):
def close(self):
self.frame.destroy()


if __name__ == "__main__":
from unittest import main
main('idlelib.idle_test.test_debugger', verbosity=2, exit=False)
Expand Down
Loading