Skip to content

Commit a62dcac

Browse files
miss-islingtontonybaloneyterryjreedy
authored
[3.12] gh-79871: IDLE - Fix and test debugger module (GH-11451) (#112256)
gh-79871: IDLE - Fix and test debugger module (GH-11451) Add docstrings to the debugger module. Fix two bugs: initialize Idb.botframe (should be in Bdb); In Idb.in_rpc_code, check whether prev_frame is None before trying to use it. Make other code changes. Expand test_debugger coverage from 19% to 66%. --------- (cherry picked from commit adedcfa) Co-authored-by: Anthony Shaw <[email protected]> Co-authored-by: Terry Jan Reedy <[email protected]>
1 parent 919be35 commit a62dcac

File tree

5 files changed

+400
-77
lines changed

5 files changed

+400
-77
lines changed

Lib/idlelib/debugger.py

+115-64
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
"""Debug user code with a GUI interface to a subclass of bdb.Bdb.
2+
3+
The Idb idb and Debugger gui instances each need a reference to each
4+
other or to an rpc proxy for each other.
5+
6+
If IDLE is started with '-n', so that user code and idb both run in the
7+
IDLE process, Debugger is called without an idb. Debugger.__init__
8+
calls Idb with its incomplete self. Idb.__init__ stores gui and gui
9+
then stores idb.
10+
11+
If IDLE is started normally, so that user code executes in a separate
12+
process, debugger_r.start_remote_debugger is called, executing in the
13+
IDLE process. It calls 'start the debugger' in the remote process,
14+
which calls Idb with a gui proxy. Then Debugger is called in the IDLE
15+
for more.
16+
"""
17+
118
import bdb
219
import os
320

@@ -10,66 +27,95 @@
1027

1128

1229
class Idb(bdb.Bdb):
30+
"Supply user_line and user_exception functions for Bdb."
1331

1432
def __init__(self, gui):
15-
self.gui = gui # An instance of Debugger or proxy of remote.
16-
bdb.Bdb.__init__(self)
33+
self.gui = gui # An instance of Debugger or proxy thereof.
34+
super().__init__()
1735

1836
def user_line(self, frame):
19-
if self.in_rpc_code(frame):
37+
"""Handle a user stopping or breaking at a line.
38+
39+
Convert frame to a string and send it to gui.
40+
"""
41+
if _in_rpc_code(frame):
2042
self.set_step()
2143
return
22-
message = self.__frame2message(frame)
44+
message = _frame2message(frame)
2345
try:
2446
self.gui.interaction(message, frame)
2547
except TclError: # When closing debugger window with [x] in 3.x
2648
pass
2749

28-
def user_exception(self, frame, info):
29-
if self.in_rpc_code(frame):
50+
def user_exception(self, frame, exc_info):
51+
"""Handle an the occurrence of an exception."""
52+
if _in_rpc_code(frame):
3053
self.set_step()
3154
return
32-
message = self.__frame2message(frame)
33-
self.gui.interaction(message, frame, info)
34-
35-
def in_rpc_code(self, frame):
36-
if frame.f_code.co_filename.count('rpc.py'):
37-
return True
38-
else:
39-
prev_frame = frame.f_back
40-
prev_name = prev_frame.f_code.co_filename
41-
if 'idlelib' in prev_name and 'debugger' in prev_name:
42-
# catch both idlelib/debugger.py and idlelib/debugger_r.py
43-
# on both Posix and Windows
44-
return False
45-
return self.in_rpc_code(prev_frame)
46-
47-
def __frame2message(self, frame):
48-
code = frame.f_code
49-
filename = code.co_filename
50-
lineno = frame.f_lineno
51-
basename = os.path.basename(filename)
52-
message = f"{basename}:{lineno}"
53-
if code.co_name != "?":
54-
message = f"{message}: {code.co_name}()"
55-
return message
55+
message = _frame2message(frame)
56+
self.gui.interaction(message, frame, exc_info)
57+
58+
def _in_rpc_code(frame):
59+
"Determine if debugger is within RPC code."
60+
if frame.f_code.co_filename.count('rpc.py'):
61+
return True # Skip this frame.
62+
else:
63+
prev_frame = frame.f_back
64+
if prev_frame is None:
65+
return False
66+
prev_name = prev_frame.f_code.co_filename
67+
if 'idlelib' in prev_name and 'debugger' in prev_name:
68+
# catch both idlelib/debugger.py and idlelib/debugger_r.py
69+
# on both Posix and Windows
70+
return False
71+
return _in_rpc_code(prev_frame)
72+
73+
def _frame2message(frame):
74+
"""Return a message string for frame."""
75+
code = frame.f_code
76+
filename = code.co_filename
77+
lineno = frame.f_lineno
78+
basename = os.path.basename(filename)
79+
message = f"{basename}:{lineno}"
80+
if code.co_name != "?":
81+
message = f"{message}: {code.co_name}()"
82+
return message
5683

5784

5885
class Debugger:
59-
60-
vstack = vsource = vlocals = vglobals = None
86+
"""The debugger interface.
87+
88+
This class handles the drawing of the debugger window and
89+
the interactions with the underlying debugger session.
90+
"""
91+
vstack = None
92+
vsource = None
93+
vlocals = None
94+
vglobals = None
95+
stackviewer = None
96+
localsviewer = None
97+
globalsviewer = None
6198

6299
def __init__(self, pyshell, idb=None):
100+
"""Instantiate and draw a debugger window.
101+
102+
:param pyshell: An instance of the PyShell Window
103+
:type pyshell: :class:`idlelib.pyshell.PyShell`
104+
105+
:param idb: An instance of the IDLE debugger (optional)
106+
:type idb: :class:`idlelib.debugger.Idb`
107+
"""
63108
if idb is None:
64109
idb = Idb(self)
65110
self.pyshell = pyshell
66111
self.idb = idb # If passed, a proxy of remote instance.
67112
self.frame = None
68113
self.make_gui()
69-
self.interacting = 0
114+
self.interacting = False
70115
self.nesting_level = 0
71116

72117
def run(self, *args):
118+
"""Run the debugger."""
73119
# Deal with the scenario where we've already got a program running
74120
# in the debugger and we want to start another. If that is the case,
75121
# our second 'run' was invoked from an event dispatched not from
@@ -104,12 +150,13 @@ def run(self, *args):
104150
self.root.after(100, lambda: self.run(*args))
105151
return
106152
try:
107-
self.interacting = 1
153+
self.interacting = True
108154
return self.idb.run(*args)
109155
finally:
110-
self.interacting = 0
156+
self.interacting = False
111157

112158
def close(self, event=None):
159+
"""Close the debugger and window."""
113160
try:
114161
self.quit()
115162
except Exception:
@@ -127,6 +174,7 @@ def close(self, event=None):
127174
self.top.destroy()
128175

129176
def make_gui(self):
177+
"""Draw the debugger gui on the screen."""
130178
pyshell = self.pyshell
131179
self.flist = pyshell.flist
132180
self.root = root = pyshell.root
@@ -135,11 +183,11 @@ def make_gui(self):
135183
self.top.wm_iconname("Debug")
136184
top.wm_protocol("WM_DELETE_WINDOW", self.close)
137185
self.top.bind("<Escape>", self.close)
138-
#
186+
139187
self.bframe = bframe = Frame(top)
140188
self.bframe.pack(anchor="w")
141189
self.buttons = bl = []
142-
#
190+
143191
self.bcont = b = Button(bframe, text="Go", command=self.cont)
144192
bl.append(b)
145193
self.bstep = b = Button(bframe, text="Step", command=self.step)
@@ -150,14 +198,14 @@ def make_gui(self):
150198
bl.append(b)
151199
self.bret = b = Button(bframe, text="Quit", command=self.quit)
152200
bl.append(b)
153-
#
201+
154202
for b in bl:
155203
b.configure(state="disabled")
156204
b.pack(side="left")
157-
#
205+
158206
self.cframe = cframe = Frame(bframe)
159207
self.cframe.pack(side="left")
160-
#
208+
161209
if not self.vstack:
162210
self.__class__.vstack = BooleanVar(top)
163211
self.vstack.set(1)
@@ -180,20 +228,20 @@ def make_gui(self):
180228
self.bglobals = Checkbutton(cframe,
181229
text="Globals", command=self.show_globals, variable=self.vglobals)
182230
self.bglobals.grid(row=1, column=1)
183-
#
231+
184232
self.status = Label(top, anchor="w")
185233
self.status.pack(anchor="w")
186234
self.error = Label(top, anchor="w")
187235
self.error.pack(anchor="w", fill="x")
188236
self.errorbg = self.error.cget("background")
189-
#
237+
190238
self.fstack = Frame(top, height=1)
191239
self.fstack.pack(expand=1, fill="both")
192240
self.flocals = Frame(top)
193241
self.flocals.pack(expand=1, fill="both")
194242
self.fglobals = Frame(top, height=1)
195243
self.fglobals.pack(expand=1, fill="both")
196-
#
244+
197245
if self.vstack.get():
198246
self.show_stack()
199247
if self.vlocals.get():
@@ -204,7 +252,7 @@ def make_gui(self):
204252
def interaction(self, message, frame, info=None):
205253
self.frame = frame
206254
self.status.configure(text=message)
207-
#
255+
208256
if info:
209257
type, value, tb = info
210258
try:
@@ -223,28 +271,28 @@ def interaction(self, message, frame, info=None):
223271
tb = None
224272
bg = self.errorbg
225273
self.error.configure(text=m1, background=bg)
226-
#
274+
227275
sv = self.stackviewer
228276
if sv:
229277
stack, i = self.idb.get_stack(self.frame, tb)
230278
sv.load_stack(stack, i)
231-
#
279+
232280
self.show_variables(1)
233-
#
281+
234282
if self.vsource.get():
235283
self.sync_source_line()
236-
#
284+
237285
for b in self.buttons:
238286
b.configure(state="normal")
239-
#
287+
240288
self.top.wakeup()
241289
# Nested main loop: Tkinter's main loop is not reentrant, so use
242290
# Tcl's vwait facility, which reenters the event loop until an
243-
# event handler sets the variable we're waiting on
291+
# event handler sets the variable we're waiting on.
244292
self.nesting_level += 1
245293
self.root.tk.call('vwait', '::idledebugwait')
246294
self.nesting_level -= 1
247-
#
295+
248296
for b in self.buttons:
249297
b.configure(state="disabled")
250298
self.status.configure(text="")
@@ -288,8 +336,6 @@ def quit(self):
288336
def abort_loop(self):
289337
self.root.tk.call('set', '::idledebugwait', '1')
290338

291-
stackviewer = None
292-
293339
def show_stack(self):
294340
if not self.stackviewer and self.vstack.get():
295341
self.stackviewer = sv = StackViewer(self.fstack, self.flist, self)
@@ -311,9 +357,6 @@ def show_frame(self, stackitem):
311357
self.frame = stackitem[0] # lineno is stackitem[1]
312358
self.show_variables()
313359

314-
localsviewer = None
315-
globalsviewer = None
316-
317360
def show_locals(self):
318361
lv = self.localsviewer
319362
if self.vlocals.get():
@@ -354,26 +397,32 @@ def show_variables(self, force=0):
354397
if gv:
355398
gv.load_dict(gdict, force, self.pyshell.interp.rpcclt)
356399

357-
def set_breakpoint_here(self, filename, lineno):
400+
def set_breakpoint(self, filename, lineno):
401+
"""Set a filename-lineno breakpoint in the debugger.
402+
403+
Called from self.load_breakpoints and EW.setbreakpoint
404+
"""
358405
self.idb.set_break(filename, lineno)
359406

360-
def clear_breakpoint_here(self, filename, lineno):
407+
def clear_breakpoint(self, filename, lineno):
361408
self.idb.clear_break(filename, lineno)
362409

363410
def clear_file_breaks(self, filename):
364411
self.idb.clear_all_file_breaks(filename)
365412

366413
def load_breakpoints(self):
367-
"Load PyShellEditorWindow breakpoints into subprocess debugger"
414+
"""Load PyShellEditorWindow breakpoints into subprocess debugger."""
368415
for editwin in self.pyshell.flist.inversedict:
369416
filename = editwin.io.filename
370417
try:
371418
for lineno in editwin.breakpoints:
372-
self.set_breakpoint_here(filename, lineno)
419+
self.set_breakpoint(filename, lineno)
373420
except AttributeError:
374421
continue
375422

423+
376424
class StackViewer(ScrolledList):
425+
"Code stack viewer for debugger GUI."
377426

378427
def __init__(self, master, flist, gui):
379428
if macosx.isAquaTk():
@@ -414,25 +463,25 @@ def load_stack(self, stack, index=None):
414463
self.select(index)
415464

416465
def popup_event(self, event):
417-
"override base method"
466+
"Override base method."
418467
if self.stack:
419468
return ScrolledList.popup_event(self, event)
420469

421470
def fill_menu(self):
422-
"override base method"
471+
"Override base method."
423472
menu = self.menu
424473
menu.add_command(label="Go to source line",
425474
command=self.goto_source_line)
426475
menu.add_command(label="Show stack frame",
427476
command=self.show_stack_frame)
428477

429478
def on_select(self, index):
430-
"override base method"
479+
"Override base method."
431480
if 0 <= index < len(self.stack):
432481
self.gui.show_frame(self.stack[index])
433482

434483
def on_double(self, index):
435-
"override base method"
484+
"Override base method."
436485
self.show_source(index)
437486

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

458507

459508
class NamespaceViewer:
509+
"Global/local namespace viewer for debugger GUI."
460510

461511
def __init__(self, master, title, dict=None):
462512
width = 0
@@ -544,6 +594,7 @@ def load_dict(self, dict, force=0, rpc_client=None):
544594
def close(self):
545595
self.frame.destroy()
546596

597+
547598
if __name__ == "__main__":
548599
from unittest import main
549600
main('idlelib.idle_test.test_debugger', verbosity=2, exit=False)

0 commit comments

Comments
 (0)