编写插件¶
本指南解释了插件 API 以及如何编写自定义插件。如果您尚未阅读,建议先阅读 插件基础。您可能还想查看 使用插件 以获取一些实际示例。
插件 API¶
任何接受一个函数并返回一个函数的 callable 都是一个有效的插件。然而,这种简单的方法有其局限性。需要更多上下文和控制的插件可以实现扩展的 Plugin
接口并使用高级功能。请注意,这不是一个你可以从 bottle
导入的真实类,只是一个插件必须实现的契约,以便被识别为扩展插件。
- class Plugin¶
插件必须是 callable 或实现
apply()
。如果定义了apply()
,则总是优先于直接调用插件。所有其他方法和属性都是可选的。- name¶
Bottle.uninstall()
和Bottle.route()
的 skip 参数都接受一个 name 字符串来引用插件或插件类型。这仅适用于具有 name 属性的插件。
- setup(self, app: Bottle)¶
当插件通过
Bottle.install()
安装到应用程序时立即调用。唯一的参数是插件被安装到的应用程序对象。此方法 **不** 会为通过 apply 应用到路由的插件调用,仅会为安装到应用程序的插件调用。
- __call__(self, callback)¶
只要未定义
apply()
,插件本身就被用作 decorator,直接应用于每个路由 callback。唯一的参数是要装饰的 callback。此方法返回的任何内容都会替换原始 callback。如果不需要包装或替换给定的 callback,只需返回未修改的 callback 参数即可。
- apply(self, callback, route: Route)¶
如果定义了此方法,则会优先使用它而不是
__call__()
来装饰路由 callback。额外的 route 参数是Route
的一个实例,提供了大量关于要装饰的路由的上下文和元信息。详情请参见 路由上下文。
- close(self)¶
当插件被卸载或应用程序关闭时立即调用(参见
Bottle.uninstall()
或Bottle.close()
)。此方法 **不** 会为通过 apply 应用到路由的插件调用,仅会为安装到应用程序的插件调用。
插件 API 版本¶
插件 API 仍在不断发展,并随着 Bottle 0.10 的发布而改变,以解决路由上下文字典的某些问题。为了确保与 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)Plugin.apply()
方法的 context 参数现在是Route
的一个实例,而不是上下文字典。
路由上下文¶
传递给 Plugin.apply()
的 Route
实例提供了关于要装饰的路由、原始路由 callback 和路由特定配置的详细信息。
请记住,Route.config
对路由是局部的,但在所有插件之间共享。一个好的做法是添加一个唯一的 prefix,或者如果你的插件需要大量配置,将其存储在 config 字典内的一个单独的 namespace 中。这有助于避免插件之间的命名冲突。
虽然一些 Route
属性是可变的,但更改可能会对其他插件产生不希望的影响,并且只影响尚未应用的插件。如果你需要对路由进行更改,并希望所有插件都能识别这些更改,请在之后调用 Route.reset()
。这将清除路由缓存,并在下次调用该路由时再次应用所有插件,使所有插件都有机会适应新的 config。然而,router 不会被更新。对 rule 或 method 值的更改对 router 没有影响,只对插件有影响。这将来可能会改变。
运行时优化¶
一旦所有插件都应用到路由,被包装的路由 callback 就会被缓存,以加快后续请求。如果你的插件的行为依赖于 configuration,并且你希望能够在运行时更改该 configuration,你需要在每次请求时读取 configuration。这很简单。
然而,出于性能原因,根据当前需求返回不同的 wrapper、使用 closures 或在运行时启用或禁用插件可能是值得的。以内置的 HooksPlugin
为例:如果没有安装任何 hooks,插件会从所有路由中移除自己,并且几乎没有开销。一旦你安装了第一个 hook,插件就会重新激活并再次生效。
要实现这一点,你需要控制 callback 缓存:Route.reset()
清除单个路由的缓存,而 Bottle.reset()
一次性清除应用程序所有路由的所有缓存。在下一次请求时,所有插件会重新应用于该路由,就像第一次请求一样。
常见模式¶
依赖或资源注入
插件可以检查 callback 是否接受特定的关键字参数,并且只有当该参数存在时才应用自己。例如,需要 db
关键字参数的路由 callback 需要数据库连接。不需要此类参数的路由可以被跳过,不进行装饰。参数名称应该是可配置的,以避免与其他插件或路由参数冲突。
请求上下文属性
插件可以将新的 request-local 属性添加到当前的 request
中,例如用于持久 session 的 request.session
或用于已登录用户的 request.user
。参见 Request.__setattr__
。
响应类型映射
插件可以检查被包装的 callback 的返回值,并将输出转换或序列化为新类型。内置的 JsonPlugin
正是这样做的。
零开销插件
在特定路由上不需要的插件应返回未更改的 callback。如果它们想在运行时从路由中移除自己,可以调用 Route.reset()
并在下次触发时跳过该路由。
每个请求之前/之后
插件可以是 before_request
或 after_request
hooks(参见 Bottle.add_hook()
)的便捷替代方案,特别是当两者都需要时。
插件示例:SQLitePlugin¶
这个插件为被包装的 callback 提供一个 sqlite3 数据库连接 handle 作为附加的关键字参数,但前提是 callback 期望它。如果不是,该路由将被忽略,并且不会增加开销。wrapper 不会影响返回值,但会正确处理与插件相关的异常。Plugin.setup()
用于检查应用程序并搜索冲突的插件。
import sqlite3
import inspect
class SQLitePlugin:
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, route):
# Override global configuration with route-specific values.
conf = route.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(route.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
这个插件只是一个示例,但实际上是可用的。
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
第一个路由需要数据库连接,并通过接受 db
关键字参数来告诉插件创建一个 handle。第二个路由不需要数据库,因此被插件忽略。第三个路由确实期望一个 'db' 关键字参数,但明确跳过了 sqlite 插件。这样,该参数就不会被插件覆盖,并且仍然包含同名 url 参数的值。