插件开发指南¶
这份指南介绍了插件的API,以及如何编写自己的插件。我建议先阅读 插件 这一部分,再看这份指南。 可用插件列表 这里也有一些实际的例子。
注解
这是一份初稿。如果你发现了任何错误,或某些部分解释的不够清楚,请通过 邮件列表 或 bug report 告知。
插件工作方式:基础知识¶
插件的API是通过Python的 修饰器 来实现的。简单来说,一个插件就是应用在route回调函数上的修饰器。
这只是一个简化。插件可以做的不仅仅是修饰路由回调,但它是一个很好的起点。让我们看看一些代码:
from bottle import response, install
import time
def stopwatch(callback):
def wrapper(*args, **kwargs):
start = time.time()
body = callback(*args, **kwargs)
end = time.time()
response.headers['X-Exec-Time'] = str(end - start)
return body
return wrapper
install(stopwatch)
这个插件计算每次请求的响应时间,并在响应头中添加了 X-Exec-Time
字段。如你所见,插件返回了一个wrapper函数,由它来调用原先的回调函数。这就是修饰器的常见工作方式了。
最后一行,将该插件安装到Bottle的默认应用里面。这样,应用中的所有route都会应用这个插件了。就是说,每次请求都会调用 stopwatch()
,更改了route的默认行为。
插件是按需加载的,就是在route第一次被访问的时候加载。为了在多线程环境下工作,插件应该是线程安全的。在大多数情况下,这都不是一个问题,但务必提高警惕。
一旦route中使用了插件后,插件中的回调函数会被缓存起来,接下来都是直接使用缓存中的版本来响应请求。意味着每个route只会请求一次插件。在应用的插件列表变化的时候,这个缓存会被清空。你的插件应当可以多次修饰同一个route。
这种修饰器般的API受到种种限制。你不知道route或相应的应用对象是如何被修饰的,也不知道如何有效地存储那些在route之间共享的数据。但别怕!插件不仅仅是修饰器函数。只要一个插件是callable的或实现了一个扩展的API(后面会讲到),Bottle都可接受。扩展的API给你更多的控制权。
插件API¶
Plugin
类不是一个真正的类(你不能从bottle中导入它),它只是一个插件需要实现的接口。只要一个对象实现了以下接口,Bottle就认可它作为一个插件。
- class Plugin(object)¶
插件应该是callable的,或实现了
apply()
方法。如果定义了apply()
方法,那么会优先调用,而不是直接调用插件。其它的方法和属性都是可选的。- name¶
Bottle.uninstall()
方法和Bottle.route()
中的 skip 参数都接受一个与名字有关的字符串,对应插件或其类型。只有插件中有一个name属性的时候,这才会起作用。
- setup(self, app)¶
插件被安装的时候调用(见
Bottle.install()
)。唯一的参数是相应的应用对象。
- __call__(self, callback)¶
如果没有定义
apply()
方法,插件本身会被直接当成一个修饰器使用(译者注:Python的Magic Method,调用一个类即是调用类的__call__函数),应用到各个route。唯一的参数就是其所修饰的函数。这个方法返回的东西会直接替换掉原先的回调函数。如果无需如此,则直接返回未修改过的回调函数即可。
- apply(self, callback, route)¶
如果存在,会优先调用,而不调用
__call__()
。额外的 route 参数是Route
类的一个实例,提供很多该route信息和上下文。详见 Route上下文 。
- close(self)¶
插件被卸载或应用关闭的时候被调用,详见
Bottle.uninstall()
或Bottle.close()
。
Plugin.setup()
方法和 Plugin.close()
方法 不 会被调用,如果插件是通过 Bottle.route()
方法来应用到route上面的,但会在安装插件的时候被调用。
插件API的改动¶
插件的API还在不断改进中。在Bottle 0.10版本中的改动,定位了route上下文字典中已确定的问题。为了保持对0.9版本插件的兼容,我们添加了一个可选的 Plugin.api
属性,告诉Bottle使用哪个版本的API。API之间的不同点总结如下。
Bottle 0.9 API 1 (无
Plugin.api
属性)0.9文档中描述的原始插件API。
Bottle 0.10 API 2 (
Plugin.api
属性为2)The context parameter of the
Plugin.apply()
method is now an instance ofRoute
instead of a context dictionary.
Route上下文¶
Route
的实例被传递给 Plugin.apply()
函数,以提供更多该route的相关信息。最重要的属性总结如下。
属性 |
描述 |
---|---|
应用程序 |
安装该route的应用对象 |
规则 |
规则字符串(例如 |
方法 |
HTTP方法的字符串(例如: |
回调 |
未应用任何插件的原始回调函数,用于内省。 |
名称 |
route的名字,如未指定则为 |
插件 |
route安装的插件列表,除了整个应用范围内的插件,额外添加的(见 |
滑雪运动员 |
应用安装了,但该route没安装的插件列表(见 meth:Bottle.route ) |
配置 |
传递给 |
对你的应用而言, Route.config
也许是最重要的属性了。记住,这个字典会在所有插件中共享,建议添加一个独一无二的前缀。如果你的插件需要很多设置,将其保存在 config 字典的一个独立的命名空间吧。防止插件之间的命名冲突。
改变 Route
对象¶
Route
的一些属性是不可变的,改动也许会影响到其它插件。坏主意就是,monkey-patch一个损坏的route,而不是提供有效的帮助信息来让用户修复问题。
在极少情况下,破坏规则也许是恰当的。在你更改了 Route
实例后,抛一个 RouteReset
异常。这会从缓存中删除当前的route,并重新应用所有插件。无论如何,router没有被更新。改变 rule 或 method 的值并不会影响到router,只会影响到插件。这个情况在将来也许会改变。
运行时优化¶
插件应用到route以后,被插件封装起来的回调函数会被缓存,以加速后续的访问。如果你的插件的行为依赖一些设置,你需要在运行时更改这些设置,你需要在每次请求的时候读取设置信息。够简单了吧。
但是,出于性能方面的考虑,可能值得根据当前的需要选择不同的包装器,使用闭包,或者在运行时启用或禁用插件。让我们以内置的HooksPlugin为例:如果没有安装钩子,插件会将自己从所有受影响的路由中删除,实际上没有开销。一旦你安装了第一个钩子,插件就会自动激活并再次生效。
为了达到这个目的,你需要控制回调函数的缓存: Route.reset()
函数清空单一route的缓存, Bottle.reset()
函数清空所有route的缓存。在下一次请求的时候,所有插件被重新应用到route上面,就像第一次请求时那样。
如果在route的回调函数里面调用,两种方法都不会影响当前的请求。当然,可以抛出一个 RouteReset
异常,来改变当前的请求。
插件例子: SQLitePlugin¶
这个插件提供对sqlite3数据库的访问,如果route的回调函数提供了关键字参数(默认是"db"),则"db"可做为数据库连接,如果route的回调函数没有提供该参数,则忽略该route。wrapper不会影响返回值,但是会处理插件相关的异常。 Plugin.setup()
方法用于检查应用,查找冲突的插件。
import sqlite3
import inspect
class SQLitePlugin(object):
''' This plugin passes an sqlite3 database handle to route callbacks
that accept a `db` keyword argument. If a callback does not expect
such a parameter, no connection is made. You can override the database
settings on a per-route basis. '''
name = 'sqlite'
api = 2
def __init__(self, dbfile=':memory:', autocommit=True, dictrows=True,
keyword='db'):
self.dbfile = dbfile
self.autocommit = autocommit
self.dictrows = dictrows
self.keyword = keyword
def setup(self, app):
''' Make sure that other installed plugins don't affect the same
keyword argument.'''
for other in app.plugins:
if not isinstance(other, SQLitePlugin): continue
if other.keyword == self.keyword:
raise PluginError("Found another sqlite plugin with "\
"conflicting settings (non-unique keyword).")
def apply(self, callback, context):
# Override global configuration with route-specific values.
conf = context.config.get('sqlite') or {}
dbfile = conf.get('dbfile', self.dbfile)
autocommit = conf.get('autocommit', self.autocommit)
dictrows = conf.get('dictrows', self.dictrows)
keyword = conf.get('keyword', self.keyword)
# Test if the original callback accepts a 'db' keyword.
# Ignore it if it does not need a database handle.
args = inspect.getargspec(context.callback)[0]
if keyword not in args:
return callback
def wrapper(*args, **kwargs):
# Connect to the database
db = sqlite3.connect(dbfile)
# This enables column access by name: row['column_name']
if dictrows: db.row_factory = sqlite3.Row
# Add the connection handle as a keyword argument.
kwargs[keyword] = db
try:
rv = callback(*args, **kwargs)
if autocommit: db.commit()
except sqlite3.IntegrityError, e:
db.rollback()
raise HTTPError(500, "Database Error", e)
finally:
db.close()
return rv
# Replace the route callback with the wrapped one.
return wrapper
这个插件十分有用,已经和Bottle提供的那个版本很类似了(译者注:=。= 一模一样)。只要60行代码,还不赖嘛!下面是一个使用例子。
sqlite = SQLitePlugin(dbfile='/tmp/test.db')
bottle.install(sqlite)
@route('/show/<page>')
def show(page, db):
row = db.execute('SELECT * from pages where name=?', page).fetchone()
if row:
return template('showpage', page=row)
return HTTPError(404, "Page not found")
@route('/static/<fname:path>')
def static(fname):
return static_file(fname, root='/some/path')
@route('/admin/set/<db:re:[a-zA-Z]+>', skip=[sqlite])
def change_dbfile(db):
sqlite.dbfile = '/tmp/%s.db' % db
return "Switched DB to %s.db" % db
第一个route提供了一个"db"参数,告诉插件它需要一个数据库连接。第二个route不需要一个数据库连接,所以会被插件忽略。第三个route确实有一个"db"参数,但显式的禁用了sqlite插件,这样,"db"参数不会被插件修改,还是包含URL传过来的那个值。