1"""Debug user code with a GUI interface to a subclass of bdb.Bdb. 2 3The Idb idb and Debugger gui instances each need a reference to each 4other or to an rpc proxy for each other. 5 6If IDLE is started with '-n', so that user code and idb both run in the 7IDLE process, Debugger is called without an idb. Debugger.__init__ 8calls Idb with its incomplete self. Idb.__init__ stores gui and gui 9then stores idb. 10 11If IDLE is started normally, so that user code executes in a separate 12process, debugger_r.start_remote_debugger is called, executing in the 13IDLE process. It calls 'start the debugger' in the remote process, 14which calls Idb with a gui proxy. Then Debugger is called in the IDLE 15for more. 16""" 17 18import bdb 19import os 20 21from tkinter import * 22from tkinter.ttk import Frame, Scrollbar 23 24from idlelib import macosx 25from idlelib.scrolledlist import ScrolledList 26from idlelib.window import ListedToplevel 27 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 return 44 message = _frame2message(frame) 45 try: 46 self.gui.interaction(message, frame) 47 except TclError: # When closing debugger window with [x] in 3.x 48 pass 49 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 return 55 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_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 73def _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 83 84 85class Debugger: 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 98 99 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 """ 108 if idb is None: 109 idb = Idb(self) 110 self.pyshell = pyshell 111 self.idb = idb # If passed, a proxy of remote instance. 112 self.frame = None 113 self.make_gui() 114 self.interacting = False 115 self.nesting_level = 0 116 117 def run(self, *args): 118 """Run the debugger.""" 119 # Deal with the scenario where we've already got a program running 120 # 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 from 122 # 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 loop 125 # run() 126 # <running program with traces> 127 # callback to debugger's interaction() 128 # nested event loop 129 # run() for second command 130 # 131 # This kind of nesting of event loops causes all kinds of problems 132 # (see e.g. issue #24455) especially when dealing with running as a 133 # subprocess, where there's all kinds of extra stuff happening in 134 # there - insert a traceback.print_stack() to check it out. 135 # 136 # By this point, we've already called restart_subprocess() in 137 # ScriptBinding. However, we also need to unwind the stack back to 138 # that outer event loop. To accomplish this, we: 139 # - return immediately from the nested run() 140 # - abort_loop ensures the nested event loop will terminate 141 # - the debugger's interaction routine completes normally 142 # - the restart_subprocess() will have taken care of stopping 143 # the running program, which will also let the outer run complete 144 # 145 # That leaves us back at the outer main event loop, at which point our 146 # after event can fire, and we'll come back to this routine with a 147 # clean stack. 148 if self.nesting_level > 0: 149 self.abort_loop() 150 self.root.after(100, lambda: self.run(*args)) 151 return 152 try: 153 self.interacting = True 154 return self.idb.run(*args) 155 finally: 156 self.interacting = False 157 158 def close(self, event=None): 159 """Close the debugger and window.""" 160 try: 161 self.quit() 162 except Exception: 163 pass 164 if self.interacting: 165 self.top.bell() 166 return 167 if self.stackviewer: 168 self.stackviewer.close(); self.stackviewer = None 169 # Clean up pyshell if user clicked debugger control close widget. 170 # (Causes a harmless extra cycle through close_debugger() if user 171 # 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.pyshell 179 self.flist = pyshell.flist 180 self.root = root = pyshell.root 181 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 = frame 254 self.status.configure(text=message) 255 256 if info: 257 type, value, tb = info 258 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 pass 268 bg = "yellow" 269 else: 270 m1 = "" 271 tb = None 272 bg = self.errorbg 273 self.error.configure(text=m1, background=bg) 274 275 sv = self.stackviewer 276 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 use 290 # Tcl's vwait facility, which reenters the event loop until an 291 # event handler sets the variable we're waiting on. 292 self.nesting_level += 1 293 self.root.tk.call('vwait', '::idledebugwait') 294 self.nesting_level -= 1 295 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 = None 301 302 def sync_source_line(self): 303 frame = self.frame 304 if not frame: 305 return 306 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_code 312 filename = code.co_filename 313 lineno = frame.f_lineno 314 return filename, lineno 315 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.stackviewer 347 if sv and not self.vstack.get(): 348 self.stackviewer = None 349 sv.close() 350 self.fstack['height'] = 1 351 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.localsviewer 362 if self.vlocals.get(): 363 if not lv: 364 self.localsviewer = NamespaceViewer(self.flocals, "Locals") 365 else: 366 if lv: 367 self.localsviewer = None 368 lv.close() 369 self.flocals['height'] = 1 370 self.show_variables() 371 372 def show_globals(self): 373 gv = self.globalsviewer 374 if self.vglobals.get(): 375 if not gv: 376 self.globalsviewer = NamespaceViewer(self.fglobals, "Globals") 377 else: 378 if gv: 379 self.globalsviewer = None 380 gv.close() 381 self.fglobals['height'] = 1 382 self.show_variables() 383 384 def show_variables(self, force=0): 385 lv = self.localsviewer 386 gv = self.globalsviewer 387 frame = self.frame 388 if not frame: 389 ldict = gdict = None 390 else: 391 ldict = frame.f_locals 392 gdict = frame.f_globals 393 if lv and gv and ldict is gdict: 394 ldict = None 395 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.setbreakpoint 404 """ 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.filename 417 try: 418 for lineno in editwin.breakpoints: 419 self.set_breakpoint(filename, lineno) 420 except AttributeError: 421 continue 422 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'll 430 # get a shaking GUI that eventually kills IDLE if the width 431 # argument is specified. 432 ScrolledList.__init__(self, master) 433 else: 434 ScrolledList.__init__(self, master, width=80) 435 self.flist = flist 436 self.gui = gui 437 self.stack = [] 438 439 def load_stack(self, stack, index=None): 440 self.stack = stack 441 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_code 449 filename = code.co_filename 450 funcname = code.co_name 451 import linecache 452 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 = "> " + item 461 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.menu 473 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 return 499 frame, lineno = self.stack[index] 500 code = frame.f_code 501 filename = code.co_filename 502 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 = 0 513 height = 40 514 if odict: 515 height = 20*len(odict) # XXX 20 == observed height of Entry widget 516 self.master = master 517 self.title = title 518 import reprlib 519 self.repr = reprlib.Repr() 520 self.repr.maxstring = 60 521 self.repr.maxother = 60 522 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.yview 533 canvas["yscrollcommand"] = vbar.set 534 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 return 543 subframe = self.subframe 544 frame = self.frame 545 for c in list(subframe.children.values()): 546 c.destroy() 547 self.prev_odict = None 548 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 yet 555 # public or pickleable), have the subprocess to send a list of 556 # keys, not a dict_keys object. sorted() will take a dict_keys 557 # (no subprocess) or a list. 558 # 559 # There is also an obscure bug in sorted(dict) where the 560 # 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 = 0 567 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+1 580 self.prev_odict = odict 581 # 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.canvas 586 self.canvas["scrollregion"] = (0, 0, width, height) 587 if height > 300: 588 canvas["height"] = 300 589 frame.pack(expand=1) 590 else: 591 canvas["height"] = height 592 frame.pack(expand=0) 593 594 def close(self): 595 self.frame.destroy() 596 597 598if __name__ == "__main__": 599 from unittest import main 600 main('idlelib.idle_test.test_debugger', verbosity=2, exit=False) 601 602# TODO: htest? 603