Pytest 源码解读 [1] - [pluggy] 插件框架介绍
Pluggy
- (https://github.com/pytest-dev/pluggy)
- Pytest 的核心实际的基于
Pluggy
这个 plugin framework 的,实际上 pytest 本身就是由一个一个插件组成的 - 本来 pluggy 的代码是在 pytest 的 repo 里,后来迁移了出来,作为一个独立的项目。Pluggy 作为一个独立的plugin framework 来看也是很优雅的存在
一个简单的Demo
Pluggy
已经从之前的 Pytest
源码中独立出了一个单独的 Repo , 对于 Pytest
自身也是把它作为一个外部的依赖来使用,我们这里就用一个独立的 Python 项目来 Demo,先看代码
from pluggy import HookspecMarker, HookimplMarker, PluginManager spec = HookspecMarker("pluggy_demo_1") impl = HookimplMarker("pluggy_demo_1") class HookSpec: @spec(historic=True) def calculate(self, a, b): pass class HookImpl1: @impl def calculate(self, a, b): return a + b pm = PluginManager("pluggy_demo_1") pm.add_hookspecs(HookSpec) pm.register(HookImpl1()) pm.hook.calculate(a=1, b=2)
Output
[3]
解释
Pluggy
的核心就是三个类HookspecMarker
,HookimplMarker
,PluginManager
,核心的插件逻辑就是定义了一组 hook 的方法,然后 plugin 是hook 方法的具体实现- 整个 Project 需要用一个全局唯一的 Project Name ,这里是
pluggy_demo_1
HookSpec
是一个申明 hook method 的 class ,每一个 hook method 需要用spec
的装饰器来装饰HookImpl1
是一个 plugin 的实现,需要完整实现对应的hook方法,并且通过impl
装饰器来装饰- 核心代码的调用逻辑就是先创建一个
PluginManager
对象,注册 Spec 和对应的 plugin 对象,然后通过PluginManager
自带的 hook 变量来调用对应的hook方法,传入相关的参数即可。切记在调用 hook 的时候参数必须是通过关键字的方式来传递
hook 和 plugin 的关系
hook 和 plugin 的对应关系是 1:N
,如果说注册了多个实现了同一个 hook 的 plugin ,会返回多个结果,我们来看这个例子
from pluggy import HookspecMarker, HookimplMarker, PluginManager spec = HookspecMarker("pluggy_demo_1") impl = HookimplMarker("pluggy_demo_1") class HookSpec: @spec def calculate(self, a, b): pass class HookImpl1: @impl def calculate(self, a, b): return a + b class HookImpl2: @impl def calculate(self, a, b): return a * b pm = PluginManager("pluggy_demo_1") pm.add_hookspecs(HookSpec) pm.register(HookImpl1()) pm.register(HookImpl2()) print(pm.hook.calculate(a=1, b=2))
Output
[2,3]
解释
-
在这里我们注册了两个 plugin ,
HookImpl1
和HookImpl2
,分别对应了加法和乘法的两个不同逻辑 -
一次 hook 的调用返回了2个plugin 执行的结果,注意一下这里是先执行后注册的
HookImpl2
,再执行先注册的HookImpl1
, 下次具体分析pluggy
实现的时候会解释
plugin 调用顺序
HookimplMarker 装饰器参数
HookimplMarker
装饰器支持一些特定的参数
- tryfirst - 顾名思义就是这个 plugin 在
1:N
的执行链路中先执行 - trylast - 顾名思义后执行
- hookwrapper - 基于
yield
实现的一个wrapper,先执行 wrapper plugin 的一部分逻辑,然后执行其他 plugin,最后执行剩余的 wrapper plugin 逻辑
tryfirst
我们修改一下刚才那个demo,把HookImpl1
加上tryfirst
参数, 执行的顺序就变了
class HookImpl1: @impl(tryfirst=True) def calculate(self, a, b): return a + b
Output
[3,2]
HookspecMarker 装饰器参数
hookwrapper
这里我们实现一个特殊 plugin ImplWrapper
,先看代码
from pluggy import HookspecMarker, HookimplMarker, PluginManager spec = HookspecMarker("plugin_demo_2") impl = HookimplMarker("plugin_demo_2") pm = PluginManager("plugin_demo_2") class Spec: @spec def calculate(self, a, b): pass class Impl1: @impl def calculate(self, a, b): return a + b class Impl2: @impl(tryfirst=True) def calculate(self, a, b): return a + b + 2 class ImplWrapper: @impl(hookwrapper=True) def calculate(self, a, b): print("before logic") outcome = yield print("Get Result %s" % outcome.result) return a * b * 10 pm.add_hookspecs(Spec) pm.register(Impl1()) pm.register(Impl2()) pm.register(ImplWrapper()) print(pm.hook.calculate(a=1, b=2))
Output
before logic Get Result [5, 3] [5, 3]
解释
ImplWrapper
是一个类似coroutine
的 生成器,它有两段逻辑,用outcome = yield
来分割- outcome 通过
yield
来获取,它是_Result
对象,包含了非wrapper 的 plugin 的执行结果,这里就是Impl1
和Impl2
,从实际的output来看,Get Result [5,3]
就是获取了返回值 - wrapper plugin 的返回值是会被 ignore 的,具体的原因下次分析源码的时候会给解释
HookspecMarker 装饰器参数
HookspckMarker
装饰器也支持一些参数,主要是
- firstresult - 获取第一个plugin 执行结果后就中断后续执行
- historic - 表示这个 hook 是需要保存call history 的,当有新的 plugin 注册的时候,需要回放历史
firstresult
调整一下 HookSpec
,添加 firstresult
参数,我们看一下执行结果
class Spec: @spec(firstresult) def calculate(self, a, b): pass class HookImpl1: @impl(tryfirst=True) def calculate(self, a, b): return a + b
Output
[2]
原文链接:https://markshao.github.io/2019/10/01/pluggy-guideline/