A proxy object that delegates method calls to an instance, but that also calls hooks for that method on the proxy, or for all methods. This can be used to implement logging of all method calls and values on an instance.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | #!/usr/bin/env python
#
# $Source$
# $Id$
#
"""
Proxy objects for any library, that allow you to add hooks before or after
methods on a specific object.
"""
__version__ = "$Revision$"
__author__ = "Martin Blais <blais@furius.ca>"
#===============================================================================
# EXTERNAL DECLARATIONS
#===============================================================================
import types
from pprint import pformat
#===============================================================================
# PUBLIC DECLARATIONS
#===============================================================================
__all__ = ['HookProxy']
#-------------------------------------------------------------------------------
#
class ProxyMethodWrapper:
"""
Wrapper object for a method to be called.
"""
def __init__( self, obj, func, name ):
self.obj, self.func, self.name = obj, func, name
assert obj is not None
assert func is not None
assert name is not None
def __call__( self, *args, **kwds ):
return self.obj._method_call(self.name, self.func, *args, **kwds)
#-------------------------------------------------------------------------------
#
class HookProxy(object):
"""
Proxy object that delegates methods and attributes that don't start with _.
You can derive from this and add appropriate hooks where needed.
Override _pre/_post to do something before/afer all method calls.
Override _pre_<name>/_post_<name> to hook before/after a specific call.
"""
def __init__( self, objname, obj ):
self._objname, self._obj = objname, obj
def __getattribute__( self, name ):
"""
Return a proxy wrapper object if this is a method call.
"""
if name.startswith('_'):
return object.__getattribute__(self, name)
else:
att = getattr(self._obj, name)
if type(att) is types.MethodType:
return ProxyMethodWrapper(self, att, name)
else:
return att
def __setitem__( self, key, value ):
"""
Delegate [] syntax.
"""
name = '__setitem__'
att = getattr(self._obj, name)
pmeth = ProxyMethodWrapper(self, att, name)
pmeth(key, value)
def _call_str( self, name, *args, **kwds ):
"""
Returns a printable version of the call.
This can be used for tracing.
"""
pargs = [pformat(x) for x in args]
for k, v in kwds.iteritems():
pargs.append('%s=%s' % (k, pformat(v)))
return '%s.%s(%s)' % (self._objname, name, ', '.join(pargs))
def _method_call( self, name, func, *args, **kwds ):
"""
This method gets called before a method is called.
"""
# pre-call hook for all calls.
try:
prefunc = getattr(self, '_pre')
except AttributeError:
pass
else:
prefunc(name, *args, **kwds)
# pre-call hook for specific method.
try:
prefunc = getattr(self, '_pre_%s' % name)
except AttributeError:
pass
else:
prefunc(*args, **kwds)
# get real method to call and call it
rval = func(*args, **kwds)
# post-call hook for specific method.
try:
postfunc = getattr(self, '_post_%s' % name)
except AttributeError:
pass
else:
postfunc(*args, **kwds)
# post-call hook for all calls.
try:
postfunc = getattr(self, '_post')
except AttributeError:
pass
else:
postfunc(name, *args, **kwds)
return rval
#===============================================================================
# TEST
#===============================================================================
def test():
import sys
class Foo:
def foo( self, bli ):
print ' (running foo -> %s)' % bli
return 42
class BabblingFoo(HookProxy):
"Proxy for Foo."
def _pre( self, name, *args, **kwds ):
print >> sys.stderr, \
"LOG :: %s" % self._call_str(name, *args, **kwds)
def _post( self, name, *args, **kwds ):
print 'after all'
def _pre_foo( self, *args, **kwds ):
print 'before foo...'
def _post_foo( self, *args, **kwds ):
print 'after foo...'
f = BabblingFoo('f', Foo())
print 'rval = %s' % f.foo(17)
# try calling non-existing method
try:
f.nonexisting()
raise RuntimeError
except AttributeError:
pass
if __name__ == '__main__':
test()
|
I needed the ability to "trace" all function calls made to a particular object instance, in order to be able to "replay" them later in the same order and with the same values. (This was useful for testing, I have tests that are generated with randomness and I wanted to be able to reproduce the random test run exactly when it found a bug.)
So I wrote this little recipe, which allows you to create a proxy object to any existing instance, with hooks for particular method calls, or for all method calls.
Derive from HookProxy and add the specific hooks you need to do logging the way you want it, and then use an instance of that in-place for your object. There is a simple example in the code. You could do other stuff in the hooks. (This idea is vaguely similar to Emacs's "advice" package, i.e. adding code to be run before/after an existing function.)
(Note: there is a convenience method in HookProxy that formats the arguments and keywords in a way that they could be re-executed later, you can use that to do logging of the calls made on the proxied-instance.)
(Any improvements/comments welcome, I "cooked" this up late at night without looking into all the possibilities but it has been tested and runs in my tests.)
a subtle bug, and a fix. In all the method calls that take args and *kwds as arguments, you should replace "name" by "_name" and "func" by "_func" in the formal parameters. Otherwise if the delegated method already has a "name" parameter, then the function get "name" twice: once with the normal actual parameter, and once in the "kwds" dict, and this results in::
TypeError: _method_call() got multiple values for keyword argument 'name'
Using _name fixes the common case (hopefully you don't use "_name" for your formal parameters).
see also aspects. You might be interested in the following module which is more generic as it does aspect-oriented programming in Python.
http://www.logilab.org/projects/aspects
one more improvement. I improved my copy with a small but useful improvement: when calling the _post and _post_ functions, I pass the return value from the delegated call.
here is the diff
different approach. indeed, very interesting stuff, thx for the pointer
the difference between the two could be summarized like this:
logilab-aspects modifies the original object methods, setting them to wrapped methods, whereas hookproxy only keeps a reference to the original object
hookproxy is a simple one-file solution, logilab-aspects is more involved, and more powerful. (i suppose if you like simplicity hookproxy still has some value.)
(i don't quite see how logilab-aspects is more generic, plz define "generic")
new version. damnit, i'm getting ahead of myself. what we need for this cookbook thing is a svn repository...
there are some bugs in the diffs sent. also, i added a way to catch exceptions for all method calls. i will write some more tests and send the new versions after some more testing.