A trace mixin for Python

Posted: Thu, 1 December 2005 | permalink | No comments

Although I am by means an expert Python programmer, I do dabble -- at the moment, I'm dabbling fairly heavily in a project for work. I had need, as you do on occasion, for a call trace (I called this method, it returned X, I then called this other method, it returned Y, and so on). Astoundingly, I got no answer when I asked on #python, as I would have expected this sort of thing to be a gimme -- even possibly including it as a core language feature. But nary an answer was forthcoming. Searches for mixins or magic function calls which might solve my problem were similarly fruitless.

I resigned myself to manually sticking print wrappers around the function calls of interest, but that got old real fast (especially after I spent about a half-hour chasing my tail because I'd forgotten to return the return value after I printed it).

As usual, my subconscious saved the day. On the train home this afternoon, I suddenly had a brainwave, and the code which I've reproduced below appeared almost fully-formed at my fingertips. It's times like this that I really enjoy programming.

Now, of course, someone will pop up and say "oh here it is over here", or "you just set the __trace_me_baby__ attribute on your object", but I can console myself with the fact that I know a lot more about grubby Python internals than I did before.

The code: (a pre-saved version is also available).

# tracemixin.py
#
# A python mixin to trace the calling of methods.
#
# Copyright (C) 2005 Matthew Palmer 
#
#   This program is free software; you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 2 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program; if not, write to the Free Software
#   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
#   or see http://www.gnu.org/licenses/gpl.html
#   

"""
A mixin class to permit the tracing of method calls on an object.

This class, when mixed in, overrides the default operation of method calls
so that requests to call methods which have been listed in the
self._traced_methods list will be printed with their arguments and return
values.  Also included is a simple call depth indication, using indents.

Tracing of particular methods can be turned on and off on a per-object basis
by simply including or removing the method's name in the _traced_methods
list on the particular object.  Whole classes can be traced by setting
the list as a class attribute.

To include this class, simply make it the first class name in the list of
parent classes when defining a new class, like so:

class Something(TraceMixin, RealParentClass):

You could probably leave the TraceMixin in your production code, although
it'll slow execution down by a bit.

Complete usage example:

from tracemixin import TraceMixin

class RandomTask(TraceMixin):
	def x(self):
		print "X!"
	
	def y(self, arg):
		print "%s Y!" % arg
		self.x()

x = RandomTask()

x.x()
x._traced_methods = ['x', 'y']
x.y('why?')

Output:

X!
::y(why?)
why? Y!
  ::x()
X!
  =>None
=>None

"""

class TraceMixin(object):
	_traced_methods = []
	_depth = 0
	def __getattribute__(self, attr):
		if attr in object.__getattribute__(self, '_traced_methods'):
			self.method = attr
			attr = '_trace_proxy'

		return object.__getattribute__(self, attr)
	
	def _trace_proxy(self, *args):
		realmethod = object.__getattribute__(self, self.method)
		if realmethod and realmethod.__class__ == self._trace_proxy.__class__:
			print "%s::%s(%s)" % (self._depth*'  ', self.method, ', '.join(args))
			self._depth += 1
			rv = realmethod(*args)
			self._depth -= 1
			print "%s=>%s" % (self._depth*'  ', rv)
			return rv
		else:
			raise AttributeError

Post a comment

All comments are held for moderation; markdown formatting accepted.

This is a honeypot form. Do not use this form unless you want to get your IP address blacklisted. Use the second form below for comments.
Name: (required)
E-mail: (required, not published)
Website: (optional)
Name: (required)
E-mail: (required, not published)
Website: (optional)