pytestプラグイン探索-hook開発


前言
公式のこの文章を参考にして、私はその中のいくつかの重点部分を翻訳して、そして関連するplugy部分の知識を広げました.pytestはplugyに基づいて構築されているので、plugyの公式ドキュメントを先に読むことを強くお勧めします.そうすれば理解しやすくなります.
本文
conftest.pyは、最も簡単なローカルpluginとしていくつかのhook関数を呼び出し、強化機能を行うことができます.
pytestフレームワーク全体は、次のように定義された良好なhooksを呼び出すことによって、構成、収集、実行、およびレポートを実現します.
  • 内蔵plugins:コード内部からの_pytestディレクトリのロード;
  • 外部カード(サードパーティカード):setuptools entry pointsメカニズムで発見されたサードパーティカードモジュール;
  • conftest.py形式のローカルプラグイン:ディレクトリの下の自動モジュール発見メカニズムをテストする;

  • 原則として、各hookは、1つの1:Npython関数呼び出しであり、ここでのNは、1つの与えられたhookに対するすべての登録呼び出し数である.すべてのhook関数はpytest_を使用します.xxxの命名規則は、検索を容易にし、他の関数と区別するために使用されます.
    decorrator
    plugyには、HookspecMarkerとHookimplMarkerの2つのdecorator helperクラスがあり、同じproject_を使用することでnameパラメータを初期化して対応する装飾器を得,その後,この装飾器で関数をhookspecとhookimplとマークすることができる.
    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関数は、@contextlibと同様に、通常のwrapper以外のhookimplsが実行する前後にいくつかの他のコードを実行することができる.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

    実行順序は次のとおりです.
  • Plugin 3のpytest_collection_modifyitemsは、hook warpperであるため、yieldポイントまで呼び出されます.
  • Plugin 1のpytest_collection_modifyitemsは、tryfirst=Trueパラメータがあるため呼び出されます.
  • Plugin 2のpytest_collection_modifyitemsが呼び出され、trylast=Trueパラメータがあるため(ただしこのパラメータがなくてもtryfirstタグのpluginの後ろに並ぶ).
  • Plugin 3のpytest_collection_modifyitemsはyieldの後のコードを呼び出す.yieldはWrapper以外のresultを受信する.Wrapper関数はこのresultを変更するべきではありません.

  • 当然ながら、tryfirsttrylasthookwrapper=Trueを同時に混用してもよいが、この場合hookwrapper間の呼び出し順序に影響を与える.
    hook実行結果処理とfirstresultオプション
    デフォルトでは、hookを呼び出すと、最下位のhookimpl関数がループ内で順番に実行され、空でない実行結果がリストに追加されます.例外として、hookspecにはfirstresultオプションがあります.このオプションをtrueと指定すると、空でない結果を最初に返すhookimplが実行された後、直接戻ります.後続のhookimplは実行されません.後述の例を参照してください.
    注意:hookwrapperは正常に実行されています
    hookの呼び出し
    各plugy.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この非空結果が得られたときにそのまま返されることがわかる.
    自動登録プラグイン
    従来のregister()メソッド登録プラグインに加えて、plugyはload_を提供しています.setuptools_entrypoints()メソッドでは、setuptools entry pointsでプラグインを自動的に登録できます.