[Python-checkins] r47145 - in sandbox/trunk/pdb: README.txt mpdb.py mthread.py test/test_mpdb.py

matt.fleming python-checkins at python.org
Wed Jun 28 14:48:54 CEST 2006


Author: matt.fleming
Date: Wed Jun 28 14:48:52 2006
New Revision: 47145

Modified:
   sandbox/trunk/pdb/README.txt
   sandbox/trunk/pdb/mpdb.py
   sandbox/trunk/pdb/mthread.py
   sandbox/trunk/pdb/test/test_mpdb.py
Log:
More work in getting breakpoints working per-thread.


Modified: sandbox/trunk/pdb/README.txt
==============================================================================
--- sandbox/trunk/pdb/README.txt	(original)
+++ sandbox/trunk/pdb/README.txt	Wed Jun 28 14:48:52 2006
@@ -46,5 +46,8 @@
   at the moment 'info target' only tells the user whether they are local
   or remote, and not whether they are the local/remote server or 
   local/remote client.
-
+* We can switch between threads (like in gdb) with 'thread <thread id>', but
+  this currently has _no_ effect. We should at some point be able to switch
+  threads and from them on not have to use the 'thread apply <tid> <cmd>' 
+  syntax, but just '<cmd>' which would be applied to the current thread.
 

Modified: sandbox/trunk/pdb/mpdb.py
==============================================================================
--- sandbox/trunk/pdb/mpdb.py	(original)
+++ sandbox/trunk/pdb/mpdb.py	Wed Jun 28 14:48:52 2006
@@ -27,6 +27,10 @@
 
 line_prefix = '\n-> '
 
+class Exit(Exception):
+    """ Causes a debugger to exit immediately. """
+    pass
+
 class MPdb(pydb.Pdb):
     """ This class extends the command set and functionality of the
     Python debugger and provides support for,
@@ -50,12 +54,15 @@
         self.orig_stdin = self.stdin
         self.prompt = '(MPdb)'
         self.target = 'local'  # local connections by default
+        self.lastcmd = ''
         self.connection = None
         self.debug_thread = False
-        self.waiter = threading.Event()
-        self.tracers = []
-        self.threads = []
-        self.current_thread = threading.currentThread()
+        # We don't trace the MainThread, so the tracer for the main thread is
+        # None.
+        self.tracers = [None]
+        self.threads = [threading.currentThread()]
+        self.current_thread = self.threads[0]
+
         self._info_cmds.append('target')
         self._info_cmds.append('thread')
 
@@ -84,6 +91,9 @@
         """ All commands in 'line' are sent across this object's connection
         instance variable.
         """
+        if not line:
+            # Execute the previous command
+            line = self.lastcmd
         # This is the simplest way I could think of to do this without
         # breaking any of the inherited code from pydb/pdb. If we're a
         # remote client, always call 'rquit' (remote quit) when connected to
@@ -104,6 +114,7 @@
         while self.local_prompt not in ret:
             ret += self.connection.readline()
         self.msg_nocr(ret)
+        self.lastcmd = line
         return
 
     def msg_nocr(self, msg, out=None):
@@ -134,11 +145,31 @@
                 self.errmsg('Thread debugging is not on.')
                 return
             # We need some way to remove old thread instances
-            self.msg(self.threads)
+            for t in self.threads:
+                if t == self.current_thread:
+                    self.msg('* %d %s' % (self.threads.index(t)+1, t))
+                else:
+                    self.msg('  %d %s' % (self.threads.index(t)+1, t))
             return
         else:
             pydb.Pdb.do_info(self, arg)
 
+    def help_info(self, *args):
+        """Extends pydb help_info command. """
+        self.subcommand_help('info', getattr(self,'help_info').__doc__,
+                             self._info_cmds, self.info_helper, args[0])
+
+    def info_helper(self, cmd, label=False):
+        """Extends pydb info_helper() to give info about a single Mpdb
+        info extension."""
+        if label:
+            self.msg_nocr("info %s --" % cmd)
+        if 'target'.startswith(cmd):
+            self.msg("Names of targets and files being debugged")
+        elif 'thread'.startswith(cmd):
+            self.msg('Information about active and inactive threads.')
+        else:
+            pydb.Pdb.info_helper(self, cmd)
 
     def do_set(self, arg):
         """ Extends pydb do_set() to allow setting thread debugging. """
@@ -167,7 +198,7 @@
 
         self.threads.append(threading.currentThread())
         self.msg('New thread: %s' % self.threads[-1])
-        m = MTracer(self.breaks, self.stdout)
+        m = MTracer(self.breaks, self.filename, self.stdout)
         self.tracers.append(m)
         sys.settrace(m.trace_dispatch)
 
@@ -345,7 +376,7 @@
 is aborted.
 """
         if self.target == 'local':
-            self.errmsg('Connected locally, cannot remotely quit')
+            self.errmsg('Connected locally; cannot remotely quit')
             return
         self._rebind_output(self.orig_stdout)
         self._rebind_input(self.orig_stdin)
@@ -356,7 +387,7 @@
         self.msg('Exiting remote debugging...')
         self.target = 'local'
         self.do_quit(None)
-
+        raise Exit
 
     def do_thread(self, arg):
         """Use this command to switch between threads.
@@ -369,30 +400,58 @@
 Type "help thread" followed by thread subcommand name for full documentation.
 Command name abbreviations are allowed if unambiguous.
 """
+        if not self.debug_thread:
+            self.errmsg('Thread debugging not on.')
+            return
         args = arg.split()
         if len(args) == 0:
-            self.msg('Current thread is %s' % self.current_thread)
-            return
-        if 'apply'.startswith(args[0]):
-            if len(args) < 2:
-                self.errmsg('Please specify a thread ID')
-                return
-            if len(args) < 3:
-                self.errmsg('Please specify a command following the thread' \
-                            + ' ID')
-                return
-            # These should always be in sync
-            if len(self.threads) == 0:
-                self.errmsg('No threads')
-                return
-            t = self.threads[int(args[1])-1]
-            t_tracer = self.tracers[int(args[1])-1]
-            func = args[2]
-            cmd = eval('t.' + func)
+            self.msg('Current thread is %d (%s)' % \
+                     (self.threads.index(self.current_thread)+1,
+                      self.current_thread))
+            return
+        if len(args) < 2:
+            if args[0].isdigit():
+                # XXX Switch to a different thread, although this doesn't
+                # actually do anything yet.
+                t_num = int(args[0])-1
+                if t_num > len(self.threads):
+                    self.errmsg('Thread ID %d not known.' % t_num+1)
+                    return
+                self.current_thread = self.threads[t_num]
+                self.msg('Switching to thread %d (%s)' % (t_num+1, \
+                                                          self.current_thread))
+                return 
+            self.errmsg('Please specify a Thread ID')
+            return
+        if len(args) < 3:
+            self.errmsg('Please specify a command following the thread' \
+                        + ' ID')
+            return
+        if len(self.threads) == 0:
+            self.errmsg('No threads')
+            return
+        if len(self.threads) < int(args[1]):
+            self.errmsg('Thread ID %d not known.' % int(args[1]))
+            return
+        # These should always be in sync
+        t = self.threads[int(args[1])-1]
+        t_tracer = self.tracers[int(args[1])-1]
+        func = args[2]
+        if len(args) > 2:
+            str_params = ""
+            for w in args[3:]:
+                str_params += w
+            str_params.rstrip()
+            eval('t_tracer.do_' + func + '(str_params)')
+            #except AttributeError:
+            #   self.errmsg('No such thread subcommand')
+            #   return
+        else:
             try:
-                result = cmd(args[3:])
+                eval('t_tracer.do_'+func+'()')
             except AttributeError:
-                self.errmsg('No such thread subcommand')
+                self.errmsg('Undefined thread apply subcommand "%s".' \
+                            % args[0])
                 return
 
 def pdbserver(addr, args):
@@ -405,10 +464,12 @@
     m.mainpyfile = mainpyfile
     m.do_pdbserver(addr)
     while True:
-        m._runscript(mainpyfile)
-        if m._user_requested_quit:
+        try:
+            m._runscript(mainpyfile)
+        except pydb.gdb.Restart:
+            m.msg('Restarting')
+        except Exit:
             break
-    sys.exit()
 
 def target(addr):
     """ Connect this debugger to a pdbserver at 'addr'. 'addr' is
@@ -431,8 +492,10 @@
     opts, args = parse_opts()
     if opts.target:
         target(opts.target)
+        sys.exit()
     elif opts.pdbserver:
         pdbserver(opts.pdbserver, args)
+        sys.exit()
     else:
         if not opts.scriptname:
             if not args:
@@ -455,6 +518,9 @@
             # In most cases SystemExit does not warrant a post-mortem session.
             mpdb.msg("The program exited via sys.exit(). " + \
                   "Exit status:",sys.exc_info()[1])
+        except Exit:
+            # This exception raised when we disconnect from a remote session
+            pass
         except:
             mpdb.msg(traceback.format_exc())
             mpdb.msg("Uncaught exception. Entering post mortem debugging")

Modified: sandbox/trunk/pdb/mthread.py
==============================================================================
--- sandbox/trunk/pdb/mthread.py	(original)
+++ sandbox/trunk/pdb/mthread.py	Wed Jun 28 14:48:52 2006
@@ -1,6 +1,8 @@
 """ This file contains all code for allowing the debugging of threads. """
 
 from bdb import Breakpoint
+import linecache
+import inspect
 import os
 import sys
 import threading
@@ -11,16 +13,17 @@
     which is useful, for instance, if a breakpoint occurs inside
     a thread's run() method.
     """
-    def __init__(self, breaks=None, stdout=None):
+    def __init__(self, breaks={}, filename=None, stdout=None):
         self.thread = threading.currentThread()
         if stdout is None:
             stdout = sys.stdout
         self.out = stdout
         # Each tracer instance must keep track of its own breakpoints
-        if breaks is None:
-            breaks = {}
         self.breaks = breaks
         self.fncache = {}
+        self.filename = filename
+        self.lineno = 0
+        self.curframe = None
 
     def canonic(self, filename):
         if filename == "<" + filename[1:-1] + ">":
@@ -37,25 +40,215 @@
         if 'break'.startswith(args[0]):
             return self.breaks
     
-    def _get_break(self, filename, lineno):
+    def get_break(self, filename, lineno):
         """ Return the breakpoint at [filename:]lineno. """
         filename = self.canonic(filename)
         return filename in self.breaks and \
                lineno in self.breaks[filename]
 
-    def _get_breaks(self, filename, lineno):
+    def get_breaks(self, filename, lineno):
         """ Return all the breakpoints set in this thread. """
         filename = self.canonic(filename)
         return filename in self.breaks and \
                lineno in self.breaks[filename] and \
                Breakpoint.bplist[filename, lineno] or []
         
-    def _set_break(self, filename, lineno, temporary=0, cond = None,
+    def set_break(self, filename, lineno, temporary=0, cond = None,
                   funcname=None):
         """ Set a breakpoint in this thread. """
-        pass
+
+        # Derived classes and clients can call the following methods
+        # to manipulate breakpoints.  These methods return an
+        # error message is something went wrong, None if all is well.
+        # Set_break prints out the breakpoint line and file:lineno.
+        # Call self.get_*break*() to see the breakpoints or better
+        # for bp in Breakpoint.bpbynumber: if bp: bp.bpprint().
+
+        filename = self.canonic(filename)
+        import linecache # Import as late as possible
+        line = linecache.getline(filename, lineno)
+        if not line:
+            return 'Line %s:%d does not exist' % (filename,
+                                   lineno)
+        if not filename in self.breaks:
+            self.breaks[filename] = []
+        list = self.breaks[filename]
+        if not lineno in list:
+            list.append(lineno)
+        bp = Breakpoint(filename, lineno, temporary, cond, funcname)
+
+    def do_break(self, arg=None):
+        """ thread-specific breakpoint information. """
+        # XXX For now we don't support temporary breakpoints in threads
+        temporary = cond = False
+        if arg is None:
+            if self.lineno is None:
+                lineno = max(1, inspect.lineno(self.curframe))
+            else:
+                lineno = self.lineno + 1
+            filename = self.curframe.f_code.co_filename
+        else:
+            filename = None
+            lineno = None
+            comma = arg.find(',')
+            if comma > 0:
+                # parse stuff after comma: "condition"
+                cond = arg[comma+1:].lstrip()
+                arg = arg[:comma].rstrip()
+            (funcname, filename, lineno) = self.__parse_filepos(arg)
+            if lineno is None: return
+            
+        # FIXME This default setting doesn't match that used in
+        # do_clear. Perhaps one is non-optimial.
+        if not filename:
+            filename = self.defaultFile()
+
+        # Check for reasonable breakpoint
+        line = self.checkline(filename, lineno)
+        if line:
+            # now set the break point
+            # Python 2.3.5 takes 5 args rather than 6.
+            # There is another way in configure to test for the version,
+            # but this works too.
+            try:
+               err = self.set_break(filename, line, temporary, cond, funcname)
+            except TypeError:
+               err = self.set_break(filename, line, temporary, cond)
+
+            if err: print >> self.out, err
+            else:
+                bp = self.get_breaks(filename, line)[-1]
+                print >> self.out, "Breakpoint %d set in file %s, line %d." \
+                         % (bp.number, self.filename(bp.file), bp.line)
+
+    def __parse_filepos(self, arg):
+        """__parse_filepos(self,arg)->(fn, filename, lineno)
+        
+        Parse arg as [filename:]lineno | function
+        Make sure it works for C:\foo\bar.py:12
+        """
+        colon = arg.rfind(':')
+        if colon >= 0:
+            filename = arg[:colon].rstrip()
+            f = self.lookupmodule(filename)
+            if not f:
+                print >> self.out, "%s not found from sys.path" % \
+                            self._saferepr(filename)
+                return (None, None, None)
+            else:
+                filename = f
+                arg = arg[colon+1:].lstrip()
+            try:
+                lineno = int(arg)
+            except ValueError, msg:
+                print >> self.out, 'Bad lineno: %s' % str(arg)
+                return (None, filename, None)
+            return (None, filename, lineno)
+        else:
+            # no colon; can be lineno or function
+            return self.__get_brkpt_lineno(arg)
+
+    def __get_brkpt_lineno(self, arg):
+        """__get_brkpt_lineno(self,arg)->(filename, file, lineno)
+
+        See if arg is a line number or a function name.  Return what
+        we've found. None can be returned as a value in the triple."""
+        funcname, filename = (None, None)
+        try:
+            # First try as an integer
+            lineno = int(arg)
+            filename = self.curframe.f_code.co_filename
+        except ValueError:
+            try:
+                func = eval(arg, self.curframe.f_globals,
+                            self.curframe.f_locals)
+            except:
+                func = arg
+            try:
+                if hasattr(func, 'im_func'):
+                    func = func.im_func
+                code = func.func_code
+                #use co_name to identify the bkpt (function names
+                #could be aliased, but co_name is invariant)
+                funcname = code.co_name
+                lineno = code.co_firstlineno
+                filename = code.co_filename
+            except:
+                # last thing to try
+                (ok, filename, ln) = self.lineinfo(arg)
+                if not ok:
+                    print >> self.out, 'The specified object %s is not' % \
+                          str(repr(arg)), 
+                    print >> self.out, ' a function, or not found' \
+                                 +' along sys.path or no line given.'
+
+                    return (None, None, None)
+                funcname = ok # ok contains a function name
+                lineno = int(ln)
+        return (funcname, filename, lineno)
+
+    def lineinfo(self, identifier):
+        failed = (None, None, None)
+        # Input is identifier, may be in single quotes
+        idstring = identifier.split("'")
+        if len(idstring) == 1:
+            # not in single quotes
+            id = idstring[0].strip()
+        elif len(idstring) == 3:
+            # quoted
+            id = idstring[1].strip()
+        else:
+            return failed
+        if id == '': return failed
+        parts = id.split('.')
+        # Protection for derived debuggers
+        if parts[0] == 'self':
+            del parts[0]
+            if len(parts) == 0:
+                return failed
+        # Best first guess at file to look at
+        fname = self.defaultFile()
+        if len(parts) == 1:
+            item = parts[0]
+        else:
+            # More than one part.
+            # First is module, second is method/class
+            f = self.lookupmodule(parts[0])
+            if f:
+                fname = f
+            item = parts[1]
+        answer = find_function(item, fname)
+        return answer or failed
+
+    def defaultFile(self):
+        """Produce a reasonable default."""
+        filename = self.curframe.f_code.co_filename
+        # Consider using is_exec_stmt(). I just don't understand
+        # the conditions under which the below test is true.
+        if filename == '<string>' and self.mainpyfile:
+            filename = self.mainpyfile
+        return filename
+
+    def checkline(self, filename, lineno):
+        """Check whether specified line seems to be executable.
+
+        Return `lineno` if it is, 0 if not (e.g. a docstring, comment, blank
+        line or EOF). Warning: testing is not comprehensive.
+        """
+        line = linecache.getline(filename, lineno)
+        if not line:
+            print >>self.out, 'End of file'
+            return 0
+        line = line.strip()
+        # Don't allow setting breakpoint at a blank line
+        if (not line or (line[0] == '#') or
+             (line[:3] == '"""') or line[:3] == "'''"):
+            print >>self.out, '*** Blank or comment'
+            return 0
+        return lineno
     
     def trace_dispatch(self, frame, event, arg):
+        self.curframe = frame
         if event == 'line':
             print >> self.out, self.thread.getName(),'*** line'
             return self.trace_dispatch
@@ -79,12 +272,3 @@
             return self.trace_dispatch
         print 'bdb.Bdb.dispatch: unknown debugging event:', repr(event)
         return self.trace_dispatch
-
-
-            
-
-
-    
-
-        
-

Modified: sandbox/trunk/pdb/test/test_mpdb.py
==============================================================================
--- sandbox/trunk/pdb/test/test_mpdb.py	(original)
+++ sandbox/trunk/pdb/test/test_mpdb.py	Wed Jun 28 14:48:52 2006
@@ -100,8 +100,6 @@
 
         self.assertEquals(errmsg, line)
 
-        server.disconnect()
-
     def testRebindOutput(self):
         """ Test rebinding output. """
         self.server = MPdb()
@@ -138,28 +136,38 @@
 
         self.client1 = MPdbTest()
         connect_to_target(self.client1)
+
+        # Turn on thread debugging
+        self.client1.onecmd('set thread')
+        line = self.client1.lines[0]
+        self.assertEquals('Thread debugging on\n', line)
+        
         # Thread with no commands should return current thread
         self.client1.onecmd('thread')
-        assert 'MainThread' in self.client1.lines[0]
+        assert 'MainThread' in self.client1.lines[1]
 
         # 'thread apply' without thread ID should return an error message
         self.client1.onecmd('thread apply')
-        line = self.client1.lines[1]
-        self.assertEquals('*** Please specify a thread ID\n', line)
+        line = self.client1.lines[2]
+        errmsg = '*** Please specify a Thread ID\n'
+        self.assertEquals(errmsg, line)
 
         # Need a command to actually apply to a thread
         self.client1.onecmd('thread apply 49843')
-        line = self.client1.lines[2]
+        line = self.client1.lines[3]
         errmsg = '*** Please specify a command following the thread ID\n'
         self.assertEquals(errmsg, line)
 
         # We've still not started any threads
         self.client1.onecmd('thread apply 2 info break')
-        line = self.client1.lines[3]
-        errmsg = '*** No threads\n'
+        line = self.client1.lines[4]
+        errmsg = '*** Thread ID 2 not known.\n'
         self.assertEquals(errmsg, line)
-        
-        server.disconnect()
+
+        self.client1.onecmd('thread')
+        line = self.client1.lines[5]
+        msg = 'Current thread is 1 (<_MainThread(MainThread, started)>)\n'
+        self.assertEquals(msg, line)
 
 def test_main():
     test_support.run_unittest(TestRemoteDebugging)


More information about the Python-checkins mailing list