debug user code with a gui interface to a subclass of bdb bdb the idb idb and debugger gui instances each need a reference to each other or to an rpc proxy for each other if idle is started with n so that user code and idb both run in the idle process debugger is called without an idb debugger init calls idb with its incomplete self idb init stores gui and gui then stores idb if idle is started normally so that user code executes in a separate process debuggerr startremotedebugger is called executing in the idle process it calls start the debugger in the remote process which calls idb with a gui proxy then debugger is called in the idle for more handle a user stopping or breaking at a line convert frame to a string and send it to gui handle an the occurrence of an exception if inrpccodeframe self setstep return message frame2messageframe self gui interactionmessage frame excinfo def inrpccodeframe determine if debugger is within rpc code if frame fcode cofilename count rpc py return true skip this frame else prevframe frame fback if prevframe is none return false prevname prevframe fcode cofilename if idlelib in prevname and debugger in prevname catch both idlelibdebugger py and idlelibdebuggerr py on both posix and windows return false return inrpccodeprevframe def frame2messageframe the debugger interface this class handles the drawing of the debugger window and the interactions with the underlying debugger session instantiate and draw a debugger window param pyshell an instance of the pyshell window type pyshell class idlelib pyshell pyshell param idb an instance of the idle debugger optional type idb class idlelib debugger idb run the debugger deal with the scenario where we ve already got a program running in the debugger and we want to start another if that is the case our second run was invoked from an event dispatched not from the main event loop but from the nested event loop in interaction below so our stack looks something like this outer main event loop run running program with traces callback to debugger s interaction nested event loop run for second command this kind of nesting of event loops causes all kinds of problems see e g issue 24455 especially when dealing with running as a subprocess where there s all kinds of extra stuff happening in there insert a traceback printstack to check it out by this point we ve already called restartsubprocess in scriptbinding however we also need to unwind the stack back to that outer event loop to accomplish this we return immediately from the nested run abortloop ensures the nested event loop will terminate the debugger s interaction routine completes normally the restartsubprocess will have taken care of stopping the running program which will also let the outer run complete that leaves us back at the outer main event loop at which point our after event can fire and we ll come back to this routine with a clean stack if self nestinglevel 0 self abortloop self root after100 lambda self runargs return try self interacting true return self idb runargs finally self interacting false def closeself eventnone clean up pyshell if user clicked debugger control close widget causes a harmless extra cycle through closedebugger if user toggled debugger from pyshell debug menu now close the debugger control window draw the debugger gui on the screen pyshell self pyshell self flist pyshell flist self root root pyshell root self top top listedtoplevelroot self top wmtitledebug control self top wmiconnamedebug top wmprotocolwmdeletewindow self close self top bindescape self close self bframe bframe frametop self bframe packanchorw self buttons bl self bcont b buttonbframe textgo commandself cont bl appendb self bstep b buttonbframe textstep commandself step bl appendb self bnext b buttonbframe textover commandself next bl appendb self bret b buttonbframe textout commandself ret bl appendb self bret b buttonbframe textquit commandself quit bl appendb for b in bl b configurestatedisabled b packsideleft self cframe cframe framebframe self cframe packsideleft if not self vstack self class vstack booleanvartop self vstack set1 self bstack checkbuttoncframe textstack commandself showstack variableself vstack self bstack gridrow0 column0 if not self vsource self class vsource booleanvartop self bsource checkbuttoncframe textsource commandself showsource variableself vsource self bsource gridrow0 column1 if not self vlocals self class vlocals booleanvartop self vlocals set1 self blocals checkbuttoncframe textlocals commandself showlocals variableself vlocals self blocals gridrow1 column0 if not self vglobals self class vglobals booleanvartop self bglobals checkbuttoncframe textglobals commandself showglobals variableself vglobals self bglobals gridrow1 column1 self status labeltop anchorw self status packanchorw self error labeltop anchorw self error packanchorw fillx self errorbg self error cgetbackground self fstack frametop height1 self fstack packexpand1 fillboth self flocals frametop self flocals packexpand1 fillboth self fglobals frametop height1 self fglobals packexpand1 fillboth if self vstack get self showstack if self vlocals get self showlocals if self vglobals get self showglobals def interactionself message frame infonone self frame frame self status configuretextmessage if info type value tb info try m1 type name except attributeerror m1 s strtype if value is not none try todo redo entire section tries not needed m1 fm1 value except pass bg yellow else m1 tb none bg self errorbg self error configuretextm1 backgroundbg sv self stackviewer if sv stack i self idb getstackself frame tb sv loadstackstack i self showvariables1 if self vsource get self syncsourceline for b in self buttons b configurestatenormal self top wakeup nested main loop tkinter s main loop is not reentrant so use tcl s vwait facility which reenters the event loop until an event handler sets the variable we re waiting on self nestinglevel 1 self root tk call vwait idledebugwait self nestinglevel 1 for b in self buttons b configurestatedisabled self status configuretext self error configuretext backgroundself errorbg self frame none def syncsourcelineself frame self frame if not frame return filename lineno self frame2filelineframe if filename 1 filename1 and os path existsfilename self flist gotofilelinefilename lineno def frame2filelineself frame code frame fcode filename code cofilename lineno frame flineno return filename lineno def contself self idb setcontinue self abortloop def stepself self idb setstep self abortloop def nextself self idb setnextself frame self abortloop def retself self idb setreturnself frame self abortloop def quitself self idb setquit self abortloop def abortloopself self root tk call set idledebugwait 1 def showstackself if not self stackviewer and self vstack get self stackviewer sv stackviewerself fstack self flist self if self frame stack i self idb getstackself frame none sv loadstackstack i else sv self stackviewer if sv and not self vstack get self stackviewer none sv close self fstack height 1 def showsourceself if self vsource get self syncsourceline def showframeself stackitem self frame stackitem0 lineno is stackitem1 self showvariables def showlocalsself lv self localsviewer if self vlocals get if not lv self localsviewer namespaceviewerself flocals locals else if lv self localsviewer none lv close self flocals height 1 self showvariables def showglobalsself gv self globalsviewer if self vglobals get if not gv self globalsviewer namespaceviewerself fglobals globals else if gv self globalsviewer none gv close self fglobals height 1 self showvariables def showvariablesself force0 lv self localsviewer gv self globalsviewer frame self frame if not frame ldict gdict none else ldict frame flocals gdict frame fglobals if lv and gv and ldict is gdict ldict none if lv lv loaddictldict force self pyshell interp rpcclt if gv gv loaddictgdict force self pyshell interp rpcclt def setbreakpointself filename lineno self idb setbreakfilename lineno def clearbreakpointself filename lineno self idb clearbreakfilename lineno def clearfilebreaksself filename self idb clearallfilebreaksfilename def loadbreakpointsself at least on with the stock aquatk version on osx 10 4 you ll get a shaking gui that eventually kills idle if the width argument is specified names sorteddict because of temporary limitations on the dictkeys type not yet public or pickleable have the subprocess to send a list of keys not a dictkeys object sorted will take a dictkeys no subprocess or a list there is also an obscure bug in sorteddict where the interpreter gets into a loop requesting nonexisting dict0 dict1 dict2 etc from the debuggerr dictproxy todo recheck above see debuggerr 159ff debugobj 60 strip extra quotes caused by calling repr on the already repr d value sent across the rpc interface xxx could we use a configure callback for the following todo htest an instance of debugger or proxy thereof handle a user stopping or breaking at a line convert frame to a string and send it to gui when closing debugger window with x in 3 x handle an the occurrence of an exception skip this frame catch both idlelib debugger py and idlelib debugger_r py on both posix and windows return a message string for frame the debugger interface this class handles the drawing of the debugger window and the interactions with the underlying debugger session instantiate and draw a debugger window param pyshell an instance of the pyshell window type pyshell class idlelib pyshell pyshell param idb an instance of the idle debugger optional type idb class idlelib debugger idb if passed a proxy of remote instance run the debugger deal with the scenario where we ve already got a program running in the debugger and we want to start another if that is the case our second run was invoked from an event dispatched not from the main event loop but from the nested event loop in interaction below so our stack looks something like this outer main event loop run running program with traces callback to debugger s interaction nested event loop run for second command this kind of nesting of event loops causes all kinds of problems see e g issue 24455 especially when dealing with running as a subprocess where there s all kinds of extra stuff happening in there insert a traceback print_stack to check it out by this point we ve already called restart_subprocess in scriptbinding however we also need to unwind the stack back to that outer event loop to accomplish this we return immediately from the nested run abort_loop ensures the nested event loop will terminate the debugger s interaction routine completes normally the restart_subprocess will have taken care of stopping the running program which will also let the outer run complete that leaves us back at the outer main event loop at which point our after event can fire and we ll come back to this routine with a clean stack close the debugger and window clean up pyshell if user clicked debugger control close widget causes a harmless extra cycle through close_debugger if user toggled debugger from pyshell debug menu now close the debugger control window draw the debugger gui on the screen todo redo entire section tries not needed nested main loop tkinter s main loop is not reentrant so use tcl s vwait facility which reenters the event loop until an event handler sets the variable we re waiting on lineno is stackitem 1 set a filename lineno breakpoint in the debugger called from self load_breakpoints and ew setbreakpoint load pyshelleditorwindow breakpoints into subprocess debugger at least on with the stock aquatk version on osx 10 4 you ll get a shaking gui that eventually kills idle if the width argument is specified xxx 20 observed height of entry widget names sorted dict because of temporary limitations on the dict_keys type not yet public or pickleable have the subprocess to send a list of keys not a dict_keys object sorted will take a dict_keys no subprocess or a list there is also an obscure bug in sorted dict where the interpreter gets into a loop requesting non existing dict 0 dict 1 dict 2 etc from the debugger_r dictproxy todo recheck above see debugger_r 159ff debugobj 60 repr value strip extra quotes caused by calling repr on the already repr d value sent across the rpc interface xxx could we use a configure callback for the following alas todo htest
import bdb
import os

