Skip to content

[3.12] gh-79871: IDLE - Fix and test debugger module (GH-11451) #112256

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 1 commit into from
Nov 19, 2023
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
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