Python pluggy 的 hook 实现与优先级控制

2次阅读

tryfirst/trylast 是协商式优先级:先按 priority 排序,再将 tryfirst 放最前、trylast 放最后;tryfirst 对应 priority=0(非负无穷),冲突时回退到 priority 比较,相同则按注册顺序(不稳定)。

Python pluggy 的 hook 实现与优先级控制

hookimpl 的 tryfirsttrylast 怎么生效?

tryfirst=Truetrylast=True 不是强制排序,而是“协商式优先级”:当多个插件注册了同名 hook,pluggy 会先按 hookimplpriority 数值排序,再把 tryfirst 的放最前、trylast 的放最后——但仅限于它们之间不冲突时。一旦 priority 显式设为高值(比如 100),它就可能压过 tryfirst

  • tryfirst 对应的 priority 实际是 0,不是负无穷;trylast 对应 0,也不是正无穷
  • 如果两个插件都设 tryfirst=True,那就退回到比 priority,priority 相同才按注册顺序(非稳定)
  • 在 hookspec 声明里加 firstresult=True 后,tryfirst 更关键——第一个返回非 None 的 tryfirst 实现会直接中断执行
@hookimpl(tryfirst=True) def pytest_runtest_makereport(item, call):     if call.excinfo:         return ReportWrapper(call.excinfo)

为什么 hook 调用顺序和预期不符?

常见现象是:明明写了 trylast=True,结果自己的逻辑还是被别的插件覆盖或截断。根本原因通常是 hookspec 定义时没开 hookwrapper=Truefirstresult=True,导致所有实现都串行执行,而你的逻辑在中间就被其他返回值干扰了。

  • 插件加载顺序影响注册顺序,但 pluggy 不保证 import 顺序 = 注册顺序,尤其用了 setuptools entry points 时
  • pm.register() 手动注册的插件,其 hookimpl 优先级只在本次注册中有效,不会覆盖已注册的同名 hook
  • 使用 pm.hook.my_hook._nonwrappers 可以查当前实际参与调用的实现列表,调试时比猜更可靠

hookwrapper=True 和普通 hookimpl 有什么实质区别?

hookwrapper=True 不是“更高优先级”,而是切换了执行模型:它不直接返回值,而是用 yield 切入 hook 执行流,在前后都能拦截——比如在测试开始前改 item,或在所有普通 impl 返回后统一收口日志。

  • 普通 hookimpl 必须有 return,且返回值会进入结果列表(除非 hookspec 设了 firstresult=True
  • hookwrapper 函数体内必须有且仅有一个 yield,否则启动时报 HookCallError
  • 多个 hookwrapper 之间仍受 priority 控制,但它们包裹的是“整个 hook 调用过程”,不是单个返回值
  • 注意:wrapper 里 yield 后的代码,是在所有普通 impl 执行完之后才运行的
@hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call):     outcome = yield     report = outcome.get_result()  # 获取普通 impl 的返回结果     if report.failed:         log_failure(report)

如何安全地覆盖第三方插件的 hook 行为?

别试图用 tryfirst=True 硬顶替——尤其当对方用了 hookwrapper 或修改了内部状态时,容易引发静默失败。真正可控的方式是:明确 hookspec 是否允许你介入、是否支持 firstresult、以及有没有公开的配置开关。

立即学习Python免费学习笔记(深入)”;

  • 先看目标 hookspec 的定义:在源码里搜 @hookspec,确认有没有 firstresulthookwrapperhistoric 等标记
  • 如果原 hook 是 firstresult=True,你设 tryfirst=True + return 非 None,就能短路后续逻辑
  • 如果原 hook 是 hookwrapper=True,你的 wrapper 必须小心 yield 后的副作用,避免重复执行或状态错乱
  • 最稳妥的覆盖点,其实是 hookspec 允许的参数注入位置(比如 pytest_configure(config) 中改 config.pluginmanager),而不是硬抢 hook 顺序

pluggy 的 hook 优先级本质是“协作契约”,不是调度器。越想绕过规则强行控制,越容易掉进注册时机、yield 生命周期、结果聚合逻辑这些隐性坑里。

text=ZqhQzanResources