Copying A Python Function’s Signature
Join the DZone community and get the full member experience.
Join For FreeLike 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.
Published at DZone with permission of A. Jesse Jiryu Davis, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments