[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