codekingpro/portable-devtools
114k
1"""Debug user code with a GUI interface to a subclass of bdb.Bdb.2 3The Idb instance 'idb' and Debugger instance 'gui' need references to each4other or to an rpc proxy for each other.5 6If IDLE is started with '-n', so that user code and idb both run in the7IDLE process, Debugger is called without an idb. Debugger.__init__8calls Idb with its incomplete self. Idb.__init__ stores gui and gui9then stores idb.10 11If IDLE is started normally, so that user code executes in a separate12process, debugger_r.start_remote_debugger is called, executing in the13IDLE process. It calls 'start the debugger' in the remote process,14which calls Idb with a gui proxy. Then Debugger is called in the IDLE15for more.16"""17 18import bdb19import os20 21from tkinter import *22from tkinter.ttk import Frame, Scrollbar23 24from idlelib import macosx25from idlelib.scrolledlist import ScrolledList26from idlelib.window import ListedToplevel27 28 29class Idb(bdb.Bdb):30 "Supply user_line and user_exception functions for Bdb."31 32 def __init__(self, gui):33 self.gui = gui # An instance of Debugger or proxy thereof.34 super().__init__()35 36 def user_line(self, 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):42 self.set_step()43 return44 message = _frame2message(frame)45 try:46 self.gui.interaction(message, frame)47 except TclError: # When closing debugger window with [x] in 3.x48 pass49 50 def user_exception(self, frame, exc_info):51 """Handle an the occurrence of an exception."""52 if _in_rpc_code(frame):53 self.set_step()54 return55 message = _frame2message(frame)56 self.gui.interaction(message, frame, exc_info)57 58def _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_back64 if prev_frame is None:65 return False66 prev_name = prev_frame.f_code.co_filename67 if 'idlelib' in prev_name and 'debugger' in prev_name:68 # catch both idlelib/debugger.py and idlelib/debugger_r.py69 # on both Posix and Windows70 return False71 return _in_rpc_code(prev_frame)72 73def _frame2message(frame):74 """Return a message string for frame."""75 code = frame.f_code76 filename = code.co_filename77 lineno = frame.f_lineno78 basename = os.path.basename(filename)79 message = f"{basename}:{lineno}"80 if code.co_name != "?":81 message = f"{message}: {code.co_name}()"82 return message83 84 85class Debugger:86 """The debugger interface.87 88 This class handles the drawing of the debugger window and89 the interactions with the underlying debugger session.90 """91 vstack = None92 vsource = None93 vlocals = None94 vglobals = None95 stackviewer = None96 localsviewer = None97 globalsviewer = None98 99 def __init__(self, pyshell, idb=None):100 """Instantiate and draw a debugger window.101 102 :param pyshell: An instance of the PyShell Window103 :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 """108 if idb is None:109 idb = Idb(self)110 self.pyshell = pyshell111 self.idb = idb # If passed, a proxy of remote instance.112 self.frame = None113 self.make_gui()114 self.interacting = False115 self.nesting_level = 0116 117 def run(self, *args):118 """Run the debugger."""119 # Deal with the scenario where we've already got a program running120 # in the debugger and we want to start another. If that is the case,121 # our second 'run' was invoked from an event dispatched not from122 # the main event loop, but from the nested event loop in 'interaction'123 # below. So our stack looks something like this:124 # outer main event loop125 # run()126 # <running program with traces>127 # callback to debugger's interaction()128 # nested event loop129 # run() for second command130 #131 # This kind of nesting of event loops causes all kinds of problems132 # (see e.g. issue #24455) especially when dealing with running as a133 # subprocess, where there's all kinds of extra stuff happening in134 # there - insert a traceback.print_stack() to check it out.135 #136 # By this point, we've already called restart_subprocess() in137 # ScriptBinding. However, we also need to unwind the stack back to138 # that outer event loop. To accomplish this, we:139 # - return immediately from the nested run()140 # - abort_loop ensures the nested event loop will terminate141 # - the debugger's interaction routine completes normally142 # - the restart_subprocess() will have taken care of stopping143 # the running program, which will also let the outer run complete144 #145 # That leaves us back at the outer main event loop, at which point our146 # after event can fire, and we'll come back to this routine with a147 # clean stack.148 if self.nesting_level > 0:149 self.abort_loop()150 self.root.after(100, lambda: self.run(*args))151 return152 try:153 self.interacting = True154 return self.idb.run(*args)155 finally:156 self.interacting = False157 158 def close(self, event=None):159 """Close the debugger and window."""160 try:161 self.quit()162 except Exception:163 pass164 if self.interacting:165 self.top.bell()166 return167 if self.stackviewer:168 self.stackviewer.close(); self.stackviewer = None169 # Clean up pyshell if user clicked debugger control close widget.170 # (Causes a harmless extra cycle through close_debugger() if user171 # toggled debugger from pyshell Debug menu)172 self.pyshell.close_debugger()173 # Now close the debugger control window....174 self.top.destroy()175 176 def make_gui(self):177 """Draw the debugger gui on the screen."""178 pyshell = self.pyshell179 self.flist = pyshell.flist180 self.root = root = pyshell.root181 self.top = top = ListedToplevel(root)182 self.top.wm_title("Debug Control")183 self.top.wm_iconname("Debug")184 top.wm_protocol("WM_DELETE_WINDOW", self.close)185 self.top.bind("<Escape>", self.close)186 187 self.bframe = bframe = Frame(top)188 self.bframe.pack(anchor="w")189 self.buttons = bl = []190 191 self.bcont = b = Button(bframe, text="Go", command=self.cont)192 bl.append(b)193 self.bstep = b = Button(bframe, text="Step", command=self.step)194 bl.append(b)195 self.bnext = b = Button(bframe, text="Over", command=self.next)196 bl.append(b)197 self.bret = b = Button(bframe, text="Out", command=self.ret)198 bl.append(b)199 self.bret = b = Button(bframe, text="Quit", command=self.quit)200 bl.append(b)201 202 for b in bl:203 b.configure(state="disabled")204 b.pack(side="left")205 206 self.cframe = cframe = Frame(bframe)207 self.cframe.pack(side="left")208 209 if not self.vstack:210 self.__class__.vstack = BooleanVar(top)211 self.vstack.set(1)212 self.bstack = Checkbutton(cframe,213 text="Stack", command=self.show_stack, variable=self.vstack)214 self.bstack.grid(row=0, column=0)215 if not self.vsource:216 self.__class__.vsource = BooleanVar(top)217 self.bsource = Checkbutton(cframe,218 text="Source", command=self.show_source, variable=self.vsource)219 self.bsource.grid(row=0, column=1)220 if not self.vlocals:221 self.__class__.vlocals = BooleanVar(top)222 self.vlocals.set(1)223 self.blocals = Checkbutton(cframe,224 text="Locals", command=self.show_locals, variable=self.vlocals)225 self.blocals.grid(row=1, column=0)226 if not self.vglobals:227 self.__class__.vglobals = BooleanVar(top)228 self.bglobals = Checkbutton(cframe,229 text="Globals", command=self.show_globals, variable=self.vglobals)230 self.bglobals.grid(row=1, column=1)231 232 self.status = Label(top, anchor="w")233 self.status.pack(anchor="w")234 self.error = Label(top, anchor="w")235 self.error.pack(anchor="w", fill="x")236 self.errorbg = self.error.cget("background")237 238 self.fstack = Frame(top, height=1)239 self.fstack.pack(expand=1, fill="both")240 self.flocals = Frame(top)241 self.flocals.pack(expand=1, fill="both")242 self.fglobals = Frame(top, height=1)243 self.fglobals.pack(expand=1, fill="both")244 245 if self.vstack.get():246 self.show_stack()247 if self.vlocals.get():248 self.show_locals()249 if self.vglobals.get():250 self.show_globals()251 252 def interaction(self, message, frame, info=None):253 self.frame = frame254 self.status.configure(text=message)255 256 if info:257 type, value, tb = info258 try:259 m1 = type.__name__260 except AttributeError:261 m1 = "%s" % str(type)262 if value is not None:263 try:264 # TODO redo entire section, tries not needed.265 m1 = f"{m1}: {value}"266 except:267 pass268 bg = "yellow"269 else:270 m1 = ""271 tb = None272 bg = self.errorbg273 self.error.configure(text=m1, background=bg)274 275 sv = self.stackviewer276 if sv:277 stack, i = self.idb.get_stack(self.frame, tb)278 sv.load_stack(stack, i)279 280 self.show_variables(1)281 282 if self.vsource.get():283 self.sync_source_line()284 285 for b in self.buttons:286 b.configure(state="normal")287 288 self.top.wakeup()289 # Nested main loop: Tkinter's main loop is not reentrant, so use290 # Tcl's vwait facility, which reenters the event loop until an291 # event handler sets the variable we're waiting on.292 self.nesting_level += 1293 self.root.tk.call('vwait', '::idledebugwait')294 self.nesting_level -= 1295 296 for b in self.buttons:297 b.configure(state="disabled")298 self.status.configure(text="")299 self.error.configure(text="", background=self.errorbg)300 self.frame = None301 302 def sync_source_line(self):303 frame = self.frame304 if not frame:305 return306 filename, lineno = self.__frame2fileline(frame)307 if filename[:1] + filename[-1:] != "<>" and os.path.exists(filename):308 self.flist.gotofileline(filename, lineno)309 310 def __frame2fileline(self, frame):311 code = frame.f_code312 filename = code.co_filename313 lineno = frame.f_lineno314 return filename, lineno315 316 def cont(self):317 self.idb.set_continue()318 self.abort_loop()319 320 def step(self):321 self.idb.set_step()322 self.abort_loop()323 324 def next(self):325 self.idb.set_next(self.frame)326 self.abort_loop()327 328 def ret(self):329 self.idb.set_return(self.frame)330 self.abort_loop()331 332 def quit(self):333 self.idb.set_quit()334 self.abort_loop()335 336 def abort_loop(self):337 self.root.tk.call('set', '::idledebugwait', '1')338 339 def show_stack(self):340 if not self.stackviewer and self.vstack.get():341 self.stackviewer = sv = StackViewer(self.fstack, self.flist, self)342 if self.frame:343 stack, i = self.idb.get_stack(self.frame, None)344 sv.load_stack(stack, i)345 else:346 sv = self.stackviewer347 if sv and not self.vstack.get():348 self.stackviewer = None349 sv.close()350 self.fstack['height'] = 1351 352 def show_source(self):353 if self.vsource.get():354 self.sync_source_line()355 356 def show_frame(self, stackitem):357 self.frame = stackitem[0] # lineno is stackitem[1]358 self.show_variables()359 360 def show_locals(self):361 lv = self.localsviewer362 if self.vlocals.get():363 if not lv:364 self.localsviewer = NamespaceViewer(self.flocals, "Locals")365 else:366 if lv:367 self.localsviewer = None368 lv.close()369 self.flocals['height'] = 1370 self.show_variables()371 372 def show_globals(self):373 gv = self.globalsviewer374 if self.vglobals.get():375 if not gv:376 self.globalsviewer = NamespaceViewer(self.fglobals, "Globals")377 else:378 if gv:379 self.globalsviewer = None380 gv.close()381 self.fglobals['height'] = 1382 self.show_variables()383 384 def show_variables(self, force=0):385 lv = self.localsviewer386 gv = self.globalsviewer387 frame = self.frame388 if not frame:389 ldict = gdict = None390 else:391 ldict = frame.f_locals392 gdict = frame.f_globals393 if lv and gv and ldict is gdict:394 ldict = None395 if lv:396 lv.load_dict(ldict, force, self.pyshell.interp.rpcclt)397 if gv:398 gv.load_dict(gdict, force, self.pyshell.interp.rpcclt)399 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.setbreakpoint404 """405 self.idb.set_break(filename, lineno)406 407 def clear_breakpoint(self, filename, lineno):408 self.idb.clear_break(filename, lineno)409 410 def clear_file_breaks(self, filename):411 self.idb.clear_all_file_breaks(filename)412 413 def load_breakpoints(self):414 """Load PyShellEditorWindow breakpoints into subprocess debugger."""415 for editwin in self.pyshell.flist.inversedict:416 filename = editwin.io.filename417 try:418 for lineno in editwin.breakpoints:419 self.set_breakpoint(filename, lineno)420 except AttributeError:421 continue422 423 424class StackViewer(ScrolledList):425 "Code stack viewer for debugger GUI."426 427 def __init__(self, master, flist, gui):428 if macosx.isAquaTk():429 # At least on with the stock AquaTk version on OSX 10.4 you'll430 # get a shaking GUI that eventually kills IDLE if the width431 # argument is specified.432 ScrolledList.__init__(self, master)433 else:434 ScrolledList.__init__(self, master, width=80)435 self.flist = flist436 self.gui = gui437 self.stack = []438 439 def load_stack(self, stack, index=None):440 self.stack = stack441 self.clear()442 for i in range(len(stack)):443 frame, lineno = stack[i]444 try:445 modname = frame.f_globals["__name__"]446 except:447 modname = "?"448 code = frame.f_code449 filename = code.co_filename450 funcname = code.co_name451 import linecache452 sourceline = linecache.getline(filename, lineno)453 sourceline = sourceline.strip()454 if funcname in ("?", "", None):455 item = "%s, line %d: %s" % (modname, lineno, sourceline)456 else:457 item = "%s.%s(), line %d: %s" % (modname, funcname,458 lineno, sourceline)459 if i == index:460 item = "> " + item461 self.append(item)462 if index is not None:463 self.select(index)464 465 def popup_event(self, event):466 "Override base method."467 if self.stack:468 return ScrolledList.popup_event(self, event)469 470 def fill_menu(self):471 "Override base method."472 menu = self.menu473 menu.add_command(label="Go to source line",474 command=self.goto_source_line)475 menu.add_command(label="Show stack frame",476 command=self.show_stack_frame)477 478 def on_select(self, index):479 "Override base method."480 if 0 <= index < len(self.stack):481 self.gui.show_frame(self.stack[index])482 483 def on_double(self, index):484 "Override base method."485 self.show_source(index)486 487 def goto_source_line(self):488 index = self.listbox.index("active")489 self.show_source(index)490 491 def show_stack_frame(self):492 index = self.listbox.index("active")493 if 0 <= index < len(self.stack):494 self.gui.show_frame(self.stack[index])495 496 def show_source(self, index):497 if not (0 <= index < len(self.stack)):498 return499 frame, lineno = self.stack[index]500 code = frame.f_code501 filename = code.co_filename502 if os.path.isfile(filename):503 edit = self.flist.open(filename)504 if edit:505 edit.gotoline(lineno)506 507 508class NamespaceViewer:509 "Global/local namespace viewer for debugger GUI."510 511 def __init__(self, master, title, odict=None): # XXX odict never passed.512 width = 0513 height = 40514 if odict:515 height = 20*len(odict) # XXX 20 == observed height of Entry widget516 self.master = master517 self.title = title518 import reprlib519 self.repr = reprlib.Repr()520 self.repr.maxstring = 60521 self.repr.maxother = 60522 self.frame = frame = Frame(master)523 self.frame.pack(expand=1, fill="both")524 self.label = Label(frame, text=title, borderwidth=2, relief="groove")525 self.label.pack(fill="x")526 self.vbar = vbar = Scrollbar(frame, name="vbar")527 vbar.pack(side="right", fill="y")528 self.canvas = canvas = Canvas(frame,529 height=min(300, max(40, height)),530 scrollregion=(0, 0, width, height))531 canvas.pack(side="left", fill="both", expand=1)532 vbar["command"] = canvas.yview533 canvas["yscrollcommand"] = vbar.set534 self.subframe = subframe = Frame(canvas)535 self.sfid = canvas.create_window(0, 0, window=subframe, anchor="nw")536 self.load_dict(odict)537 538 prev_odict = -1 # Needed for initial comparison below.539 540 def load_dict(self, odict, force=0, rpc_client=None):541 if odict is self.prev_odict and not force:542 return543 subframe = self.subframe544 frame = self.frame545 for c in list(subframe.children.values()):546 c.destroy()547 self.prev_odict = None548 if not odict:549 l = Label(subframe, text="None")550 l.grid(row=0, column=0)551 else:552 #names = sorted(dict)553 #554 # Because of (temporary) limitations on the dict_keys type (not yet555 # public or pickleable), have the subprocess to send a list of556 # keys, not a dict_keys object. sorted() will take a dict_keys557 # (no subprocess) or a list.558 #559 # There is also an obscure bug in sorted(dict) where the560 # interpreter gets into a loop requesting non-existing dict[0],561 # dict[1], dict[2], etc from the debugger_r.DictProxy.562 # TODO recheck above; see debugger_r 159ff, debugobj 60.563 keys_list = odict.keys()564 names = sorted(keys_list)565 566 row = 0567 for name in names:568 value = odict[name]569 svalue = self.repr.repr(value) # repr(value)570 # Strip extra quotes caused by calling repr on the (already)571 # repr'd value sent across the RPC interface:572 if rpc_client:573 svalue = svalue[1:-1]574 l = Label(subframe, text=name)575 l.grid(row=row, column=0, sticky="nw")576 l = Entry(subframe, width=0, borderwidth=0)577 l.insert(0, svalue)578 l.grid(row=row, column=1, sticky="nw")579 row = row+1580 self.prev_odict = odict581 # XXX Could we use a <Configure> callback for the following?582 subframe.update_idletasks() # Alas!583 width = subframe.winfo_reqwidth()584 height = subframe.winfo_reqheight()585 canvas = self.canvas586 self.canvas["scrollregion"] = (0, 0, width, height)587 if height > 300:588 canvas["height"] = 300589 frame.pack(expand=1)590 else:591 canvas["height"] = height592 frame.pack(expand=0)593 594 def close(self):595 self.frame.destroy()596 597 598if __name__ == "__main__":599 from unittest import main600 main('idlelib.idle_test.test_debugger', verbosity=2, exit=False)601 602# TODO: htest?603 