Wednesday, July 29, 2009

Wrapping Python methods maintaining the signature (aka wrapping soapmethod)

We lately came across a problem trying to wrap our server side SOAP methods to do some logging.
Looking through the source code of soaplib the problem was clear - it inspects the func_code of the method for it's arguments and we're losing the method signature when wrapping.

Because when you write a wrapper (for logging for example) you generally write a method using *args and **kwargs, for example:


def wrapMethod(f):
def wrapper(*args, **kwargs):
print "do something before"
#call original method
ret=f(*args, **kwargs)
print "do something after"
return ret


So the catch with this is that the signature that the method callers see is different (basically you can match the method's name, but not much more) - you have lost the parameters of the method.
The soapmethod decorator inspects the func_code of the method to see what arguments it receives. So if you wrap it, you basically break it - something like this:


...
descriptor = func(_soap_descriptor=True,klazz=self.__class__)
File "/usr/lib/python2.5/site-packages/soaplib-0.7.2dev_r27-py2.5.egg/soaplib/service.py", line 39, in explainMethod
in_params = [(_inVariableNames.get(param_names[i],param_names[i]),params[i]) for i in range(0,len(params))]
IndexError: tuple index out of range


Our workaround is to create dynamic code that defines a method with the same signature:


#The original wrapper
def wrapMethod(f):
def wrapper(*args, **kwargs):
print "do something before"
#call original method
ret=f(*args, **kwargs)
print "do something after"
return ret


def literalWrapMethod(f, makeWrapper):
#get the method's parmeters
sargs=", ".join(f.func_code.co_varnames[1:f.func_code.co_argcount])

#dynamic code for the wrapper
code="\
def curryF(f):\n\
def %(origname)s(self, %(args)s):\n\
return f(self, %(args)s)\n\
\n\
return X\
"%{"args":sargs, "origname":f.func_name}

#interpret the method's code
exec(code)
#obtain the wrapper for f
wrp=curryF(makeWrapper(f))
return wrp


#Use the wrapper we wanted to use originally
def logSOAP(f):
return literalWrapMethod(f, wrapMethod)


#Demo on how you'd use this
@soapmethod(soap_types.String,
_returns=soap_types.String)
@logSOAP
def doSomething(inparam):
...


This way we make yet another wrapper with the original signature that calls the generic wrapper. You can obviously avoid one layer writing all the code in the dynamically evaluated code block... but I got tired of writing code in a string ;)

No comments: