pytest系列——pluggy插件源码解读(三)add_hookspecs增加自定义的接口类

pluggy使用举例子代码:
下面这个例子中前面已经分析完了,下面的步骤就是pm.add_hookspecs(MySpec) 这个一步骤了,同样,这个add_hookspecs方法也是PluginManager类的一个方法,下面就针对这个函数进行分析

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)

首先看下这个函数的源代码(如下):

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)
        )

为了详细的分析这段代码,先看一下python的一些常用语法或python的基本功底

  • dir 内置函数
    dir内置函数可以查看一个类或者对象的所欲属性或者方法
    首先看下如下一小片段代码:
class Test(object):
    def __init__(self):
        pass

    def test(self):
        pass

print(dir(Test))

执行结果为:

['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'test']

可见,dir函数可以获得一个类或者对象的所有属性和方法,比如这里我们定义的test函数同样也是在dir的结果列表中,在注意一下,dir的记过是一个列表,列表中的元素时这个类或者对象的所有属性或者方法的名称,比如这里的“test”是一个字符串,并不是test函数对象,这一点需要明确

  • setattr和getattr是给一个对象或者实例设置属性或者获取属性的,具体用例这里不再详细演示了

接下来就开始详细的分析pluggy中的add_hookspecs这个方法源码了

  1. 这个方法形参是module_or_class,而从使用实例可以知道,这个参数传进来的是我们自己定会的Spec类,即自己定义的接口类

  2. for循环中的每次循环的name是接口类的属性或者方法的名称,注意这个name是一个字符串

  3. 获取spec_opts的函数 parse_hookspec_opts 的代码如下:

def parse_hookspec_opts(self, module_or_class, name):
    method = getattr(module_or_class, name)
    return getattr(method, self.project_name + "_spec", None)

这里首先根据自定义接口类的方法或者属性的名称获取其对应的对象,然后根据project_name+”_spec”获取对象的属性,这里就是前面讲到的HookspecMarker类会给自定义接口类总的接口函数设置一个属性,正是这个属性,只要在自定义的接口类中的接口方法上加了HookspecMarker生成的装饰器,则此接口函数就拥有了project_name+”_spec”的属性,其属性值为firstresult,historic,warn_on_impl这三个字符串作为key的一个字典,如果没有加装饰器装饰的则就不会拥有这个属性

  1. 通过判断spec_opts是否为None,即当获取到的属性不为None时,则此方法就是装饰器类装饰过的即自定义的接口类的接口方法

  2. hc在从hook获取name对应的属性,显然前面说过hook本质是一个空类对应的实例,显然此时hc是None

  3. 当hc为None时,正常情况下hc都为None,只有一种情况,即前面已经调用过add_hookspecs方法,而由于代码量很大,导致后面或者其他
    文件中忘记是否调用过,则会出现hc不为None即走else分支,这里我们按照正常流程应该为None,此时hc继续实例化为_HookCaller类的一个实例

_HookCaller类实例化的源代码如下(这里只列出此类的实例化用到的init方法,此类还有其他方法,这里不再列出):

class _HookCaller:
    def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None):
        self.name = name
        self._wrappers = []
        self._nonwrappers = []
        self._hookexec = hook_execute
        self._call_history = None
        self.spec = None
        if specmodule_or_class is not None:
            assert spec_opts is not None
            self.set_specification(specmodule_or_class, spec_opts)

hc在实例化的时候主要也是对一些变量做初始化赋值,下面简单介绍一个每个参数的作用

  • name 就是接口类中接口函数的名称,字符串,即add_hookspecs函数中for循环的变量name
  • _wrappers和_nonwrappers是两个列表,用于存放两种类型的实现函数,即在前面讲过的HookimplMarker装饰器类中,设置属性的时候有个hookwrapper=False的参数
    如果设置为False,则此对应的函数实现则放到_nonwrappers列表中,如果设置为True,则存放到_wrappers列表中
  • _hookexec 即为PluginManager实例的的_inner_hookexec属性,亦即callers.py文件中的_multicall函数
  • _call_history 后续是为了通过判断自定义接口类中接口函数的historic属性而进行设置,如果historic属性为True,则此属性设置为列表,起始值设置为None
  • spec是用于HookSpec类的实例的,起始值设置为None

接下来有判断,当传过来的接口类和接口的属性均非空的时候则进行spec的设置,设置的代码如下:

def set_specification(self, specmodule_or_class, spec_opts):
    assert not self.has_spec()
    self.spec = HookSpec(specmodule_or_class, self.name, spec_opts)
    if spec_opts.get("historic"):
        self._call_history = []

此时可以发现_HookCaller类的spec属性已经设置值了,即为HookSpec类的一个实例,而_HookCaller类则纯粹是一个设置属性的类

class HookSpec:
    def __init__(self, namespace, name, opts):
        self.namespace = namespace
        self.function = function = getattr(namespace, name)
        self.name = name
        self.argnames, self.kwargnames = varnames(function)
        self.opts = opts
        self.warn_on_impl = opts.get("warn_on_impl")

在HookSpec类中的参数设置解释:

  • namespace,命名空间,通过调用函数知这个命名空间其实就是前面定义的Spec接口类,注意这个namespace是一个类对象,而不是字符串
  • function ,即接口类中的接口方法,是函数对象,不是字符串
  • name ,即接口函数名,是字符串
  • argnames和kwargnames则是function函数的参数,可变参数和键值对参数,通过varnames函数解析的
def varnames(func):
    """Return tuple of positional and keywrord argument names for a function,
    method, class or callable.

    In case of a class, its ``__init__`` method is considered.
    For methods the ``self`` parameter is not included.
    """
    if inspect.isclass(func):
        try:
            func = func.__init__
        except AttributeError:
            return (), ()
    elif not inspect.isroutine(func):  # callable object?
        try:
            func = getattr(func, "__call__", func)
        except Exception:
            return (), ()

    try:  # func MUST be a function or method here or we won't parse any args
        spec = inspect.getfullargspec(func)
    except TypeError:
        return (), ()

    args, defaults = tuple(spec.args), spec.defaults
    if defaults:
        index = -len(defaults)
        args, kwargs = args[:index], tuple(args[index:])
    else:
        kwargs = ()

    # strip any implicit instance arg
    # pypy3 uses "obj" instead of "self" for default dunder methods
    implicit_names = ("self",) if not _PYPY else ("self", "obj")
    if args:
        if inspect.ismethod(func) or (
            "." in getattr(func, "__qualname__", ()) and args[0] in implicit_names
        ):
            args = args[1:]

    return args, kwargs

这个函数主要是inspect模块的应用以及常用的属性分析方法,这里不再详细讲这个函数了

  • opts 则是前面讲的装饰器设置的属性,亦即spec_opts
  • warn_on_impl 则是spec_opts中的一个参数

OK,现在,我们开始从宏观层面总结一下add_hookspecs做了哪些事情

  1. 首先分析出自定义的Spec类中定义的通过装饰器装饰的接口函数
  2. 然后给PluginManager对象的hook成员增加一个1)中分析出来的接口函数属性,即hook拥有了Spec中定义的接口函数名的属性,属性对应的就是值是_HookCaller对象
  3. _HookCaller对象中存放中两种不同类型的接口wrapers类型和nonwrapper类型
  4. _HookCaller对象的spec属性则存放了接口函数名,接口函数对象,接口函数的参数

至此 add_hookspecs方法就分析完成了

posted @ 2022-09-01 16:04  观棋不雨  阅读(265)  评论(0编辑  收藏  举报