Bottle的插件与view装饰器冲突问题

其实这个问题由来已久,早在2012年就有人在bottle那报告过,但是因为python的inspec库本身的问题,它的wraps函数不能保留被包装函数的参数信息,所以一直不能彻底解决。

问题大致是这样:

@app.get("/")
@view("index")
def get_index():
    return dict(a="a")

上面是一个典型的使用view decorator的请求响应函数。如果我们要在这个请求中加入SQLite插件,则代码就是这样:

from bottle.ext.sqlite import Plugin
app.install(Plugin(dbfile="xxxx"))

@app.get("/") @view("index") def get_index(db): a = some_func(db) return dict(a=a)

但这样的代码在运行中会出错,报告参数db没有值…虽然已经安装了SQLite插件。原因就在于其中@app.get这个route decorator处理的是被view decorator包装过的函数,并不是原始的get_index函数。

通过阅读SQLite插件源码(所有类似的插件都是类似这样实现的),可以发现它是通过inspect库的getargspec来取得原始函数的参数信息,如果其中含有db这个参数,就提供一个SQLite连接给这个参数,否则就略过。

但 是因为有view的存在,它会对get_index作一次再包装,返回一个wrapped的函数,虽然view的实现里已经把这个wrapped的函数加 上了functools.wraps,以保持它的函数名和doc不变,但是参数还是从(db)变成了(*args, **kwargs),所以这时inspect.getargspec()取得的结果就不对了,没有了db参数,所以SQLite插件被略过,然后函数里的 db就取不到值,导致出错。

网上看到的建议(不知道算不算官方建议)有两个方法:

一是用apply参数,形如:

@app.get("/", apply=[view("index")])
def get_index(db):
    a = some_func(db)
    return dict(a=a)

这样一来就把view decorator放到插件route里去了,SQLite插件里的inspect.getargspec()就可以取得get_index的参数信息。

另一种方法是用template参数,如:

@app.get("/", template="index")
def get_index(db):
    a = some_func(db)
    return dict(a=a)

这种方法比上面一种更简单,实际是使用了bottle内置的一个叫template的插件(详见bottle源码)。

不过因为我并不用默认模板,一直都是用mako,要用这两种方法的话都有点麻烦(尤其是template方式,参考下面的两段代码),所以以前都是用自己写的一个类似内置template插件的模板插件(用法与使用默认模板的template参数方式差不多)。

from bottle import mako_view as view

@app.get("/", apply=[view("index.html")]) def get_index(db): a = some_func(db) return dict(a=a)

or

from bottle import MakoTemplate

@app.get("/", template=("index.html", dict(template_adapter=MakoTemplate))) def get_index(db): a = some_func(db) return dict(a=a)

之所以现在来提这个旧问题,是因为在今年的PyCon上听了孔令开的一个演讲提到参数处理的 事情,就请教了他一下,他提供了一个给bottle打补丁的方案,在自定义的wraps里额外保存一份参数信息。群里还有人提供了一些代替 functools.wraps的方案,可以保留函数参数信息。不过我研究了一番以后,觉得都还是不够好。

孔兄的方案虽然比较简单,但是因为额外保存的参数信息并不会被inspect.getargspec()所获得,所以除了自己写的插件以外,别的插件还是取不到参数信息。

而第三方wraps的方法虽然可以解决其它插件问题,但是却需要依赖第三方库,而且同样需要对bottle打补丁以替换原来的wraps,但最让我不能接受的原因是——这些第三方库对此的实现方式是类似eval的方法。

看来bottle之所以三年没有解决这个问题,的确是因为这并不是个容易解决的问题。归根到底还是要怪functools.wraps没有解决函数参数信息保留的问题。

最后说一个刚刚看到的解决方法:使用python3的inspect.signature(),这个可以取得wraps之前的函数参数信息。但是遗憾的是python2.x不支持…

但我会在我自己写的插件里换上这个,至少可以为py3环境提供更方便的使用。

推送到[go4pro.org]