• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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