复制Python函数的签名


像所有Python程序员一样,我正在编写一个最小的博客平台。在我的具体案例中,我正在使用Tornado,MongoDB和我编写的一个实验性MongoDB驱动程序构建我的博客,我很快就会宣布这个驱动程序。我不是构建一个可以创建,编辑和删除博客文章的管理UI,而是依赖于MarsEdit我的博客只是实现了MarsEdit使用的metaWeblog XML-RPC API的部分。为了实现这个API,我使用Josh Marshall的优秀的Tornado-RPC包裹。

使用Tornado-RPC,我声明了我的特定处理程序(例如,MetaWeblog.GetRecentPosts处理程序),Tornado-RPC会检查我的方法的签名,以检查它们在运行时是否接收到正确的参数:

args, varargs, varkw, defaults = inspect.getargspec(func)

这太棒了。但是我的XML-RPC处理程序往往都有类似的签名:

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

我想检查每个处理程序方法中的用户和密码是否正确,而不需要重复大量代码。显而易见的方法是一个装饰器:

@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

很简单,对吧?我的修饰方法检查用户和密码,或者返回身份验证错误,或者执行包装方法。

问题是,一个简单的功能工具。包装当Tornado-RPC使用检查GetArgSpec功能工具。包装可以将包装器的模块,名称,docstring和__dict__更改为包装函数的值,但不会更改包装器的实际方法签名。

受到启发Mock,我找到了这个解决方案:

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)

是的,评估是邪恶的。但是在这种情况下,创建一个新的包装函数,使其具有与包装函数相同的签名是唯一的方法。我的decorator将字符串格式化为:

    lambda self, blogid, user, password, struct, publish:\
        _auth(self, blogid, user, password, struct, publish)

并对其进行评估以创建lambda。这个lambda是最后的包装器。是因为@身份验证decorator将代替包装的函数返回。现在当Tornado-RPC检查GetArgSpec在包装函数上检查其参数时,它认为包装器具有正确的方法签名。