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主要的设计思路都介绍完了,后面会对于几个核心类的实现做进一步的分析,欢迎有兴趣的同学和我留言