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

前言

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

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

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

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 的执行结果,这里就是 Impl1Impl2,从实际的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]

总结

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

Author: 妞爸爸
Link: http://markshao.github.io/2019/10/01/pluggy-guideline/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.