pytest系列——pluggy插件源码解读(五)hook钩子函数调用执行过程分析
经过pluggy源码解读系列1-4的分析,已经完成插件定义、spec定义,插件注册等环节,下面就到了调用插件执行了,即hook钩子函数是如何被调用执行的,下面还是先把pluggy使用的代码放下面:
import pluggy
# HookspecMarker 和 HookimplMarker 实质上是一个装饰器带参数的装饰器类,作用是给函数增加额外的属性设置
hookspec = pluggy.HookspecMarker("myproject")
hookimpl = pluggy.HookimplMarker("myproject")
# 定义自己的Spec,这里可以理解为定义接口类
class MySpec:
# hookspec 是一个装饰类中的方法的装饰器,为此方法增额外的属性设置,这里myhook可以理解为定义了一个接口
@hookspec
def myhook(self, arg1, arg2):
pass
# 定义了一个插件
class Plugin_1:
# 插件中实现了上面定义的接口,同样这个实现接口的方法用 hookimpl装饰器装饰,功能是返回两个参数的和
@hookimpl
def myhook(self, arg1, arg2):
print("inside Plugin_1.myhook()")
return arg1 + arg2
# 定义第二个插件
class Plugin_2:
# 插件中实现了上面定义的接口,同样这个实现接口的方法用 hookimpl装饰器装饰,功能是返回两个参数的差
@hookimpl
def myhook(self, arg1, arg2):
print("inside Plugin_2.myhook()")
return arg1 - arg2
# 实例化一个插件管理的对象,注意这里的名称要与文件开头定义装饰器的时候的名称一致
pm = pluggy.PluginManager("myproject")
# 将自定义的接口类加到钩子定义中去
pm.add_hookspecs(MySpec)
# 注册定义的两个插件
pm.register(Plugin_1())
pm.register(Plugin_2())
# 通过插件管理对象的钩子调用方法,这时候两个插件中的这个方法都会执行,而且遵循后注册先执行即LIFO的原则,两个插件的结果讲义列表的形式返回
results = pm.hook.myhook(arg1=1, arg2=2)
print(results)
通过上面的例子,可以看出,最后一个步骤就是通过PluginManager实例的pm的一个hook属性调用myhook函数,而myhook即定义的接口函数,在这个例子中,这个接口函数在pluggin_1和pluggin_2两个插件中都有实现,则这里两个插件的myhook函数都会执行,执行的顺序也是后讲究的,那么这些流程的控制执行等都本节详细讲述
现在先回头再看一下,在分析add_hookspecs方法的时候讲到,首先hook是PluginManager类的一个实例,这个比较好理解,下面是add_hookspecs方法的源代码,这个在前面都已经详细的分析过了,这里放这里再简单回顾一下,通过下面的代码可以发现,就是在这个函数中给hook设置了接口函数myhook的属性,myhook的属性值是_HookCaller类的一个实例,那么这里一个实例为什么当做函数调用了呢,这就涉及到python的高级语法中call魔法函数的应用了
def add_hookspecs(self, module_or_class):
""" add new hook specifications defined in the given ``module_or_class``.
Functions are recognized if they have been decorated accordingly. """
names = []
for name in dir(module_or_class):
spec_opts = self.parse_hookspec_opts(module_or_class, name)
if spec_opts is not None:
hc = getattr(self.hook, name, None)
if hc is None:
hc = _HookCaller(name, self._hookexec, module_or_class, spec_opts)
setattr(self.hook, name, hc)
else:
# plugins registered this hook without knowing the spec
hc.set_specification(module_or_class, spec_opts)
for hookfunction in hc.get_hookimpls():
self._verify_hook(hc, hookfunction)
names.append(name)
if not names:
raise ValueError(
"did not find any %r hooks in %r" % (self.project_name, module_or_class)
)
前面也都分析过call的应用,所以这里就是应用了这个特点,即把_HookCaller类的一个实例当做函数调用,实质上就是调用了_HookCaller类的call魔法函数,这里把_HookCaller类的call方法的代码放到下面,前面层提过,这个方法是整个pluggy最最核心的一个函数(pluggy最最核心的类是PluginManager类,它是插件管理注册等等控制类,而pluggy最最核心的函数就是_HookCaller类的call函数了,它控制了整个插件系统的钩子函数的执行过程)
def __call__(self, *args, **kwargs):
if args:
raise TypeError("hook calling supports only keyword arguments")
assert not self.is_historic()
# This is written to avoid expensive operations when not needed.
if self.spec:
for argname in self.spec.argnames:
if argname not in kwargs:
notincall = tuple(set(self.spec.argnames) - kwargs.keys())
warnings.warn(
"Argument(s) {} which are declared in the hookspec "
"can not be found in this hook call".format(notincall),
stacklevel=2,
)
break
firstresult = self.spec.opts.get("firstresult")
else:
firstresult = False
return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)
下面就对这个函数做详细的分析
- 首先这个函数的前两行就限定了插件中定义的函数的参数必须是key-value键值对的形式,不支持可变参数的形式
- 然后就是对参数做分析,主要就是分析出firstresult的值是True还是False
- 下面就是调用self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)函数了这里,首先name就是接口函数的名字,比如这里就是myhook字符串
下面看下第二个参数,第二个参数是一个函数,这个函数的代码如下:这里可以看出,这里就是上一节分析的注册函数的列表,所以这个返回的是一个实现函数的列表,第三个参数是函数的参数,第四个参数就是firstresult值
def get_hookimpls(self):
# Order is important for _hookexec
return self._nonwrappers + self._wrappers
下面就到了最最核心的函数了,即hookexec函数,通过前面几节的分析,已经知道这个函数就是callers.py文件中的_multicall函数,代码如下:
def _multicall(hook_name, hook_impls, caller_kwargs, firstresult):
"""Execute a call into multiple python functions/methods and return the
result(s).
``caller_kwargs`` comes from _HookCaller.__call__().
"""
__tracebackhide__ = True
results = []
excinfo = None
try: # run impl and wrapper setup functions in a loop
teardowns = []
try:
for hook_impl in reversed(hook_impls):
try:
args = [caller_kwargs[argname] for argname in hook_impl.argnames]
except KeyError:
for argname in hook_impl.argnames:
if argname not in caller_kwargs:
raise HookCallError(
"hook call must provide argument %r" % (argname,)
)
if hook_impl.hookwrapper:
try:
gen = hook_impl.function(*args)
next(gen) # first yield
teardowns.append(gen)
except StopIteration:
_raise_wrapfail(gen, "did not yield")
else:
res = hook_impl.function(*args)
if res is not None:
results.append(res)
if firstresult: # halt further impl calls
break
except BaseException:
excinfo = sys.exc_info()
finally:
if firstresult: # first result hooks return a single value
outcome = _Result(results[0] if results else None, excinfo)
else:
outcome = _Result(results, excinfo)
# run all wrapper post-yield blocks
for gen in reversed(teardowns):
try:
gen.send(outcome)
_raise_wrapfail(gen, "has second yield")
except StopIteration:
pass
return outcome.get_result()
这个最核心的函数,其实也是比较容易看懂的,只要前几节的分析大概都还有个印象,那么这个函数还是比较容易理解的
首先定义个一个结果列表,用于存放每个插件的实现函数执行的结果
然后定义了个teardown的列表,用于存放执行teardown操作的操作对象
然后将hook_impls即插件中对接口函数的实现函数倒序遍历,这也是看很多文档博客会说pluggy插件执行的顺序是后注册先执行的原因,然后开始解析函数的参数
然后判断实现函数的hookwrapper属性值是否为True,如果为True表示此函数带有yield关键字,即首先执行yield之前的代码,然后会生成一个对象,即生成器,将生成的对象存放teardowns,用于所有插件之后再来执行这些操作,这也就是为什么网上很多博客等说的pluggy的插件实现函数中如果带有yield,则yield之后的代码会在所有的普通插件执行完成之后再去执行。else分支就是不带yield关键字的实现函数,则执行执行,并且将结果存放到results列表,同时如果判断firstresult结果为True,则结束循环,即执行得到一个结果即OK
当然如果firstresult为False,则所有的插件注册的函数都会执行的
在finnally代码块中可以看到,如果firstresult结果为True,则直接返回第一个结果,而如果firstresult为False,则会讲所有的结果以列表的形式返回
最后再去倒序遍历执行teardown列表中存放的操作,即当带有多个yield关键字插件的时候,后注册的yeild之后的代码先执行
最后将结果返回
ok,pluggy的钩子函数的执行过程的源码分析就到这里了