pytest系列——pluggy插件源码解读(四)register注册插件源码解析

首先还是把pluggy使用的代码放在这,前面已经分析完add_hookspecs的源代码,下面紧接着就是注册插件了

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)

在分析注册插件函数的源代码之前,先看一下上面的代码中定义的两个插件类:Plugin_1和Plugin_2,这两个类中都定义了myhook方法,而这个方法在MySpec中也定义了(但未实现),在这两个类中这个myhook方法有具体的实现
这其实也就是pluggy插件系统中MySpec和Plugin_1类的本质含义,看名称有时候可能会觉很难理解,其实Spec和插件的本质就是定义接口和接口实现,换言之Spec类就是定义接口的,而不同的插件类中定义与Spec类中同名方法,本质上其实就是不同插件对Spec类中定义的方法做不同的实现,“impl”这个简写其实就是实现的英文”implementation”的简写

插件类中的方法使用了@hookimpl装饰器,而由pluggy源码解读系列(1)-HookspecMarker类和HookimplMarker类分析 分析知@hookimpl装饰器其实就是给被装饰函数增加了一个project_name + “_impl”的属性,其属性值由hookwrapper,optionalhook,tryfirst,trylast,specname这几个字段组成的字典,其默认值为None或者False

所以插件类中对接口类中接口的实现方法加上装饰器装饰之后,定义的插件类的接口实现函数也就多了这么一个属性

下面就开始来分析一下注册的过程,首先看下register的源码,同样这也是PluginManager类的一个方法,所以说PluginManager类是pluggy模块一个非常非常核心的类

def register(self, plugin, name=None):
    """ Register a plugin and return its canonical name or ``None`` if the name
    is blocked from registering.  Raise a :py:class:`ValueError` if the plugin
    is already registered. """
    plugin_name = name or self.get_canonical_name(plugin)

    if plugin_name in self._name2plugin or plugin in self._plugin2hookcallers:
        if self._name2plugin.get(plugin_name, -1) is None:
            return  # blocked plugin, return None to indicate no registration
        raise ValueError(
            "Plugin already registered: %s=%s\n%s"
            % (plugin_name, plugin, self._name2plugin)
        )

    # XXX if an error happens we should make sure no state has been
    # changed at point of return
    self._name2plugin[plugin_name] = plugin

    # register matching hook implementations of the plugin
    self._plugin2hookcallers[plugin] = hookcallers = []
    for name in dir(plugin):
        hookimpl_opts = self.parse_hookimpl_opts(plugin, name)
        if hookimpl_opts is not None:
            normalize_hookimpl_opts(hookimpl_opts)
            method = getattr(plugin, name)
            hookimpl = HookImpl(plugin, plugin_name, method, hookimpl_opts)
            name = hookimpl_opts.get("specname") or name
            hook = getattr(self.hook, name, None)
            if hook is None:
                hook = _HookCaller(name, self._hookexec)
                setattr(self.hook, name, hook)
            elif hook.has_spec():
                self._verify_hook(hook, hookimpl)
                hook._maybe_apply_history(hookimpl)
            hook._add_hookimpl(hookimpl)
            hookcallers.append(hook)
    return plugin_name

首先看下register的参数,可以接受一个插件对象plugin和名称name,name可选,在我们的例子中只传进来一个插件类的实例
如果传入的name参数,则plugin_name的值就取name,否则,则通过plugin对象来取,
通过插件对象来取的函数代码如下,其实很简单,就是取对象的name属性值,如果这个属性值不存在则去其对象的id(id是python中判断身份的唯一标识,任何对象都会有自己的id,判断两个对象是否为同一个其实就是通过id这个内置函数判断的,比较简单,这个就不详细展开了)

def get_canonical_name(self, plugin):
    """ Return canonical name for a plugin object. Note that a plugin
    may be registered under a different name which was specified
    by the caller of :py:meth:`register(plugin, name) <.PluginManager.register>`.
    To obtain the name of an registered plugin use :py:meth:`get_name(plugin)
    <.PluginManager.get_name>` instead."""
    return getattr(plugin, "__name__", None) or str(id(plugin))

紧接着是做了一下异常判断,前面分析PluginManager类初始化的时候层分析过,_name2plugin属性时存放名称和插件对象的映射关系的字典,_plugin2hookcallers是存放插件对象和其调用函数的映射的字典,
这里判断如果插件名称已经在_name2plugin中,或者插件对象已经在_plugin2hookcallers中,则说明此插件已经注册过了,这个时候继续判断一下如果当前名称的插件对象是None,则说明当前插件是存在阻塞状态的(pluggy是提供了阻塞方法的,这个后面再详细分析),
当然如果不是阻塞状态的,重复注册的时候这里会报出异常,提示已经注册过了

异常判断之后即开始注册了,下面可以看到_name2plugin属性里直接存放了名称和插件对象的映射关系,而_plugin2hookcallers的属性首先给插件对象plugin字段设置了一个空列表的值,显然这个将是存放多个调用函数的

下面紧接着是一个for循环,这个for循环pluggy源码解读系列(3)–add_hookspecs增加自定义的接口类 中的for循环就很类似了,
首先获取plugin对象的所有方法和属性,然后获取project_name + “_impl”属性,如果存在这个属性,说明就是插件类中对接口的实现方法,然后对hookimpl_opts做了一下规范化处理,其实就是设置默认值,接下来就是根据实现接口库的函数名获取函数对象method,再接下来就是实例化了一个HookImpl对象hookimpl,HookImpl类的代码如下:

class HookImpl:
    def __init__(self, plugin, plugin_name, function, hook_impl_opts):
        self.function = function
        self.argnames, self.kwargnames = varnames(self.function)
        self.plugin = plugin
        self.opts = hook_impl_opts
        self.plugin_name = plugin_name
        self.__dict__.update(hook_impl_opts)

即HookImpl的实例化是为了存放实现接口的函数独享,插件,函数参数,同步更新了hook_impl_opts属性

接下来更新了一下name,即如果hookimpl_opts中设置了specname属性,则将name更新为它,否则继续保持name就是函数名

hook取PluginManager类的hook属性,显然在PluginManager初始化的时候就已经存在hook属性了,所以下面直接代码直接走到
hook._add_hookimpl(hookimpl)这一行,在分析_add_hookimpl的具体实现之前,先看下下面一行代码,此时将hookimpl实例存放到hookcallers列表中了

下面看下_add_hookimpl的实现,这个方法是_HookCaller类的一个方法,在pluggy源码解读系列(3)–add_hookspecs增加自定义的接口类
分析过,_HookCaller对象的属性有_wrappers和_nonwrappers以及_call_history,他们都是列表的,当时解释过,就是为了根据接口实现方法的不同属性进行不同的存放的,
下面的代码可以看出,就是做这件事的,首先判断hookimpl.hookwrapper是否True还是False,这里用了一个中间变量methods,如果hookimpl.hookwrapper为True,则methods就指向_wrappers属性,
反之指向_nonwrappers,然后再根据trylast如果为真,则放到列表的第一个位置,这里提前透露一下,多个插件注册的时候如果都是默认设置的话是,后注册先执行的方式,所以如果trylast设置为True,表示这个插件的接口实现
努力靠后执行,所以就把它存放在了列表的第一个位置,同理如果tryfirst为True,则放到列表的末尾,如果tryfirst和trylast都是False,即默认情况下,下面的代码可以看出,是通过遍历,放到第一个tryfirst属性为True的前面的,
换据说在很多插件实现中,可以有很多插件设置tryfirst为True,也可以有很多插件设置trylast为True,所以说tryfirst是一个努力第一个执行,但并不保证是第一个执行,比如注册了多个tryfirst为True的,那么最后注册的那个tryfirst为True的才是第一个执行的,
trylast也是同样的方式

def _add_hookimpl(self, hookimpl):
    """Add an implementation to the callback chain.
    """
    if hookimpl.hookwrapper:
        methods = self._wrappers
    else:
        methods = self._nonwrappers

    if hookimpl.trylast:
        methods.insert(0, hookimpl)
    elif hookimpl.tryfirst:
        methods.append(hookimpl)
    else:
        # find last non-tryfirst method
        i = len(methods) - 1
        while i >= 0 and methods[i].tryfirst:
            i -= 1
        methods.insert(i + 1, hookimpl)

OK,至此注册的代码就分析完了,下面简单的总结一下注册函数都做了啥:

  • 核心就是将注册插件存放到PluginManager对象的_name2plugin属性和_plugin2hookcallers属性
  • 存放_plugin2hookcallers属性的时候,key是插件对象,value是hook列表,同时实例化了HookImpl对象,这个对象用于存放实现函数的函数,参数等信息,然后将其加入到hook中

至此,register注册插件的流程就分析完成了

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