[pytest源碼5]-pluggy深挖調用邏輯

前言

本篇將詳細分析如何通過PluginManager.hook調用plugin實現。
個人拙見,有錯請各位指出。
如果的我的文章對您有幫助,不符動動您的金手指給個Star,予人玫瑰,手有餘香,不勝感激。 GitHub



pytest-pluggy深挖hook調用邏輯

前面介紹了不少hook的調用邏輯,但是還有個hook_execute沒接上,這裏來完整的分析pm.hook.calculate(a=2, b=3)的執行過程

每當我們調用pm.hook.xxx(**kwargs)的時候,實際上是調用了_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)
  • __call__的代碼可以看到核心邏輯是最後一行的self._hookexec,我們可以發現這是_HookCaller的一個屬性



我們順着self._hookexec往回找到_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
  • 可以發現,這個屬性是構造_HookCaller對象時傳入的的方法,我們再往回找,看看hook_execute是從哪裏傳進來的



我們發現hook_execute是從PluginManager類的register方法中實例化_HookCaller時傳遞的

    if hook is None:
        hook = _HookCaller(name, self._hookexec)



這是PluginManager類的一個方法,找到該方法,發現只是一個封裝,繼續往上找

    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_execute其實是hook.multicall方法,也就是multicall函數的封裝

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



往回找,沒想到回到了_HookCaller類的self.multicall

  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



最後找到了_HookCaller類的_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,)
                              )
  • 反轉hook_impls,plugin執行從list末尾開始,這也是爲什麼後註冊的plugin先執行的原因
  • 檢查參數,如果hook_impls使用的參數沒有在hookspec中預先定義的話,拋出HookCallError



  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
  • hookwrapper
    • 如果定義plugin時hookwrapper參數爲True時,會先執行plugin中yield之前的代碼,等其他plugin執行完才繼續執行yield後面的的部分。
    • gen = hook_impl.function(*args)執行plugin function中yield前的部分,然後停下
    • next(gen)迭代到plugin function中yield後面的部分
    • 將gen得到的generator加到teardown中,用於後續的callback
  • nonwrapper
    • 直接調用plugin function
    • 將執行結果保存到result中



    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()
  • 在一個hook的所有plugin實現都執行完後,所有的執行結果都要用_Result類封裝
  • 如果仍存在前面存入teardowns中的generator,遍歷並執行這些“執行了一半的”hookwrapper
  • 並將nowrapper的結果傳遞給hookwrapper的後半部分
  • 這裏不允許在一個hookwrapper使用兩次yield,會導致在外部拋出異常終止邏輯 _raise_wrapfail(gen, "has second yield")->RuntimeError
    • 這裏詳細講下
    • 當gen.send(outcome)將結果傳遞迴hookwrapper,hookwrapper會接着往下執行,沒有yield的時候會按執行完plugin的邏輯走
    • 當再次遇到yield的時候,會再次跳出,跳回的位置就是這裏,所以再往下執行_raise_wrapfail(gen, "has second yield")會拋出錯誤了。
  • 最後將outcome的結果返回給調用方hook_execute->self._hookexec->pm.hook.xxx(**kwargs)



額外再看看_Result的代碼,先看構造函數,就是把執行結果與執行異常信息封裝到類_Result

  class _Result(object):
      def __init__(self, result, excinfo):
          self._result = result
          self._excinfo = excinfo



還有主要方法get_result()

  def get_result(self):
      """Get the result(s) for this hook call.

      If the hook was marked as a ``firstresult`` only a single value
      will be returned otherwise a list of results.
        """
      __tracebackhide__ = True
      if self._excinfo is None:
          return self._result
      else:
          ex = self._excinfo
          if _py3:
              raise ex[1].with_traceback(ex[2])
          _reraise(*ex)  # noqa
  • 判斷hook call過程是有否異常。
  • 無異常情況下返回hook call的執行結果。
  • 有異常情況下,拋出基礎異常類BaseException的回溯結果




總結

斷斷續續堅持扒了一個多星期pluggy源碼終於扒完了,以前聽說看完優秀源碼會覺得自己寫的東西是垃圾,現感受到了。




GitHubhttps://github.com/potatoImp/pytestCodeParsing

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