ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Bottle 插件开发指南:深入 Plugin API 与自定义插件实战

Bottle 插件开发指南:深入 Plugin API 与自定义插件实战 后端Web框架【免费下载链接】bottlebottle.py is a fast and simple micro-framework for python web-applications.项目地址https://gitcode.com/gh_mirrors/bo/bottle点击查看免费下载本指南以 Bottle 官方《Writing Plugins》文档为骨架系统讲解 Bottle 的插件机制从任何可调用对象都可作为插件的最小模型到带setup/apply/close的扩展Plugin接口再到路由上下文Route、运行时缓存优化与常见设计模式。读完本文你将能独立编写具备依赖注入、请求上下文扩展、响应序列化能力的生产级插件并借助仓库源码与测试用例验证每一个结论。插件是什么一个装饰器模型Bottle 的插件系统建立在 Python装饰器decorator概念之上。简单来说插件就是被应用到应用内所有路由回调上的装饰器。这个模型在 docs/plugins/index.rst《Using Plugins》中有基础说明而本指南是它的进阶篇。先看一个最朴素、可立即运行的示例——一个测量执行耗时并写入响应头的stopwatch装饰器from bottle import response import time def stopwatch(callback): def wrapper(*args, **kwargs): start time.time() result callback(*args, **kwargs) end time.time() response.headers[X-Exec-Time] str(end - start) return result return wrapper手动把这个装饰器挂到单个路由上当然可行但不够优雅from bottle import route route(/timed) stopwatch # 可行但不要这样做 def timed(): time.sleep(1) return DONE正确的做法是通过install()把它注册为全局插件让 Bottle 自动应用到所有路由from bottle import route, install install(stopwatch) route(/timed) def timed(): ...install()的调用时机无关紧要——无论先安装插件还是先绑定路由插件最终都会被应用到全部路由上。但多个插件的安装顺序至关重要它们会按照安装的先后顺序依次作用于每个回调详见下文插件应用顺序一节的源码验证。扩展 Plugin 接口接受一个函数并返回一个函数这种朴素模型有它的局限。需要更多上下文和控制的插件可以实现扩展的Plugin接口。需要特别强调Plugin并不是一个可以从bottle模块导入的真实类它只是一份契约——插件只要实现其中约定的方法就会被 Bottle 识别为扩展插件。真正的类型定义与文档注释见 bottle.py 中Route类及其周边代码。成员说明name字符串标识。Bottle.uninstall()和Bottle.route()的skip参数都可以用这个名字字符串来指代某个插件或插件类型只有具备name属性的插件才支持这种按名操作。内置JSONPlugin的name json、TemplatePlugin的name template即为例证见 bottle.py 与 bottle.pyapi整型版本号告诉 Bottle 使用哪一代插件 API。缺省时 Bottle 默认按第一版处理当前版本为2setup(self, app: Bottle)插件通过Bottle.install()被安装到某个应用时立即调用参数是目标应用对象。仅对安装到应用上的插件调用对通过apply应用到路由上的插件不会调用__call__(self, callback)只要未定义apply插件自身就被当作装饰器直接应用到每个路由回调上。返回值会替换原始回调若无需包装或替换直接原样返回callback即可apply(self, callback, route: Route)若定义则优先于__call__被用来装饰路由回调。多出的route参数是Route实例携带大量路由上下文与元信息详见Route 上下文一节close(self)插件被卸载或应用被关闭时调用对应Bottle.uninstall()与Bottle.close()。同样仅对安装到应用上的插件调用源码中的调度逻辑apply优先于__call__这条规则在Route._make_callback()中有着明确的实现bottle.pydef _make_callback(self): callback self.callback for plugin in self.all_plugins(): if hasattr(plugin, apply): callback plugin.apply(callback, self) else: callback plugin(callback) if callback is not self.callback: update_wrapper(callback, self.callback) return callbacktest/test_plugins.py中的test_apply用例test/test_plugins.py专门验证了这一点一个同时实现了apply和__call__的插件如果__call__抛出了AssertionError请求依然能正常返回证明 Bottle 走的是apply分支。测试用route.config[test]读取路由级配置返回值test:plugin.cfg; tail也印证了apply能访问Route上下文。Plugin API 版本演进插件 API 仍在演进。Bottle 0.10 为了处理路由上下文字典的问题修改了 API同时为了保持对 0.9 插件向后兼容引入了可选的Plugin.api属性。两者差异如下Bottle 0.9 / API 1Plugin.api不存在0.9 文档中描述的原始插件 API。Bottle 0.10 / API 2Plugin.api等于2Plugin.apply()方法的context参数从上下文字典变成了Route实例。因此新编写的插件建议显式声明api 2并充分利用Route对象提供的丰富上下文。Route 上下文传给Plugin.apply()的Route实例提供了关于被装饰路由、原始回调以及路由级配置的详细信息。从源码看bottle.pyRoute至少包含这些关键成员app该路由所属的应用对象rule路径规则字符串如/wiki/pagemethodHTTP 方法字符串如GETcallback未应用任何插件的原始回调非常适合内省name路由名若指定否则为Noneplugins路由级插件列表来自route()的apply参数skiplist本路由要跳过的插件列表来自route()的skip参数config路由级配置字典它实际上是应用配置app.config的一个 overlay覆盖层并额外加载了route()装饰器传入的任意关键字参数。config 的共享与命名空间务必牢记Route.config对单条路由来说是局部的但在所有插件之间是共享的。为了避免插件之间的命名冲突强烈建议给配置键加上唯一前缀如sqlite.db或当插件需要大量配置时把配置统一存放在config字典内的独立命名空间里例如一个专属子字典。route()装饰器会把它收到的任意额外关键字参数存入Route.configbottle.py 的 docstring 明确写道Any additional keyword arguments are stored as route-specific configuration and passed to plugins。这正是访问控制插件检查自定义roles_allowed参数这类模式的底层支撑。测试test/test_plugins.py的setUptest/test_plugins.py就是用self.app.route(/, testplugin.cfg)注入自定义参数再由插件通过route.config[test]读回。可变属性与 Route.resetRoute的部分属性是可变的但修改它们可能对其他插件产生非预期影响而且只会影响尚未被应用的插件。如果希望修改能被所有插件感知必须随后调用Route.reset()它会清空路由缓存下次该路由被调用时重新应用所有插件给每个插件适应新配置的机会。注意reset()不会更新路由器Router。对rule或method的改动只对插件生效、对路由器无效——文档明确提示这一点未来可能改变。Route.reset()的实现在 bottle.pydef reset(self): Forget any cached values. The next time :attr:call is accessed, all plugins are re-applied. self.__dict__.pop(call, None)运行时优化回调缓存与零开销插件所有插件应用完毕后包装后的路由回调会被缓存Route.call是一个cached_property按需创建并缓存见 bottle.py以加速后续请求。这带来一个重要的设计考量如果插件行为依赖配置且希望在运行时修改配置则插件必须在每次请求时重新读取配置——这很容易做到。但从性能角度更可取的做法是根据当前需要返回不同的包装器、使用闭包或者在运行时启用/禁用插件自身。文档给出的范例是内置的HooksPlugin式思路当没有任何钩子被安装时插件把自己从所有路由上移除几乎没有开销一旦安装了第一个钩子插件自动激活并重新生效。要实现这种动态控制你需要掌控回调缓存Route.reset()清空单条路由的缓存Bottle.reset()一次清空整个应用所有路由的缓存。下一次请求到来时所有插件都会像首次请求那样被重新应用到路由上。Bottle.reset()的实现bottle.py会遍历self.routes逐个调用route.reset()并触发app_reset钩子在DEBUG模式下还会立即route.prepare()以便调试。插件应用顺序源码级验证Route.all_plugins()bottle.py定义了插件的实际应用顺序def all_plugins(self): Yield all Plugins affecting this route. unique set() for p in reversed(self.app.plugins self.plugins): if True in self.skiplist: break name getattr(p, name, False) if name and (name in self.skiplist or name in unique): continue if p in self.skiplist or type(p) in self.skiplist: continue if name: unique.add(name) yield p从这段代码可以推断出三条关键规则skipTrue会立即中断遍历跳过所有插件全局插件与路由级插件被拼接后反向迭代因此先安装的全局插件后应用靠近回调的是后安装的插件同名插件只应用一次通过unique集合去重避免重复包装。test/test_plugins.py的test_plugin_odertest/test_plugins.py验证了这一点全局插件输出顺序为;global-2;global-1后安装的先应用路由级插件输出顺序为;local-2;local-1;global-2;global-1局部插件先于全局插件应用。这与 docs/plugins/index.rst 中route-level plugins are applied first的描述完全一致。常见插件设计模式文档归纳了五种被官方认可且广泛使用于生态中的模式值得逐条展开。1. 依赖或资源注入插件可以检查回调是否接受某个特定关键字参数只在参数存在时才应用自身。例如期望db关键字参数的route回调需要数据库连接而不需要该参数的路由可以跳过、不加任何装饰。为避免与其他插件或路由参数冲突参数名应可配置。SQLitePlugin见下文完整示例正是这一模式的代表。2. 请求上下文属性插件可以为当前request对象添加新的请求局部属性例如持久会话用的request.session、登录用户用的request.user。其底层机制是BaseRequest.__setattr__Bottle 的Request对象在收到未知属性赋值时会把值存入请求本地的environ字典从而保证每个请求相互隔离、请求结束后自动回收。3. 响应类型映射插件可以检查包装回调的返回值并把输出转换或序列化为新类型。内置的JSONPluginbottle.py就是典型它只包装而非替换回调当返回值为dict或HTTPResponse且 body 为dict时用json_dumps序列化并设置Content-Type: application/json否则原样放行。它还通过setup()向app.config注册了json.enable、json.ascii、json.indent、json.dump_func四个可配置项。4. 零开销插件在不需要的特定路由上插件应当原样返回回调不包装。若要在运行时把自己从某个路由上移除可调用Route.reset()并在下次触发时跳过该路由。这要求插件把是否需要本路由的判断放在apply阶段完成而非包装后每次请求再判断。5. 每个请求的前后处理插件可以作为before_request/after_request钩子见Bottle.add_hook()bottle.py的便捷替代尤其是两者需要同时使用的场景——插件可以把前置逻辑与后置逻辑放进同一个apply返回的wrapper里围绕callback(*args, **kwargs)执行。完整实战编写 SQLitePlugin文档提供了一个虽为示例但实际可用的完整插件。它用sqlite3连接句柄作为额外的关键字参数注入包装后的回调且仅在回调确实需要它时才注入否则忽略该路由、不增加任何开销。包装器不改变返回值但妥善处理插件相关的异常。setup()用于检查应用、查找冲突插件。import sqlite3 import inspect class SQLitePlugin: name sqlite api 2 def __init__(self, dbfile:memory:, autocommitTrue, dictrowsTrue, keyworddb): self.dbfile dbfile self.autocommit autocommit self.dictrows dictrows self.keyword keyword def setup(self, app): 确保其他已安装插件不会占用同一个关键字参数。 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): # 用路由级配置覆盖全局配置。 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) # 测试原始回调是否接受 db 关键字参数。 # 若它不需要数据库句柄则直接忽略该路由。 args inspect.getargspec(route.callback)[0] if keyword not in args: return callback def wrapper(*args, **kwargs): # 连接数据库 db sqlite3.connect(dbfile) # 启用按列名访问row[column_name] if dictrows: db.row_factory sqlite3.Row # 把连接句柄作为关键字参数传入 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 # 用包装器替换路由回调。 return wrapper这段代码浓缩了上文所有要点逐行解读如下name sqlite与api 2分别启用按名卸载/跳过并声明使用当代Route上下文 APIsetup()遍历app.plugins检查关键字冲突冲突时抛出PluginError定义于 bottle.py是BottleException的子类apply()先从route.config读取sqlite命名空间下的路由级覆盖配置再通过inspect.getargspec(route.callback)检查参数名——这正是依赖注入模式的关键步骤返回的wrapper在finally中保证连接关闭IntegrityError时回滚并转成 500HTTPError同时不修改正常返回值。提示示例中的inspect.getargspec在 Python 3 中已移除实际使用请替换为inspect.signature。Bottle 自身的Route.get_callback_args()bottle.py正是用inspect.signature实现参数名探测的可作为现代写法的参考。路由配置覆盖route.config 的实际用法apply()中route.config.get(sqlite) or {}表明插件用户可以在单条路由上用route()装饰器的自定义关键字参数覆盖全局设置。例如route(/admin, sqlite{dbfile: /tmp/admin.db, autocommit: False}) def admin(db): ...route()装饰器会把sqlite这个字典原样存入该路由的Route.config见 bottle.py插件据此实现全局配置 路由级覆盖的灵活模型。安装、使用与按路由跳过插件编写完成后通过Bottle.install()安装通过Bottle.uninstall()卸载。两者都支持按实例、按类型、按名字与全量操作源码bottle.py中的匹配逻辑为install(plugin)若插件有setup方法先调用之随后校验callable(plugin) or hasattr(plugin, apply)不满足则抛TypeError(Plugins must be callable or implement .apply())测试test_install_non_plugin验证了这一点test/test_plugins.py加入self.plugins后调用reset()使全部缓存失效uninstall(plugin)当remove is True or remove is plugin or remove is type(plugin) or getattr(plugin, name, True) remove时命中移除命中且实现了close的插件会被调用close()有实际移除则reset()。结合 SQLitePlugin 的完整使用示例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, pagerow) 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三条路由展示了三种不同的交互方式/show/page回调声明了db关键字参数插件检测到后自动注入连接句柄/static/fname:path回调没有db参数插件原样放行零开销/admin/set/db:re:[a-zA-Z]回调的db是URL 路径参数通过skip[sqlite]显式跳过该插件避免插件覆盖同名参数——这样db依然保留 URL 中捕获的值而不是数据库句柄。这也是一个按路由控制插件行为的生动案例。skip参数在route()装饰器中的处理位于 bottle.pyskiplist makelist(skip)支持传入单个值或列表值可以是插件实例、类或名字字符串True则跳过全部。test/test_plugins.py中的test_skip_by_instance/test_skip_by_class/test_skip_by_name/test_skip_all/test_skip_nonlisttest/test_plugins.py覆盖了这五种形态。install/uninstall也提供模块级便捷函数但请注意模块级函数作用于默认应用default application若要管理某个特定Bottle实例的插件请使用该实例的install()/uninstall()方法见 docs/plugins/index.rst 的说明。插件与挂载Bottle.mount插件默认不会穿透到挂载的子应用。当一个应用被mount挂载时Bottle 会在挂载方创建一条转发请求的代理路由而插件默认对这类代理路由禁用。在 bottle.py 中可以看到_mount_wsgi通过options.setdefault(skip, True)默认跳过全部插件。root Bottle() root.install(plugins.WTForms()) root.mount(/blog, apps.blog) root.route(/contact) def contact(): return template(contact, emailcontactexample.com)这里虚构的WTForms插件会作用于/contact路由但不会影响/blog子应用的路由——这是刻意设计的合理默认值。需要恢复时可对特定代理路由重新激活插件root.mount(/blog, apps.blog, skipNone)注意此时插件把整个子应用视为一条路由即代理路由。若要对子应用的每一条路由逐一包装必须直接把插件安装到子应用上apps.blog.install(...)。运行时卸载与配置热更新插件可以在任意时刻——包括服务请求进行中——被安装或移除。插件是按需应用的即路由第一次被请求时才应用插件。这带来了一些实用技巧例如只在需要时安装调试/性能分析插件但不应滥用每次插件列表发生变化路由缓存都会被清空所有插件需要重新应用一遍。对于支持从Bottle.config读取配置的新式插件配置甚至可以跨部署环境覆盖并通过 config 钩子conf-hook在运行时动态调整配置钩子的触发见 bottle.py 中_add_change_listener对trigger_hook(config)的注册。例如app.config[sqlite.db] /tmp/test.db app.install(SQLitePlugin())测试驱动用官方测试验证你的插件仓库的 test/test_plugins.py 是编写插件时最好的对照参考。它包含两组测试TestPluginManagementtest/test_plugins.py覆盖安装/卸载全流程与顺序语义test_install_plugin/test_install_decorator插件与普通装饰器均可安装test_install_non_plugin非插件对象安装时抛TypeErrortest_uninstall_by_instance/test_uninstall_by_type/test_uninstall_by_name/test_uninstall_all四种卸载方式test_route_pluginapply[...]仅作用于指定路由test_plugin_oder局部插件先于全局插件、同层按后装先应用排列test_skip_*按实例/类/名字/全部/非列表值跳过test_json_plugin_catches_httpresponseJSONPlugin对HTTPResponse(dict)与raise HTTPResponse(dict)两种形态都能正确序列化。TestPluginAPItest/test_plugins.py逐项验证扩展接口test_callable仅实现__call__的纯装饰器插件test_apply实现apply后__call__不被调用test_instance_method_wrapperapply可直接返回绑定方法test_setup安装时setup(app)收到应用对象test_close卸载与app.close()都会触发close()。这些测试直接对应本文描述的全部接口语义可作为自己插件的行为验证清单。小结Bottle 的插件体系建立在三个层次上最小模型任何接受函数、返回函数的可调用对象即可成为插件通过install()全局应用扩展接口实现Plugin契约name/api/setup/apply/close获得应用级生命周期回调与Route上下文运行时控制借助Route.reset()/Bottle.reset()管理回调缓存实现按需激活、零开销与运行时重配置。配合仓库中的 bottle.pyinstall/uninstall实现、bottle.py插件调度、bottle.pyJSONPlugin参考实现以及 test/test_plugins.py 的完整测试矩阵你完全可以以此为蓝本写出安全、高效、可复用且行为可验证的自定义插件。赞分享后端Web框架【免费下载链接】bottlebottle.py is a fast and simple micro-framework for python web-applications.项目地址https://gitcode.com/gh_mirrors/bo/bottle点击查看免费下载相关推荐TypeDoc 插件系统实战指南从 --plugin 加载到自定义插件开发TypeDoc 插件系统实战指南从 plugin 加载到自定义插件开发 TypeDoc 的插件机制是其生态扩展的核心通过 plugin 命令行参数或 plu开发工具文档Eclipse Theia 无头插件Headless Plugin与自定义插件 API 实战以 plugin-gotd 示例插件为例Eclipse Theia 无头插件Headless Plugin与自定义插件 API 实战以 plugin gotd 示例插件为例 本篇技术指南围绕 EIDE代码编辑器开发工具前端桌面应用插件系统后端AI 应用Docker CLI 插件开发指南深入理解 Plugin APIDocker CLI 插件开发指南深入理解 Plugin API 前言 Docker 插件系统是 Docker 生态中一个强大但常被忽视的功能它允许开发者扩CLI开发工具上一篇Jetson设备YOLO部署实战从环境搭建到性能优化全解析下一篇ESP32 HWCDC大数据传输优化从性能瓶颈到流畅传输的终极解决方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表