3

Pytest 源码解读 [1] - [pluggy] 插件框架介绍

 2 years ago
source link: http://markshao.github.io/2019/10/01/pluggy-guideline/#firstresult
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

今天是祖国母亲的70华诞,早上在家里和媳妇一起看了令人激动的阅兵,真心为自己做位一个中国人而自豪。国庆的上海天气有点操蛋,受到台风的影响外面是大风大雨,不过因为媳妇怀孕的原因本来也没计划出去玩,趁着难得的假期,在家喝喝茶,吃吃东西,写写博客,把的之前拖欠的 Pytest 源码解读 给完成。

言归正传,上次说了 Pytest 的核心是基于Pluggy这个 Python Plugin 框架,这次先介绍一下 Pluggy 的核心功能

一个简单的Demo

Pluggy 已经从之前的 Pytest源码中独立出了一个单独的 Repo , 对于 Pytest自身也是把它作为一个外部的依赖来使用,我们这里就用一个独立的 Python 项目来 Demo,先看代码

python
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

plaintext
[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 ,会返回多个结果,我们来看这个例子

python
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

plaintext
[2,3]
  • 在这里我们注册了两个 plugin , HookImpl1HookImpl2,分别对应了加法和乘法的两个不同逻辑

  • 一次 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参数, 执行的顺序就变了

python
class HookImpl1:
@impl(tryfirst=True)
def calculate(self, a, b):
return a + b

Output

plaintext
[3,2]

HookspecMarker 装饰器参数

hookwrapper

这里我们实现一个特殊 plugin ImplWrapper,先看代码

python
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

shell
before logic
Get Result [5, 3]
[5, 3]
  • ImplWrapper 是一个类似 coroutine的 生成器,它有两段逻辑,用outcome = yield来分割
  • outcome 通过 yield来获取,它是_Result对象,包含了非wrapper 的 plugin 的执行结果,这里就是 Impl1Impl2,从实际的output来看,Get Result [5,3]就是获取了返回值
  • wrapper plugin 的返回值是会被 ignore 的,具体的原因下次分析源码的时候会给解释

HookspecMarker 装饰器参数

HookspckMarker装饰器也支持一些参数,主要是

  • firstresult - 获取第一个plugin 执行结果后就中断后续执行
  • historic - 表示这个 hook 是需要保存call history 的,当有新的 plugin 注册的时候,需要回放历史

firstresult

调整一下 HookSpec,添加 firstresult参数,我们看一下执行结果

plaintext
class Spec:
@spec(firstresult)
def calculate(self, a, b):
pass

class HookImpl1:
@impl(tryfirst=True)
def calculate(self, a, b):
return a + b

Output

shell
[2]

关于 plugin的基本使用就先介绍到这里了,大家有有兴趣可以看这篇文章,介绍的很细致 https://buildmedia.readthedocs.org/media/pdf/pluggy/latest/pluggy.pdf


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK