Pytest 源码解读 [2] - [pluggy] 核心设计理念和代码结构
代码结构
pluggy
的核心代码非常简介,把 repo 克隆到本地目录后,它的核心代码就是在 src
目录下的4个文件
_tracing.py
- 调试作用,把 hookspec 和 plugin 调用链路的分析打印出来,一般不开启callers.py
- 主要是_multicall
这个方法,用来实现对于 plugin 的调用逻辑hooks.py
- 核心是HookspeckMarker
和HookimplMarker
, 用来装饰 hook 和 plugin 的方法manager.py
-PluginManager
是整个 pluggy 的核心类,负责 hook 和 plugin 的管理和核心调用逻辑
核心设计理念
整个 pluggy
的核心逻辑就是这么几行代码
1 | pm = PluginManager("pluggy_demo_1") |
- 创建一个
PluginManager
对象 - 调用
add_hookspecs
,注册一个新的 hook object - 调用
register
注册一个新的 plugin object - 通过
pm.hook.calculate
来实现对 plugin 的调用
这里就有几个核心设计的问题,通过解答这些问题可以帮助我们对于整个 pluggy
的核心设计原理有进一步的了解
pluggy
是怎么知道 Spec 里面哪些方法是定义的 hook ?pluggy
是怎么把 Plugin 的具体实现和 hook 做关联的实际调用的时候,
pm.hook
是个什么, 如何实现最终的 plugin 调用的
pluggy
是怎么知道 Spec 里面哪些方法是定义的 hook ?
这个就要全靠 HookspecMarker
和 HookimplMarker
这两个装饰器了,因为两者的实现逻辑类似,这里就只解释一下HookspecMarker
, 我们先看上篇博文的demo中,我们一开始就定义了2个装饰器,并用他们来注释了 hook的方法
1 | spec = HookspecMarker("pluggy_demo_1") |
这里HookspecMarker
是一个基于class实现的装饰器,我们看一下代码里它的具体实现逻辑 , __call__
方法,
1 | def __call__( |
原来主要的功能就是给被装饰的函数增加一个特别的属性, 属性的名字是 Project_name + _spec
, 属性的value 就是装饰器的参数取值,
这样在PluginManger
的 add_hookspecs
函数的逻辑中,我们就可以看到通过遍历对象的所以属性,找到含有这这个特殊的attribute 的方法就好了
1 | for name in dir(module_or_class): |
当然这里就有一个前提,全局的project_name 必须是一致的且唯一,因为这个key是拼出来的,如果不一致就找不到 对应的 hook 和 impl 定义了
pluggy
是怎么把 Plugin 的具体实现和 hook 做关联的 ?
首先我们先看一下 pm.hook
, 这个 hook 是个什么东西,我们在 PluginManager
的构造函数看到了hook
的定义
1 | self.hook = _HookRelay() |
看了一下_HookReplay
的代码,只有一个类的定义,看起来就是一个桩
,这里可以猜测一下,当我们真实调用pm.hook.calculate(a=1,b=2)
的时候,实际的调用可能是这样的
1 | getattr(pm.hook, "calculate")(a=1, b=2) |
为了印证我的猜测,我在 PluginManager
的 register
方法这找到了这段逻辑
1 | for name in dir(plugin): |
- 遍历 plugin 对象的所有属性(method), 通过
self.parse_hookimpl_opts
这个方法找到含有特殊 attribute 的方法,就是被impl
装饰器装饰过的方法 - 然后再把一个
_HookCaller
的对象添加到hook
对象中,setattr(self.hook, name, hook)
, 所以看起来真实的调用是_HookCaller(a=1, b=2)
, 具体我们下次再分析 - 如何实现
1:N
的调用关系呢,实际上一个hook 可以添加多个hookimpl
, 看这行代码就清楚了,它是在找到了hook
绑定后调用的hook._add_hookimpl(hookimpl)
实际调用的时候,pm.hook
是个什么, 如何实现最终的 plugin 调用的
前面说了实际上 pm.hook.calculate
获取的是一个 _HookCaller
对象,所以真实的是调用了它的__call__
方法,我们简单看一下它的逻辑
1 | def __call__(self, *args, **kwargs): |
核心代码就是最后一行, self._hookexec
,把具体的kwargs
传入,来调用实际的 plugin 实现,具体的这个 self._hookexec
方法,是在构造_HookCaller
的时候作为一个参数传入的,它的定义看起来也只是一个封装
1 | def _hookexec(self, hook, methods, kwargs): |
真正的逻辑是 PluginManger
构造时候封装的 _multicall
函数,这个具体我们下次再分析它是如何实现的
1 | self._inner_hookexec = lambda hook, methods, kwargs: hook.multicall( |
总结
到这里我们把pluggy
主要的设计思路都介绍完了,后面会对于几个核心类的实现做进一步的分析,欢迎有兴趣的同学和我留言