Home | History | Annotate | Download | only in idlelib
      1 import os
      2 import bdb
      3 from Tkinter import *
      4 from idlelib.WindowList import ListedToplevel
      5 from idlelib.ScrolledList import ScrolledList
      6 from idlelib import macosxSupport
      7 
      8 
      9 class Idb(bdb.Bdb):
     10 
     11     def __init__(self, gui):
     12         self.gui = gui
     13         bdb.Bdb.__init__(self)
     14 
     15     def user_line(self, frame):
     16         if self.in_rpc_code(frame):
     17             self.set_step()
     18             return
     19         message = self.__frame2message(frame)
     20         try:
     21             self.gui.interaction(message, frame)
     22         except TclError:  # When closing debugger window with [x] in 3.x
     23             pass
     24 
     25     def user_exception(self, frame, info):
     26         if self.in_rpc_code(frame):
     27             self.set_step()
     28             return
     29         message = self.__frame2message(frame)
     30         self.gui.interaction(message, frame, info)
     31 
     32     def in_rpc_code(self, frame):
     33         if frame.f_code.co_filename.count('rpc.py'):
     34             return True
     35         else:
     36             prev_frame = frame.f_back
     37             if prev_frame.f_code.co_filename.count('Debugger.py'):
     38                 # (that test will catch both Debugger.py and RemoteDebugger.py)
     39                 return False
     40             return self.in_rpc_code(prev_frame)
     41 
     42     def __frame2message(self, frame):
     43         code = frame.f_code
     44         filename = code.co_filename
     45         lineno = frame.f_lineno
     46         basename = os.path.basename(filename)
     47         message = "%s:%s" % (basename, lineno)
     48         if code.co_name != "?":
     49             message = "%s: %s()" % (message, code.co_name)
     50         return message
     51 
     52 
     53 class Debugger:
     54 
     55     vstack = vsource = vlocals = vglobals = None
     56 
     57     def __init__(self, pyshell, idb=None):
     58         if idb is None:
     59             idb = Idb(self)
     60         self.pyshell = pyshell
     61         self.idb = idb
     62         self.frame = None
     63         self.make_gui()
     64         self.interacting = 0
     65         self.nesting_level = 0
     66 
     67     def run(self, *args):
     68         # Deal with the scenario where we've already got a program running
     69         # in the debugger and we want to start another. If that is the case,
     70         # our second 'run' was invoked from an event dispatched not from
     71         # the main event loop, but from the nested event loop in 'interaction'
     72         # below. So our stack looks something like this:
     73         #       outer main event loop
     74         #         run()
     75         #           <running program with traces>
     76         #             callback to debugger's interaction()
     77         #               nested event loop
     78         #                 run() for second command
     79         #
     80         # This kind of nesting of event loops causes all kinds of problems
     81         # (see e.g. issue #24455) especially when dealing with running as a
     82         # subprocess, where there's all kinds of extra stuff happening in
     83         # there - insert a traceback.print_stack() to check it out.
     84         #
     85         # By this point, we've already called restart_subprocess() in
     86         # ScriptBinding. However, we also need to unwind the stack back to
     87         # that outer event loop.  To accomplish this, we:
     88         #   - return immediately from the nested run()
     89         #   - abort_loop ensures the nested event loop will terminate
     90         #   - the debugger's interaction routine completes normally
     91         #   - the restart_subprocess() will have taken care of stopping
     92         #     the running program, which will also let the outer run complete
     93         #
     94         # That leaves us back at the outer main event loop, at which point our
     95         # after event can fire, and we'll come back to this routine with a
     96         # clean stack.
     97         if self.nesting_level > 0:
     98             self.abort_loop()
     99             self.root.after(100, lambda: self.run(*args))
    100             return
    101         try:
    102             self.interacting = 1
    103             return self.idb.run(*args)
    104         finally:
    105             self.interacting = 0
    106 
    107     def close(self, event=None):
    108         try:
    109             self.quit()
    110         except Exception:
    111             pass
    112         if self.interacting:
    113             self.top.bell()
    114             return
    115         if self.stackviewer:
    116             self.stackviewer.close(); self.stackviewer = None
    117         # Clean up pyshell if user clicked debugger control close widget.
    118         # (Causes a harmless extra cycle through close_debugger() if user
    119         # toggled debugger from pyshell Debug menu)
    120         self.pyshell.close_debugger()
    121         # Now close the debugger control window....
    122         self.top.destroy()
    123 
    124     def make_gui(self):
    125         pyshell = self.pyshell
    126         self.flist = pyshell.flist
    127         self.root = root = pyshell.root
    128         self.top = top = ListedToplevel(root)
    129         self.top.wm_title("Debug Control")
    130         self.top.wm_iconname("Debug")
    131         top.wm_protocol("WM_DELETE_WINDOW", self.close)
    132         self.top.bind("<Escape>", self.close)
    133         #
    134         self.bframe = bframe = Frame(top)
    135         self.bframe.pack(anchor="w")
    136         self.buttons = bl = []
    137         #
    138         self.bcont = b = Button(bframe, text="Go", command=self.cont)
    139         bl.append(b)
    140         self.bstep = b = Button(bframe, text="Step", command=self.step)
    141         bl.append(b)
    142         self.bnext = b = Button(bframe, text="Over", command=self.next)
    143         bl.append(b)
    144         self.bret = b = Button(bframe, text="Out", command=self.ret)
    145         bl.append(b)
    146         self.bret = b = Button(bframe, text="Quit", command=self.quit)
    147         bl.append(b)
    148         #
    149         for b in bl:
    150             b.configure(state="disabled")
    151             b.pack(side="left")
    152         #
    153         self.cframe = cframe = Frame(bframe)
    154         self.cframe.pack(side="left")
    155         #
    156         if not self.vstack:
    157             self.__class__.vstack = BooleanVar(top)
    158             self.vstack.set(1)
    159         self.bstack = Checkbutton(cframe,
    160             text="Stack", command=self.show_stack, variable=self.vstack)
    161         self.bstack.grid(row=0, column=0)
    162         if not self.vsource:
    163             self.__class__.vsource = BooleanVar(top)
    164         self.bsource = Checkbutton(cframe,
    165             text="Source", command=self.show_source, variable=self.vsource)
    166         self.bsource.grid(row=0, column=1)
    167         if not self.vlocals:
    168             self.__class__.vlocals = BooleanVar(top)
    169             self.vlocals.set(1)
    170         self.blocals = Checkbutton(cframe,
    171             text="Locals", command=self.show_locals, variable=self.vlocals)
    172         self.blocals.grid(row=1, column=0)
    173         if not self.vglobals:
    174             self.__class__.vglobals = BooleanVar(top)
    175         self.bglobals = Checkbutton(cframe,
    176             text="Globals", command=self.show_globals, variable=self.vglobals)
    177         self.bglobals.grid(row=1, column=1)
    178         #
    179         self.status = Label(top, anchor="w")
    180         self.status.pack(anchor="w")
    181         self.error = Label(top, anchor="w")
    182         self.error.pack(anchor="w", fill="x")
    183         self.errorbg = self.error.cget("background")
    184         #
    185         self.fstack = Frame(top, height=1)
    186         self.fstack.pack(expand=1, fill="both")
    187         self.flocals = Frame(top)
    188         self.flocals.pack(expand=1, fill="both")
    189         self.fglobals = Frame(top, height=1)
    190         self.fglobals.pack(expand=1, fill="both")
    191         #
    192         if self.vstack.get():
    193             self.show_stack()
    194         if self.vlocals.get():
    195             self.show_locals()
    196         if self.vglobals.get():
    197             self.show_globals()
    198 
    199     def interaction(self, message, frame, info=None):
    200         self.frame = frame
    201         self.status.configure(text=message)
    202         #
    203         if info:
    204             type, value, tb = info
    205             try:
    206                 m1 = type.__name__
    207             except AttributeError:
    208                 m1 = "%s" % str(type)
    209             if value is not None:
    210                 try:
    211                     m1 = "%s: %s" % (m1, str(value))
    212                 except:
    213                     pass
    214             bg = "yellow"
    215         else:
    216             m1 = ""
    217             tb = None
    218             bg = self.errorbg
    219         self.error.configure(text=m1, background=bg)
    220         #
    221         sv = self.stackviewer
    222         if sv:
    223             stack, i = self.idb.get_stack(self.frame, tb)
    224             sv.load_stack(stack, i)
    225         #
    226         self.show_variables(1)
    227         #
    228         if self.vsource.get():
    229             self.sync_source_line()
    230         #
    231         for b in self.buttons:
    232             b.configure(state="normal")
    233         #
    234         self.top.wakeup()
    235         # Nested main loop: Tkinter's main loop is not reentrant, so use
    236         # Tcl's vwait facility, which reenters the event loop until an
    237         # event handler sets the variable we're waiting on
    238         self.nesting_level += 1
    239         self.root.tk.call('vwait', '::idledebugwait')
    240         self.nesting_level -= 1
    241         #
    242         for b in self.buttons:
    243             b.configure(state="disabled")
    244         self.status.configure(text="")
    245         self.error.configure(text="", background=self.errorbg)
    246         self.frame = None
    247 
    248     def sync_source_line(self):
    249         frame = self.frame
    250         if not frame:
    251             return
    252         filename, lineno = self.__frame2fileline(frame)
    253         if filename[:1] + filename[-1:] != "<>" and os.path.exists(filename):
    254             self.flist.gotofileline(filename, lineno)
    255 
    256     def __frame2fileline(self, frame):
    257         code = frame.f_code
    258         filename = code.co_filename
    259         lineno = frame.f_lineno
    260         return filename, lineno
    261 
    262     def cont(self):
    263         self.idb.set_continue()
    264         self.abort_loop()
    265 
    266     def step(self):
    267         self.idb.set_step()
    268         self.abort_loop()
    269 
    270     def next(self):
    271         self.idb.set_next(self.frame)
    272         self.abort_loop()
    273 
    274     def ret(self):
    275         self.idb.set_return(self.frame)
    276         self.abort_loop()
    277 
    278     def quit(self):
    279         self.idb.set_quit()
    280         self.abort_loop()
    281 
    282     def abort_loop(self):
    283         self.root.tk.call('set', '::idledebugwait', '1')
    284 
    285     stackviewer = None
    286 
    287     def show_stack(self):
    288         if not self.stackviewer and self.vstack.get():
    289             self.stackviewer = sv = StackViewer(self.fstack, self.flist, self)
    290             if self.frame:
    291                 stack, i = self.idb.get_stack(self.frame, None)
    292                 sv.load_stack(stack, i)
    293         else:
    294             sv = self.stackviewer
    295             if sv and not self.vstack.get():
    296                 self.stackviewer = None
    297                 sv.close()
    298             self.fstack['height'] = 1
    299 
    300     def show_source(self):
    301         if self.vsource.get():
    302             self.sync_source_line()
    303 
    304     def show_frame(self, stackitem):
    305         self.frame = stackitem[0]  # lineno is stackitem[1]
    306         self.show_variables()
    307 
    308     localsviewer = None
    309     globalsviewer = None
    310 
    311     def show_locals(self):
    312         lv = self.localsviewer
    313         if self.vlocals.get():
    314             if not lv:
    315                 self.localsviewer = NamespaceViewer(self.flocals, "Locals")
    316         else:
    317             if lv:
    318                 self.localsviewer = None
    319                 lv.close()
    320                 self.flocals['height'] = 1
    321         self.show_variables()
    322 
    323     def show_globals(self):
    324         gv = self.globalsviewer
    325         if self.vglobals.get():
    326             if not gv:
    327                 self.globalsviewer = NamespaceViewer(self.fglobals, "Globals")
    328         else:
    329             if gv:
    330                 self.globalsviewer = None
    331                 gv.close()
    332                 self.fglobals['height'] = 1
    333         self.show_variables()
    334 
    335     def show_variables(self, force=0):
    336         lv = self.localsviewer
    337         gv = self.globalsviewer
    338         frame = self.frame
    339         if not frame:
    340             ldict = gdict = None
    341         else:
    342             ldict = frame.f_locals
    343             gdict = frame.f_globals
    344             if lv and gv and ldict is gdict:
    345                 ldict = None
    346         if lv:
    347             lv.load_dict(ldict, force, self.pyshell.interp.rpcclt)
    348         if gv:
    349             gv.load_dict(gdict, force, self.pyshell.interp.rpcclt)
    350 
    351     def set_breakpoint_here(self, filename, lineno):
    352         self.idb.set_break(filename, lineno)
    353 
    354     def clear_breakpoint_here(self, filename, lineno):
    355         self.idb.clear_break(filename, lineno)
    356 
    357     def clear_file_breaks(self, filename):
    358         self.idb.clear_all_file_breaks(filename)
    359 
    360     def load_breakpoints(self):
    361         "Load PyShellEditorWindow breakpoints into subprocess debugger"
    362         pyshell_edit_windows = self.pyshell.flist.inversedict.keys()
    363         for editwin in pyshell_edit_windows:
    364             filename = editwin.io.filename
    365             try:
    366                 for lineno in editwin.breakpoints:
    367                     self.set_breakpoint_here(filename, lineno)
    368             except AttributeError:
    369                 continue
    370 
    371 class StackViewer(ScrolledList):
    372 
    373     def __init__(self, master, flist, gui):
    374         if macosxSupport.isAquaTk():
    375             # At least on with the stock AquaTk version on OSX 10.4 you'll
    376             # get a shaking GUI that eventually kills IDLE if the width
    377             # argument is specified.
    378             ScrolledList.__init__(self, master)
    379         else:
    380             ScrolledList.__init__(self, master, width=80)
    381         self.flist = flist
    382         self.gui = gui
    383         self.stack = []
    384 
    385     def load_stack(self, stack, index=None):
    386         self.stack = stack
    387         self.clear()
    388         for i in range(len(stack)):
    389             frame, lineno = stack[i]
    390             try:
    391                 modname = frame.f_globals["__name__"]
    392             except:
    393                 modname = "?"
    394             code = frame.f_code
    395             filename = code.co_filename
    396             funcname = code.co_name
    397             import linecache
    398             sourceline = linecache.getline(filename, lineno)
    399             import string
    400             sourceline = string.strip(sourceline)
    401             if funcname in ("?", "", None):
    402                 item = "%s, line %d: %s" % (modname, lineno, sourceline)
    403             else:
    404                 item = "%s.%s(), line %d: %s" % (modname, funcname,
    405                                                  lineno, sourceline)
    406             if i == index:
    407                 item = "> " + item
    408             self.append(item)
    409         if index is not None:
    410             self.select(index)
    411 
    412     def popup_event(self, event):
    413         "override base method"
    414         if self.stack:
    415             return ScrolledList.popup_event(self, event)
    416 
    417     def fill_menu(self):
    418         "override base method"
    419         menu = self.menu
    420         menu.add_command(label="Go to source line",
    421                          command=self.goto_source_line)
    422         menu.add_command(label="Show stack frame",
    423                          command=self.show_stack_frame)
    424 
    425     def on_select(self, index):
    426         "override base method"
    427         if 0 <= index < len(self.stack):
    428             self.gui.show_frame(self.stack[index])
    429 
    430     def on_double(self, index):
    431         "override base method"
    432         self.show_source(index)
    433 
    434     def goto_source_line(self):
    435         index = self.listbox.index("active")
    436         self.show_source(index)
    437 
    438     def show_stack_frame(self):
    439         index = self.listbox.index("active")
    440         if 0 <= index < len(self.stack):
    441             self.gui.show_frame(self.stack[index])
    442 
    443     def show_source(self, index):
    444         if not (0 <= index < len(self.stack)):
    445             return
    446         frame, lineno = self.stack[index]
    447         code = frame.f_code
    448         filename = code.co_filename
    449         if os.path.isfile(filename):
    450             edit = self.flist.open(filename)
    451             if edit:
    452                 edit.gotoline(lineno)
    453 
    454 
    455 class NamespaceViewer:
    456 
    457     def __init__(self, master, title, dict=None):
    458         width = 0
    459         height = 40
    460         if dict:
    461             height = 20*len(dict) # XXX 20 == observed height of Entry widget
    462         self.master = master
    463         self.title = title
    464         import repr
    465         self.repr = repr.Repr()
    466         self.repr.maxstring = 60
    467         self.repr.maxother = 60
    468         self.frame = frame = Frame(master)
    469         self.frame.pack(expand=1, fill="both")
    470         self.label = Label(frame, text=title, borderwidth=2, relief="groove")
    471         self.label.pack(fill="x")
    472         self.vbar = vbar = Scrollbar(frame, name="vbar")
    473         vbar.pack(side="right", fill="y")
    474         self.canvas = canvas = Canvas(frame,
    475                                       height=min(300, max(40, height)),
    476                                       scrollregion=(0, 0, width, height))
    477         canvas.pack(side="left", fill="both", expand=1)
    478         vbar["command"] = canvas.yview
    479         canvas["yscrollcommand"] = vbar.set
    480         self.subframe = subframe = Frame(canvas)
    481         self.sfid = canvas.create_window(0, 0, window=subframe, anchor="nw")
    482         self.load_dict(dict)
    483 
    484     dict = -1
    485 
    486     def load_dict(self, dict, force=0, rpc_client=None):
    487         if dict is self.dict and not force:
    488             return
    489         subframe = self.subframe
    490         frame = self.frame
    491         for c in subframe.children.values():
    492             c.destroy()
    493         self.dict = None
    494         if not dict:
    495             l = Label(subframe, text="None")
    496             l.grid(row=0, column=0)
    497         else:
    498             names = dict.keys()
    499             names.sort()
    500             row = 0
    501             for name in names:
    502                 value = dict[name]
    503                 svalue = self.repr.repr(value) # repr(value)
    504                 # Strip extra quotes caused by calling repr on the (already)
    505                 # repr'd value sent across the RPC interface:
    506                 if rpc_client:
    507                     svalue = svalue[1:-1]
    508                 l = Label(subframe, text=name)
    509                 l.grid(row=row, column=0, sticky="nw")
    510                 l = Entry(subframe, width=0, borderwidth=0)
    511                 l.insert(0, svalue)
    512                 l.grid(row=row, column=1, sticky="nw")
    513                 row = row+1
    514         self.dict = dict
    515         # XXX Could we use a <Configure> callback for the following?
    516         subframe.update_idletasks() # Alas!
    517         width = subframe.winfo_reqwidth()
    518         height = subframe.winfo_reqheight()
    519         canvas = self.canvas
    520         self.canvas["scrollregion"] = (0, 0, width, height)
    521         if height > 300:
    522             canvas["height"] = 300
    523             frame.pack(expand=1)
    524         else:
    525             canvas["height"] = height
    526             frame.pack(expand=0)
    527 
    528     def close(self):
    529         self.frame.destroy()
    530