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的钩子函数的执行过程的源码分析就到这里了

posted @ 2022-09-02 13:44  观棋不雨  阅读(331)  评论(0编辑  收藏  举报