Copying A Python Function's Signature
Like all Python programmers, I'm writing a minimal blogging platform. In my particular case, I'm building my blog using Tornado, MongoDB, and an experimental MongoDB driver I wrote, which I'll announce soon. Rather than build an admin UI where I can create, edit, and delete blog posts, I rely on MarsEdit. My blog simply implements the portion of the metaWeblog XML-RPC API that MarsEdit uses. To implement this API I use Josh Marshall's excellent Tornado-RPC package.
With Tornado-RPC, I declare my particular handlers (e.g., the
metaWeblog.getRecentPosts
handler), and Tornado-RPC introspects my
methods' signatures to check if they're receiving the right arguments at
run time:
args, varargs, varkw, defaults = inspect.getargspec(func)
This is fantastic. But my XML-RPC handlers tend to all have similar signatures:
def metaWeblog_newPost(self, blogid, user, password, struct, publish):
pass
def metaWeblog_editPost(self, postid, user, password, struct, publish):
pass
def metaWeblog_getPost(self, postid, user, password):
pass
I want to check that the user and password are correct in each handler method, without duplicating a ton of code. The obvious approach is a decorator:
@auth
def metaWeblog_newPost(self, blogid, user, password, struct, publish):
pass
def auth(fn):
argspec = inspect.getargspec(fn)
@functools.wraps(fn)
def _auth(*args, **kwargs):
self = args[0]
user = args[argspec.args.index('user')]
password = args[argspec.args.index('password')]
if not check_authentication(user, password):
self.result(xmlrpclib.Fault(
403, 'Bad login/pass combination.'))
else:
return fn(*args, **kwargs)
return _auth
Simple enough, right? My decorated method checks the user and password, and either returns an authentication fault, or executes the wrapped method.
Problem is, a simple functools.wraps()
isn't enough to fool
Tornado-RPC when it inspects my handler methods' signatures using
inspect.getargspec()
. functools.wraps()
can change a wrapper's
module, name, docstring, and __dict__ to the wrapped function's
values, but it doesn't change the wrapper's actual method signature.
Inspired by Mock, I found this solution:
def auth(fn):
argspec = inspect.getargspec(fn)
def _auth(*args, **kwargs):
user = args[argspec.args.index('user')]
password = args[argspec.args.index('password')]
if not check_authentication(user, password):
self.result(xmlrpclib.Fault(403, 'Bad login/pass combination.'))
else:
return fn(*args, **kwargs)
# For tornadorpc to think _auth has the same arguments as fn,
# functools.wraps() isn't enough.
formatted_args = inspect.formatargspec(*argspec)
fndef = 'lambda %s: _auth%s' % (
formatted_args.lstrip('(').rstrip(')'), formatted_args)
fake_fn = eval(fndef, {'_auth': _auth})
return functools.wraps(fn)(fake_fn)
Yes, eval
is evil. But for this case, it's the only way to create a
new wrapper function with the same signature as the wrapped function. My
decorator formats a string like:
lambda self, blogid, user, password, struct, publish:\
_auth(self, blogid, user, password, struct, publish)
And evals it to create a lambda. This lambda is the final wrapper. It's
what the @auth
decorator returns in lieu of the wrapped function. Now
when Tornado-RPC does inspect.getargspec()
on the wrapped function to
check its arguments, it thinks the wrapper has the proper method
signature.