python (3.11.7)

(root)/
lib/
python3.11/
idlelib/
debugger.py
       1  """Debug user code with a GUI interface to a subclass of bdb.Bdb.
       2  
       3  The Idb idb and Debugger gui instances each need a reference to each
       4  other or to an rpc proxy for each other.
       5  
       6  If IDLE is started with '-n', so that user code and idb both run in the
       7  IDLE process, Debugger is called without an idb.  Debugger.__init__
       8  calls Idb with its incomplete self.  Idb.__init__ stores gui and gui
       9  then stores idb.
      10  
      11  If IDLE is started normally, so that user code executes in a separate
      12  process, debugger_r.start_remote_debugger is called, executing in the
      13  IDLE process.  It calls 'start the debugger' in the remote process,
      14  which calls Idb with a gui proxy.  Then Debugger is called in the IDLE
      15  for more.
      16  """
      17  
      18  import bdb
      19  import os
      20  
      21  from tkinter import *
      22  from tkinter.ttk import Frame, Scrollbar
      23  
      24  from idlelib import macosx
      25  from idlelib.scrolledlist import ScrolledList
      26  from idlelib.window import ListedToplevel
      27  
      28  
      29  class ESC[4;38;5;81mIdb(ESC[4;38;5;149mbdbESC[4;38;5;149m.ESC[4;38;5;149mBdb):
      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  
      58  def _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  
      73  def _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  
      85  class ESC[4;38;5;81mDebugger:
      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  
     424  class ESC[4;38;5;81mStackViewer(ESC[4;38;5;149mScrolledList):
     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  
     508  class ESC[4;38;5;81mNamespaceViewer:
     509      "Global/local namespace viewer for debugger GUI."
     510  
     511      def __init__(self, master, title, dict=None):
     512          width = 0
     513          height = 40
     514          if dict:
     515              height = 20*len(dict) # 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(dict)
     537  
     538      dict = -1
     539  
     540      def load_dict(self, dict, force=0, rpc_client=None):
     541          if dict is self.dict 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.dict = None
     548          if not dict:
     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 = dict.keys()
     564              names = sorted(keys_list)
     565              ###
     566              row = 0
     567              for name in names:
     568                  value = dict[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.dict = dict
     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  
     598  if __name__ == "__main__":
     599      from unittest import main
     600      main('idlelib.idle_test.test_debugger', verbosity=2, exit=False)
     601  
     602  # TODO: htest?