from tkinter import *
from tkinter.ttk import Frame, Scrollbar

from idlelib import macosx
from idlelib.scrolledlist import ScrolledList
from idlelib.window import ListedToplevel


class Idb(bdb.Bdb):
    "Supply user_line and user_exception functions for Bdb."

    def __init__(self, gui):
        self.gui = gui  
        super().__init__()

    def user_line(self, frame):
        
        if _in_rpc_code(frame):
            self.set_step()
            return
        message = _frame2message(frame)
        try:
            self.gui.interaction(message, frame)
        except TclError:  
            pass

    def user_exception(self, frame, exc_info):
        
        if _in_rpc_code(frame):
            self.set_step()
            return
        message = _frame2message(frame)
        self.gui.interaction(message, frame, exc_info)

def _in_rpc_code(frame):
    "Determine if debugger is within RPC code."
    if frame.f_code.co_filename.count('rpc.py'):
        return True  
    else:
        prev_frame = frame.f_back
        if prev_frame is None:
            return False
        prev_name = prev_frame.f_code.co_filename
        if 'idlelib' in prev_name and 'debugger' in prev_name:
            
            
            return False
        return _in_rpc_code(prev_frame)

def _frame2message(frame):
    
    code = frame.f_code
    filename = code.co_filename
    lineno = frame.f_lineno
    basename = os.path.basename(filename)
    message = f"{basename}:{lineno}"
    if code.co_name != "?":
        message = f"{message}: {code.co_name}()"
    return message


class Debugger:
    
    vstack = None
    vsource = None
    vlocals = None
    vglobals = None
    stackviewer = None
    localsviewer = None
    globalsviewer = None

    def __init__(self, pyshell, idb=None):
        
        if idb is None:
            idb = Idb(self)
        self.pyshell = pyshell
        self.idb = idb  
        self.frame = None
        self.make_gui()
        self.interacting = False
        self.nesting_level = 0

    def run(self, *args):
        
        if self.nesting_level > 0:
            self.abort_loop()
            self.root.after(100, lambda: self.run(*args))
            return
        try:
            self.interacting = True
            return self.idb.run(*args)
        finally:
            self.interacting = False

    def close(self, event=None):
        
        try:
            self.quit()
        except Exception:
            pass
        if self.interacting:
            self.top.bell()
            return
        if self.stackviewer:
            self.stackviewer.close(); self.stackviewer = None
        
        self.pyshell.close_debugger()
        
        self.top.destroy()

    def make_gui(self):
        
        pyshell = self.pyshell
        self.flist = pyshell.flist
        self.root = root = pyshell.root
        self.top = top = ListedToplevel(root)
        self.top.wm_title("Debug Control")
        self.top.wm_iconname("Debug")
        top.wm_protocol("WM_DELETE_WINDOW", self.close)
        self.top.bind("<Escape>", self.close)

        self.bframe = bframe = Frame(top)
        self.bframe.pack(anchor="w")
        self.buttons = bl = []

        self.bcont = b = Button(bframe, text="Go", command=self.cont)
        bl.append(b)
        self.bstep = b = Button(bframe, text="Step", command=self.step)
        bl.append(b)
        self.bnext = b = Button(bframe, text="Over", command=self.next)
        bl.append(b)
        self.bret = b = Button(bframe, text="Out", command=self.ret)
        bl.append(b)
        self.bret = b = Button(bframe, text="Quit", command=self.quit)
        bl.append(b)

        for b in bl:
            b.configure(state="disabled")
            b.pack(side="left")

        self.cframe = cframe = Frame(bframe)
        self.cframe.pack(side="left")

        if not self.vstack:
            self.__class__.vstack = BooleanVar(top)
            self.vstack.set(1)
        self.bstack = Checkbutton(cframe,
            text="Stack", command=self.show_stack, variable=self.vstack)
        self.bstack.grid(row=0, column=0)
        if not self.vsource:
            self.__class__.vsource = BooleanVar(top)
        self.bsource = Checkbutton(cframe,
            text="Source", command=self.show_source, variable=self.vsource)
        self.bsource.grid(row=0, column=1)
        if not self.vlocals:
            self.__class__.vlocals = BooleanVar(top)
            self.vlocals.set(1)
        self.blocals = Checkbutton(cframe,
            text="Locals", command=self.show_locals, variable=self.vlocals)
        self.blocals.grid(row=1, column=0)
        if not self.vglobals:
            self.__class__.vglobals = BooleanVar(top)
        self.bglobals = Checkbutton(cframe,
            text="Globals", command=self.show_globals, variable=self.vglobals)
        self.bglobals.grid(row=1, column=1)

        self.status = Label(top, anchor="w")
        self.status.pack(anchor="w")
        self.error = Label(top, anchor="w")
        self.error.pack(anchor="w", fill="x")
        self.errorbg = self.error.cget("background")

        self.fstack = Frame(top, height=1)
        self.fstack.pack(expand=1, fill="both")
        self.flocals = Frame(top)
        self.flocals.pack(expand=1, fill="both")
        self.fglobals = Frame(top, height=1)
        self.fglobals.pack(expand=1, fill="both")

        if self.vstack.get():
            self.show_stack()
        if self.vlocals.get():
            self.show_locals()
        if self.vglobals.get():
            self.show_globals()

    def interaction(self, message, frame, info=None):
        self.frame = frame
        self.status.configure(text=message)

        if info:
            type, value, tb = info
            try:
                m1 = type.__name__
            except AttributeError:
                m1 = "%s" % str(type)
            if value is not None:
                try:
                   
                    m1 = f"{m1}: {value}"
                except:
                    pass
            bg = "yellow"
        else:
            m1 = ""
            tb = None
            bg = self.errorbg
        self.error.configure(text=m1, background=bg)

        sv = self.stackviewer
        if sv:
            stack, i = self.idb.get_stack(self.frame, tb)
            sv.load_stack(stack, i)

        self.show_variables(1)

        if self.vsource.get():
            self.sync_source_line()

        for b in self.buttons:
            b.configure(state="normal")

        self.top.wakeup()
        
        
        
        self.nesting_level += 1
        self.root.tk.call('vwait', '::idledebugwait')
        self.nesting_level -= 1

        for b in self.buttons:
            b.configure(state="disabled")
        self.status.configure(text="")
        self.error.configure(text="", background=self.errorbg)
        self.frame = None

    def sync_source_line(self):
        frame = self.frame
        if not frame:
            return
        filename, lineno = self.__frame2fileline(frame)
        if filename[:1] + filename[-1:] != "<>" and os.path.exists(filename):
            self.flist.gotofileline(filename, lineno)

    def __frame2fileline(self, frame):
        code = frame.f_code
        filename = code.co_filename
        lineno = frame.f_lineno
        return filename, lineno

    def cont(self):
        self.idb.set_continue()
        self.abort_loop()

    def step(self):
        self.idb.set_step()
        self.abort_loop()

    def next(self):
        self.idb.set_next(self.frame)
        self.abort_loop()

    def ret(self):
        self.idb.set_return(self.frame)
        self.abort_loop()

    def quit(self):
        self.idb.set_quit()
        self.abort_loop()

    def abort_loop(self):
        self.root.tk.call('set', '::idledebugwait', '1')

    def show_stack(self):
        if not self.stackviewer and self.vstack.get():
            self.stackviewer = sv = StackViewer(self.fstack, self.flist, self)
            if self.frame:
                stack, i = self.idb.get_stack(self.frame, None)
                sv.load_stack(stack, i)
        else:
            sv = self.stackviewer
            if sv and not self.vstack.get():
                self.stackviewer = None
                sv.close()
            self.fstack['height'] = 1

    def show_source(self):
        if self.vsource.get():
            self.sync_source_line()

    def show_frame(self, stackitem):
        self.frame = stackitem[0]  
        self.show_variables()

    def show_locals(self):
        lv = self.localsviewer
        if self.vlocals.get():
            if not lv:
                self.localsviewer = NamespaceViewer(self.flocals, "Locals")
        else:
            if lv:
                self.localsviewer = None
                lv.close()
                self.flocals['height'] = 1
        self.show_variables()

    def show_globals(self):
        gv = self.globalsviewer
        if self.vglobals.get():
            if not gv:
                self.globalsviewer = NamespaceViewer(self.fglobals, "Globals")
        else:
            if gv:
                self.globalsviewer = None
                gv.close()
                self.fglobals['height'] = 1
        self.show_variables()

    def show_variables(self, force=0):
        lv = self.localsviewer
        gv = self.globalsviewer
        frame = self.frame
        if not frame:
            ldict = gdict = None
        else:
            ldict = frame.f_locals
            gdict = frame.f_globals
            if lv and gv and ldict is gdict:
                ldict = None
        if lv:
            lv.load_dict(ldict, force, self.pyshell.interp.rpcclt)
        if gv:
            gv.load_dict(gdict, force, self.pyshell.interp.rpcclt)

    def set_breakpoint(self, filename, lineno):
        
        self.idb.set_break(filename, lineno)

    def clear_breakpoint(self, filename, lineno):
        self.idb.clear_break(filename, lineno)

    def clear_file_breaks(self, filename):
        self.idb.clear_all_file_breaks(filename)

    def load_breakpoints(self):
        
        for editwin in self.pyshell.flist.inversedict:
            filename = editwin.io.filename
            try:
                for lineno in editwin.breakpoints:
                    self.set_breakpoint(filename, lineno)
            except AttributeError:
                continue


class StackViewer(ScrolledList):
    "Code stack viewer for debugger GUI."

    def __init__(self, master, flist, gui):
        if macosx.isAquaTk():
            
            
            
            ScrolledList.__init__(self, master)
        else:
            ScrolledList.__init__(self, master, width=80)
        self.flist = flist
        self.gui = gui
        self.stack = []

    def load_stack(self, stack, index=None):
        self.stack = stack
        self.clear()
        for i in range(len(stack)):
            frame, lineno = stack[i]
            try:
                modname = frame.f_globals["__name__"]
            except:
                modname = "?"
            code = frame.f_code
            filename = code.co_filename
            funcname = code.co_name
            import linecache
            sourceline = linecache.getline(filename, lineno)
            sourceline = sourceline.strip()
            if funcname in ("?", "", None):
                item = "%s, line %d: %s" % (modname, lineno, sourceline)
            else:
                item = "%s.%s(), line %d: %s" % (modname, funcname,
                                                 lineno, sourceline)
            if i == index:
                item = "> " + item
            self.append(item)
        if index is not None:
            self.select(index)

    def popup_event(self, event):
        "Override base method."
        if self.stack:
            return ScrolledList.popup_event(self, event)

    def fill_menu(self):
        "Override base method."
        menu = self.menu
        menu.add_command(label="Go to source line",
                         command=self.goto_source_line)
        menu.add_command(label="Show stack frame",
                         command=self.show_stack_frame)

    def on_select(self, index):
        "Override base method."
        if 0 <= index < len(self.stack):
            self.gui.show_frame(self.stack[index])

    def on_double(self, index):
        "Override base method."
        self.show_source(index)

    def goto_source_line(self):
        index = self.listbox.index("active")
        self.show_source(index)

    def show_stack_frame(self):
        index = self.listbox.index("active")
        if 0 <= index < len(self.stack):
            self.gui.show_frame(self.stack[index])

    def show_source(self, index):
        if not (0 <= index < len(self.stack)):
            return
        frame, lineno = self.stack[index]
        code = frame.f_code
        filename = code.co_filename
        if os.path.isfile(filename):
            edit = self.flist.open(filename)
            if edit:
                edit.gotoline(lineno)


class NamespaceViewer:
    "Global/local namespace viewer for debugger GUI."

    def __init__(self, master, title, dict=None):
        width = 0
        height = 40
        if dict:
            height = 20*len(dict) 
        self.master = master
        self.title = title
        import reprlib
        self.repr = reprlib.Repr()
        self.repr.maxstring = 60
        self.repr.maxother = 60
        self.frame = frame = Frame(master)
        self.frame.pack(expand=1, fill="both")
        self.label = Label(frame, text=title, borderwidth=2, relief="groove")
        self.label.pack(fill="x")
        self.vbar = vbar = Scrollbar(frame, name="vbar")
        vbar.pack(side="right", fill="y")
        self.canvas = canvas = Canvas(frame,
                                      height=min(300, max(40, height)),
                                      scrollregion=(0, 0, width, height))
        canvas.pack(side="left", fill="both", expand=1)
        vbar["command"] = canvas.yview
        canvas["yscrollcommand"] = vbar.set
        self.subframe = subframe = Frame(canvas)
        self.sfid = canvas.create_window(0, 0, window=subframe, anchor="nw")
        self.load_dict(dict)

    dict = -1

    def load_dict(self, dict, force=0, rpc_client=None):
        if dict is self.dict and not force:
            return
        subframe = self.subframe
        frame = self.frame
        for c in list(subframe.children.values()):
            c.destroy()
        self.dict = None
        if not dict:
            l = Label(subframe, text="None")
            l.grid(row=0, column=0)
        else:
            
            keys_list = dict.keys()
            names = sorted(keys_list)
            
            row = 0
            for name in names:
                value = dict[name]
                svalue = self.repr.repr(value) 
                
                
                if rpc_client:
                    svalue = svalue[1:-1]
                l = Label(subframe, text=name)
                l.grid(row=row, column=0, sticky="nw")
                l = Entry(subframe, width=0, borderwidth=0)
                l.insert(0, svalue)
                l.grid(row=row, column=1, sticky="nw")
                row = row+1
        self.dict = dict
        
        subframe.update_idletasks() 
        width = subframe.winfo_reqwidth()
        height = subframe.winfo_reqheight()
        canvas = self.canvas
        self.canvas["scrollregion"] = (0, 0, width, height)
        if height > 300:
            canvas["height"] = 300
            frame.pack(expand=1)
        else:
            canvas["height"] = height
            frame.pack(expand=0)

    def close(self):
        self.frame.destroy()


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