[pytest源碼3]-pluggy代碼結構與核心設計

前言

現在我們開始分析,按照demo代碼順序先進行簡單分析。
個人拙見,有錯請各位指出。
如果的我的文章對您有幫助,不符動動您的金手指給個Star,予人玫瑰,手有餘香,不勝感激。 GitHub



pluggy代碼結構

按照前面demo中的代碼順序,在分析pluggy的核心邏輯之前,我們先來了解HookspecMarkerHookspecMarker的用處是什麼?


1.HookspecMarker的實現邏輯是什麼?

我們來先來看它的代碼註釋

class HookspecMarker(object):
      """ Decorator helper class for marking functions as hook specifications.

      You can instantiate it with a project_name to get a decorator.
      Calling PluginManager.add_hookspecs later will discover all marked functions
      if the PluginManager uses the same project_name.
      """

      def __init__(self, project_name):
          self.project_name = project_name
  • 我們可以傳入project_name實例化HookspecMarker以獲得裝飾器,當我們調用PluginManager.add_hookspec將會尋找所有與當前PluginManagerproject_name的標記函數,這也是前面要求整個項目project name一致的原因之一。
def __call__(
        self, function=None, firstresult=False, historic=False, warn_on_impl=None
    ):
        """ if passed a function, directly sets attributes on the function
        which will make it discoverable to add_hookspecs().  If passed no
        function, returns a decorator which can be applied to a function
        later using the attributes supplied.

        If firstresult is True the 1:N hook call (N being the number of registered
        hook implementation functions) will stop at I<=N when the I'th function
        returns a non-None result.

        If historic is True calls to a hook will be memorized and replayed
        on later registered plugins.

        """

        def setattr_hookspec_opts(func):
            if historic and firstresult:
                raise ValueError("cannot have a historic firstresult hook")
            setattr(
                func,
                self.project_name + "_spec",
                dict(
                    firstresult=firstresult,
                    historic=historic,
                    warn_on_impl=warn_on_impl,
                ),
            )
            return func

        if function is not None:
            return setattr_hookspec_opts(function)
        else:
            return setattr_hookspec_opts
  • 通過分析__call__的邏輯代碼可以發現,主要功能是調用了一個setattr(object, name, value),給被裝飾的函數新增一個屬性project_nam + _spec,並且該屬性的value爲裝飾器參數取值。


2.HookspecMarker的實現邏輯是什麼?

HookimplMarker的實現邏輯類似,區別在於被裝飾的函數新增的屬性爲project_name + _impl,下面只顯示了部分代碼

        def setattr_hookimpl_opts(func):
            setattr(
                func,
                self.project_name + "_impl",
                dict(
                    hookwrapper=hookwrapper,
                    optionalhook=optionalhook,
                    tryfirst=tryfirst,
                    trylast=trylast,
                ),
            )
            return func
            
        if function is None:
            return setattr_hookimpl_opts
        else:
            return setattr_hookimpl_opts(function)



pluggy核心設計

plugy的核心邏輯就是幾行代碼

pm = PluginManager("myPluggyDemo")
pm.add_hookspecs(HookSpec)
pm.register(HookImpl1())
pm.hook.calculate(a=2, b=3)
  • 創建一個PluginManager對象,用於管理plugin
  • 調用add_hookspecs, 增加一個新的hook module object(標準對象)
  • 調用register,註冊一個新的plugin object
  • 通過pm.hook實現對與calculate同名的所有plugin的調用

按照上面的代碼邏輯來走,我們來分析三行代碼的實現,以幫助我們更好的理解

  1. pm.add_hookspecs(HookSpec)是怎麼實現的?

  2. pm.register(HookImpl1())是怎麼實現的?

  3. pm.hook.calculate(a=2, b=3)是怎麼實現的?



1.PluginManager.add_hookspecs()是怎麼實現的?

Demo中的pm.add_hookspecs(HookSpec)是怎麼實現的?

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):          #1.遍歷傳入對象的所有屬性方法列表
        spec_opts = self.parse_hookspec_opts(module_or_class, name)        #2.拿到我們前面在HookspecMarker爲函數新增的那個屬性project_name + _spec
  • 遍歷傳入對象的所有屬性方法列表
  • 拿到每個屬性方法,若有特殊屬性project_name + _spec,則返回它,否則返回None,下面是該方法的代碼展示
def parse_hookspec_opts(self, module_or_class, name):        #2.1拿到該屬性的方法實現
   method = getattr(module_or_class, name)        #此處獲取到我們之前定義的hook方法
   return getattr(method, self.project_name + "_spec", None)        #此處獲取到爲該方法新增的屬性project_name + _spec



2.PluginManager.register()是怎麼實現的?

Demo中的pm.register(HookImpl1())是怎麼實現的?

pm.register的作用是註冊一個pluggy的實現並將其與對應的hook關聯起來,我們來看主要代碼

# register matching hook implementations of the plugin
self._plugin2hookcallers[plugin] = hookcallers = []
for name in dir(plugin):
    hookimpl_opts = self.parse_hookimpl_opts(plugin, name)    #獲取pluggy的屬性或方法中的特殊attribute project_name + _impl
    if hookimpl_opts is not None:
        normalize_hookimpl_opts(hookimpl_opts)
        method = getattr(plugin, name)    #特殊attribute存在時獲取到plugin的對應方法
        hookimpl = HookImpl(plugin, plugin_name, method, hookimpl_opts)
        hook = getattr(self.hook, name, None)
        if hook is None:
            hook = _HookCaller(name, self._hookexec)    #爲hook添加一個_HookCaller對象
            setattr(self.hook, name, hook)
        elif hook.has_spec():
            self._verify_hook(hook, hookimpl)
            hook._maybe_apply_history(hookimpl)
        hook._add_hookimpl(hookimpl)    #將hookimpl添加到hook中
        hookcallers.append(hook)    #將遍歷找到的每一個plugin hook添加到hookcallers,以待調用
  • 遍歷pluggy對象的所有屬性或方法(method),並獲取該pluggy method的特殊attribute project_name + _impl
  • 將帶有project_name + _impl的method封裝成一個HookImpl中
  • 再把一個_HookCaller的對象添加到hook中,併爲self.hook新增一個value爲hook,name爲method的屬性(比如前面的demo的calculate
  • 最後將遍歷找到的每一個_HookCaller添加到hookcallers,以待調用



3.PluginManager.hook.method()是怎麼實現的?

pm.hook是什麼?實現調用pluggy的邏輯是什麼?

這裏就涉及到了上一步的_HookCaller了,pm.hook.calculate其實是相當於獲取了對應_HookCaller,調用的是他的__call__方法,來看下代碼
    def __call__(self, *args, **kwargs):
        if args:      #只能傳入鍵值對形式的參數
            raise TypeError("hook calling supports only keyword arguments")
        assert not self.is_historic()
        if self.spec and self.spec.argnames:
            notincall = (
                set(self.spec.argnames) - set(["__multicall__"]) - set(kwargs.keys())
            )
            if notincall:
                warnings.warn(
                    "Argument(s) {} which are declared in the hookspec "
                    "can not be found in this hook call".format(tuple(notincall)),
                    stacklevel=2,
                )
        return self._hookexec(self, self.get_hookimpls(), kwargs)

核心代碼在最後一行,我們再來看看self._hhokexec是什麼,發現它是在構造_HookCaller時傳入的一個參數,再找到它的定義

    def _hookexec(self, hook, methods, kwargs):
        # called from all hookcaller instances.
        # enable_tracing will set its own wrapping function at self._inner_hookexec
        return self._inner_hookexec(hook, methods, kwargs)

順着走到最後,發現核心其實是hook.multicall

self._inner_hookexec = lambda hook, methods, kwargs: hook.multicall(
            methods,
            kwargs,
            firstresult=hook.spec.opts.get("firstresult") if hook.spec else False,
        )

這是一個PluggyManager構建時的封裝函數_multicall,代碼實現如下,詳細邏輯留到後面再講。

def _multicall(hook_impls, caller_kwargs, firstresult=False):
    """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()



GitHubhttps://github.com/potatoImp/pytestCodeParsing

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章