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