pytest插件探索——hook開發


前言

參考官方的這篇文章,我嘗試翻譯其中一些重點部分,並且拓展了相關的pluggy部分的知識。由於pytest是在pluggy基礎上構建的,強烈建議先閱讀一下pluggy的官方文檔,這樣理解起來更加容易一點。

 

正文

conftest.py可以作為最簡單的本地plugin調用一些hook函數,以此來做些強化功能。pytest整個框架通過調用如下定義良好的hooks來實現配置,收集,執行和報告這些過程:

  • 內置plugins:從代碼內部的_pytest目錄加載;
  • 外部插件(第三方插件):通過setuptools entry points機制發現的第三方插件模塊;
  • conftest.py形式的本地插件:測試目錄下的自動模塊發現機制;

原則上,每個hook都是一個 1:N 的python函數調用, 這里的 N 是對一個給定hook的所有注冊調用數。所有的hook函數都使用pytest_xxx的命名規則,以便於查找並且同其他函數區分開來。

資源網站大全 https://55wd.com 我的007辦公資源網站 https://www.wode007.com

decorrator

pluggy里提供了兩個decorator helper類,分別是HookspecMarkerHookimplMarker,通過使用相同的project_name參數初始化得到對應的裝飾器,后續可以用這個裝飾器將函數標記為hookspechookimpl

 

hookspec

hook specification (hookspec)用來validate每個hookimpl,保證hookimpl被正確的定義。

hookspec 通過 add_hookspecs()方法加載,一般在注冊hookimpl之前先加載;

 

hookimpl

hook implementation (hookimpl) 是一個被恰當標記過的回調函數。hookimpls 通過register()方法加載。

注:為了保證hookspecs在項目里可以不斷演化, hookspec里的參數對於hookimpls是可選的,即可以定義少於spec里定義數量的參數。

hookwrapper

hookimpl 里還有一個hookwrapper選項,用來表示這個函數是個hookwrapper函數。hookwrapper函數可以在普通的非wrapper的hookimpls執行的前后執行一些其他代碼, 類似於@contextlib.contextmanager,hookwrapper必須在它的主體包含單一的yield,用來實現生成器函數,例如:

import pytest

@pytest.hookimpl(hookwrapper=True)
def pytest_pyfunc_call(pyfuncitem):
    do_something_before_next_hook_executes()

    outcome = yield
    # outcome.excinfo may be None or a (cls, val, tb) tuple

    res = outcome.get_result()  # will raise if outcome was exception

    post_process_result(res)

    outcome.force_result(new_res)  # to override the return value to the plugin system

  

生成器發送一個 pluggy.callers._Result對象 , 這個對象可以在 yield表達式里指定並且通過 force_result()或者get_result() 方法重寫或者拿到最終結果。

注:hookwrapper不能返回結果 (跟所有的生成器函數一樣);

 

hookimpl的調用順序

默認情況下,hook的調用順序遵循注冊時的順序LIFO(后進先出),hookimpl允許通過tryfirst, trylast*選項調整這一項順序。

舉個例子,對於如下的代碼:

# Plugin 1
@pytest.hookimpl(tryfirst=True)
def pytest_collection_modifyitems(items):
    # will execute as early as possible
    ...


# Plugin 2
@pytest.hookimpl(trylast=True)
def pytest_collection_modifyitems(items):
    # will execute as late as possible
    ...


# Plugin 3
@pytest.hookimpl(hookwrapper=True)
def pytest_collection_modifyitems(items):
    # will execute even before the tryfirst one above!
    outcome = yield
    # will execute after all non-hookwrappers executed

  

執行順序如下:

  1. Plugin3的pytest_collection_modifyitems先調用,直到yield點,因為這是一個hook warpper。
  2. Plugin1的pytest_collection_modifyitems被調用,因為有 tryfirst=True參數。
  3. Plugin2的pytest_collection_modifyitems被調用,因為有 trylast=True參數 (不過即使沒有這個參數也會排在tryfirst標記的plugin后面)。
  4. Plugin3的pytest_collection_modifyitems調用yield后面的代碼. yield接收非Wrapper的result返回. Wrapper函數不應該修改這個result。

當然也可以同時將 tryfirst 和 trylast與 hookwrapper=True 混用,這種情況下它將影響hookwrapper之間的調用順序.

 

hook執行結果處理和firstresult選項

默認情況下,調用一個hook會使底層的hookimpl函數在一個循環里按順序執行,並且將其非空的執行結果添加到一個list里面。例外的是,hookspec里有一個firstresult選項,如果指定這個選項為true,那么得到第一個返回非空的結果的hookimpl執行后就直接返回,后續的hookimpl將不在被執行,參考后面的例子。

注: hookwrapper還是正常的執行

 

hook的調用

每一個pluggy.PluginManager 都有一個hook屬性, 可以通過調用這個屬性的call函數來調用hook,需要注意的是,調用時必須使用關鍵字參數語法來調用。

請看下面這個firstresult和hook調用例子:

from pluggy import PluginManager, HookimplMarker, HookspecMarker

hookspec = HookspecMarker("myproject")
hookimpl = HookimplMarker("myproject")


class MySpec1(object):
    @hookspec
    def myhook(self, arg1, arg2):
        pass


class MySpec2(object):
    # 這里將firstresult設置為True
    @hookspec(firstresult=True)
    def myhook(self, arg1, arg2):
        pass


class Plugin1(object):
    @hookimpl
    def myhook(self, arg1, arg2):
        """Default implementation.
        """
        return 1


class Plugin2(object):
    # hookimpl可以定義少於hookspec里定義數量的參數,這里只定義arg1
    @hookimpl
    def myhook(self, arg1):
        """Default implementation.
        """
        return 2


class Plugin3(object):
    # 同上,甚至可以不定義hookspec里的參數 
    @hookimpl
    def myhook(self):
        """Default implementation.
        """
        return 3


pm1 = PluginManager("myproject")
pm2 = PluginManager("myproject")
pm1.add_hookspecs(MySpec1)
pm2.add_hookspecs(MySpec2)
pm1.register(Plugin1())
pm1.register(Plugin2())
pm1.register(Plugin3())
pm2.register(Plugin1())
pm2.register(Plugin2())
pm2.register(Plugin3())
# hook調用必須使用關鍵字參數的語法
print(pm1.hook.myhook(arg1=None, arg2=None))
print(pm2.hook.myhook(arg1=None, arg2=None))

  

得到結果如下:

[3, 2, 1] 3

可以看到,由於pm2里的hookspec里有firstresult參數,在得到3這個非空結果時就直接返回了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM