[pytest源碼4]-pluggy之Plugin註冊邏輯分析

前言

本篇將詳細對plugin的註冊邏輯進行分析
個人拙見,有錯請各位指出。
如果的我的文章對您有幫助,不符動動您的金手指給個Star,予人玫瑰,手有餘香,不勝感激。 GitHub



pluggy註冊邏輯分析性

我們來詳細分析一下plugin的註冊邏輯register方法

下面以分片段的形式呈現

  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)
      )
  • 根據傳入plugin name或由plugin對象獲取到插件名,將其賦值給plugin_name
  • self._name2plugin是以plugin_name爲key的dict
  • self._pluginhookcallers是以plugin object爲key的dict
  • 通過上述兩個dict來判斷傳入的plugin是否已經註冊過了



  self._name2plugin[plugin_name] = plugin
  • 將這個pluggy以plugin_name:plugin object的形式保存到self._name2plugin



  self._plugin2hookcallers[plugin] = hookcallers = []
  • 創建一個list對象hookcallers用來保存每個pluggy的實際調用對象_HookCaller,以plugin object:hookcallers object的形式保存在self._plugin2hookcallers



  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 name的屬性(比如前面的demo的calculate
  • 最後將遍歷找到的每一個_HookCaller添加到hookcallers,以待調用



我們來分析下上面代碼提到的兩個對象HookImpl與_HookCaller

首先看下HookImpl的實現,其實就是一個數據封裝類

  class HookImpl(object):
      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)

      def __repr__(self):
          return "<HookImpl plugin_name=%r, plugin=%r>" % (self.plugin_name, self.plugin)




最後看看核心_HookCaller的實現,它是整個plugin的核心類

  class _HookCaller(object):
      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.argnames = None
          self.kwargnames = None
          self.multicall = _multicall
          self.spec = None
          if specmodule_or_class is not None:
              assert spec_opts is not None
              self.set_specification(specmodule_or_class, spec_opts)
  • 在register邏輯中,我們傳入的參數是name與hook_execute(保存在self._hookexec中),其中hook_execute表示的是一個函數對象,負責實際plugin的調用



    def has_spec(self):
        return self.spec is not None

    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 = []

    def is_historic(self):
        return hasattr(self, "_call_history")
  • 增加調用歷史記錄,返回調用歷史記錄



    def _add_hookimpl(self, hookimpl):
        """Add an implementation to the callback chain.
        """
        if hookimpl.hookwrapper:    #根據hookimpl.hookwrapper將plugin分爲兩類
            methods = self._wrappers
        else:
            methods = self._nonwrappers

        if hookimpl.trylast:    #根據參數爲plugin調整對應的執行順序
            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)

        if "__multicall__" in hookimpl.argnames:    #版本更新做的處理,不再支持__multicall__的參數方式
            warnings.warn(
                "Support for __multicall__ is now deprecated and will be"
                "removed in an upcoming release.",
                DeprecationWarning,
            )
            self.multicall = _legacymulticall 
  • 在構造函數中我們可以看到self._wrappersself._nonwrappers,通過_add_hookimpl我們將plugin分成兩類
  • 按照裝飾器傳入的參數對plugin的執行順序進行排序



    def _remove_plugin(self, plugin):
        def remove(wrappers):
            for i, method in enumerate(wrappers):
                if method.plugin == plugin:
                    del wrappers[i]
                    return True

        if remove(self._wrappers) is None:
            if remove(self._nonwrappers) is None:
                raise ValueError("plugin %r not found" % (plugin,))
  • remove plugin時需將_wrappers_nonwrappers兩類中的pluginremove
  • 通過遍歷大類中的methodplugin屬性來找到要刪除的plugin,通過del來刪除引用變量



    def call_extra(self, methods, kwargs):
        """ Call the hook with some additional temporarily participating
        methods using the specified kwargs as call parameters. """
        old = list(self._nonwrappers), list(self._wrappers)    #獲取到所有原始插件list
        for method in methods:    #遍歷plugin method
            opts = dict(hookwrapper=False, trylast=False, tryfirst=False)    #統一臨時plugin參數
            hookimpl = HookImpl(None, "<temp>", method, opts)    #創建臨時plugin實例
            self._add_hookimpl(hookimpl)    #默認排序plugin執行順序
        try:
            return self(**kwargs)    #返回插件增加了臨時plugin的插件引用
        finally:
            self._nonwrappers, self._wrappers = old    #執行完畢,恢復原始插件list
  • 有時我們會需要在某一次執行增加一些臨時的plugin,是Plugin爲我們提供一個方法call_extra
  • 首先獲取原本的plugin列表,在方法執行的最後需要恢復原來的plugin列表
  • 對我們傳入的臨時plugin method都統一創建默認執行順序的名爲""的臨時plugin object
  • 並將增加了臨時plugin_HookCaller object返回,以待調用



    def _maybe_apply_history(self, method):
        """Apply call history to a new hookimpl if it is marked as historic.
        """
        if self.is_historic():
            for kwargs, result_callback in self._call_history:
                res = self._hookexec(self, [method], kwargs)
                if res and result_callback is not None:
                    result_callback(res[0])
  • 判斷hook是否有指定參數
  • 遍歷調用歷史,得到插件執行的結果



GitHubhttps://github.com/potatoImp/pytestCodeParsing

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