pytest系列——pluggy插件源码解读(二)PluginManager类实例化

PluginManager类简介

首先还是把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)

从上面的使用代码可以看出,通过HookspecMarker类和HookimplMarker实例化了两个对象,通过源码解读(1)知道,这两个实例实质上是两个装饰器,装饰器的作用是给函数设置一个名称为”{project_name}_spec”和”{project_name}_impl”属性

紧接着就是定义的子集的Spec类了,这个类实质上类似于面向对象编程中的定义接口,相当于在这个类中可以定义好许多接口(方法),即只要将自定义的Spec类中的方法加上@hookspec即相当于成为了插件的接口

然后即可以开始定义插件类了,插件类当然需要去对接口类中定义的方法去做接口的实现,即实现接口在不同的插件中的具体功能实现,即在插件中对方法加上@hookimpl装饰即相当于就约定好了是接口的实现(理解上可以这么理解,具体实现后面都会讲到)

紧接着就是到了PluginManager类的实例化了,这个类是pluggy模块中最最核心的一个类,它相当于pluggy的中枢大脑,pluggy的所有动作指令都是从这个类中发出的,这个类的是在manager.py文件中定义的

下面就是PluginManager类的初始化函数的源码(这个类还有很多功能代码,这里先只讲初始化,所以先只放这一小段源码)

class PluginManager:
    """ Core :py:class:`.PluginManager` class which manages registration
    of plugin objects and 1:N hook calling.

    You can register new hooks by calling :py:meth:`add_hookspecs(module_or_class)
    <.PluginManager.add_hookspecs>`.
    You can register plugin objects (which contain hooks) by calling
    :py:meth:`register(plugin) <.PluginManager.register>`.  The :py:class:`.PluginManager`
    is initialized with a prefix that is searched for in the names of the dict
    of registered plugin objects.

    For debugging purposes you can call :py:meth:`.PluginManager.enable_tracing`
    which will subsequently send debug information to the trace helper.
    """

    def __init__(self, project_name):
        self.project_name = project_name
        self._name2plugin = {}
        self._plugin2hookcallers = {}
        self._plugin_distinfo = []
        self.trace = _tracing.TagTracer().get("pluginmanage")
        self.hook = _HookRelay()
        self._inner_hookexec = _multicall

PluginManager 类实例化的时候主要是初始化了几个变量:

  • project_name 可以理解为项目名称
  • _name2plugin 是一个字典,主要用于存放插件名称和插件对象的映射关系
  • _plugin2hookcallers 是也是一个字典,主要用于存放插件对象和插件对象对应的调用钩子函数的映射关系
  • _plugin_distinfo 是一个列表,用来存放通过setuptools注册的插件的信息
  • hook 是 _HookRelay类的实例,这个类的代码在hooks.py文件中,这个是一个空类,目的是用于存放hook函数的,所以主要用在后面注册插件的时候给这个空类的实例加设置属性的

_HookRelay 类的源代码如下:

class _HookRelay:
    """ hook holder object for performing 1:N hook calls where N is the number
    of registered plugins.

    """
  • _inner_hookexec 是一个函数,即是_multicall,这个函数的代码在callers.py文件中,这函数是整个pluggy插件模块最最核心的一个函数,所有的被注册的插件中的接口的执行顺序以及结果返回等等逻辑都在这个函数中

_multicall 的代码如下,这里先暂时不分析这个函数,现在只要知道_inner_hookexec属性其实就是_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()

至此,PluginManager类的实例化流程就完成了

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