Написание крючковых функций

проверка и выполнение функций крючка

pytest вызывает хук-функции из зарегистрированных плагинов для любой заданной спецификации хука. Рассмотрим типичную хук-функцию для хука pytest_collection_modifyitems(session, config, items), который pytest вызывает после завершения сбора всех элементов теста.

Когда мы реализуем функцию pytest_collection_modifyitems в нашем плагине, pytest при регистрации проверит, что вы используете имена аргументов, соответствующие спецификации, и откажет, если это не так.

Давайте рассмотрим возможную реализацию:

def pytest_collection_modifyitems(config, items):
    # called after collection is completed
    # you can modify the ``items`` list
    ...

Здесь pytest передаст config (объект конфигурации pytest) и items (список собранных элементов теста), но не передаст аргумент session, поскольку мы не указали его в сигнатуре функции. Такая динамическая «обрезка» аргументов позволяет pytest быть «совместимым с будущим»: мы можем вводить новые именованные параметры хука, не нарушая сигнатуры существующих реализаций хука. Это одна из причин общей долгоживущей совместимости плагинов pytest.

Обратите внимание, что функции hook, отличные от pytest_runtest_*, не могут вызывать исключения. Это приведет к нарушению работы pytest.

firstresult: остановка на первом результате, не являющемся None

Большинство вызовов хуков pytest приводят к списку результатов, который содержит все не-None результаты вызванных хук-функций.

Некоторые спецификации хуков используют опцию firstresult=True, чтобы вызов хука выполнялся только до тех пор, пока первая из N зарегистрированных функций не вернет результат не None, который затем принимается за результат общего вызова хука. Остальные функции хука в этом случае не будут вызваны.

hookwrapper: выполнение вокруг других крючков

Подключаемые модули pytest могут реализовывать обертки хуков, которые оборачивают выполнение других реализаций хуков. Обертка хука - это функция-генератор, которая выполняется ровно один раз. Когда pytest вызывает хуки, он сначала выполняет хук-обертку и передает ей те же аргументы, что и обычным хукам.

В точке выхода обертки хука pytest выполнит следующие реализации хука и вернет их результат в точку выхода в виде экземпляра Result, который содержит результат или информацию об исключении. Таким образом, сама точка выхода обычно не вызывает исключений (если нет ошибок).

Вот пример определения обертки крючка:

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 documentation about hookwrappers.

Заказ функции крючка / пример вызова

Для любой данной спецификации хука может существовать более одной реализации, поэтому мы обычно рассматриваем выполнение hook как вызов функции 1:N, где N - количество зарегистрированных функций. Существуют способы повлиять на то, идет ли реализация хука раньше или позже других, т.е. на позицию в списке функций размером N:

# 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’s pytest_collection_modifyitems вызывается до точки выхода, поскольку является оберткой хука.

  2. Plugin1’s pytest_collection_modifyitems вызывается, потому что он помечен tryfirst=True.

  3. Вызывается pytest_collection_modifyitems плагина Plugin2, потому что он помечен trylast=True (но даже без этой пометки он шел бы после Plugin1).

  4. Plugin3’s pytest_collection_modifyitems затем выполняет код после точки выхода. Выход получает экземпляр Result, который инкапсулирует результат вызова необёрток. Обертки не должны изменять результат.

Можно использовать tryfirst и trylast также в сочетании с hookwrapper=True, в этом случае они будут влиять на упорядочивание крючкотворов между собой.

Объявление новых крючков

Примечание

Это краткий обзор того, как добавлять новые крючки и как они работают в целом, но более полный обзор можно найти в the pluggy documentation.

Плагины и файлы conftest.py могут объявлять новые хуки, которые затем могут быть реализованы другими плагинами для изменения поведения или взаимодействия с новым плагином:

pytest_addhooks(pluginmanager)[исходный код]

Вызывается во время регистрации плагина, чтобы позволить добавлять новые хуки через вызов pluginmanager.add_hookspecs(module_or_class, prefix).

Parameters:

pluginmanager (pytest.PytestPluginManager) – Менеджер плагинов pytest.

Примечание

Этот крючок несовместим с hookwrapper=True.

Крючки обычно объявляются как «ничего не делающие» функции, которые содержат только документацию, описывающую, когда будет вызван крючок и какие значения возврата ожидаются. Имена функций должны начинаться с pytest_, иначе pytest их не распознает.

Вот пример. Предположим, что этот код находится в модуле sample_hook.py.

def pytest_my_hook(config):
    """
    Receives the pytest config and does things with it
    """

Для регистрации хуков в pytest они должны быть структурированы в собственный модуль или класс. Затем этот класс или модуль может быть передан в pluginmanager с помощью функции pytest_addhooks (которая сама является хуком, раскрываемым pytest).

def pytest_addhooks(pluginmanager):
    """This example assumes the hooks are grouped in the 'sample_hook' module."""
    from my_app.tests import sample_hook

    pluginmanager.add_hookspecs(sample_hook)

В качестве примера из реального мира смотрите newhooks.py из xdist.

Хуки могут вызываться как из фикстур, так и из других хуков. В обоих случаях хуки вызываются через объект hook, доступный в объекте config. Большинство хуков получают объект config напрямую, в то время как фикстуры могут использовать фикстуру pytestconfig, которая предоставляет тот же объект.

@pytest.fixture()
def my_fixture(pytestconfig):
    # call the hook called "pytest_my_hook"
    # 'result' will be a list of return values from all registered functions.
    result = pytestconfig.hook.pytest_my_hook(config=pytestconfig)

Примечание

Крючки получают параметры, используя только аргументы в виде ключевых слов.

Теперь ваш хук готов к использованию. Чтобы зарегистрировать функцию на хуке, другие плагины или пользователи теперь должны просто определить функцию pytest_my_hook с правильной сигнатурой в своем conftest.py.

Пример:

def pytest_my_hook(config):
    """
    Print all active hooks to the screen.
    """
    print(config.hook)

Использование крючков в pytest_addoption

Иногда возникает необходимость изменить способ определения опций командной строки одним плагином на основе крючков в другом плагине. Например, плагин может открывать опцию командной строки, для которой другой плагин должен определить значение по умолчанию. С помощью pluginmanager можно установить и использовать крючки для достижения этой цели. Плагин определяет и добавляет крючки и использует pytest_addoption следующим образом:

# contents of hooks.py

# Use firstresult=True because we only want one plugin to define this
# default value
@hookspec(firstresult=True)
def pytest_config_file_default_value():
    """Return the default value for the config file command line option."""


# contents of myplugin.py


def pytest_addhooks(pluginmanager):
    """This example assumes the hooks are grouped in the 'hooks' module."""
    from . import hooks

    pluginmanager.add_hookspecs(hooks)


def pytest_addoption(parser, pluginmanager):
    default_value = pluginmanager.hook.pytest_config_file_default_value()
    parser.addoption(
        "--config-file",
        help="Config file to use, defaults to %(default)s",
        default=default_value,
    )

В файле conftest.py, использующем myplugin, крючок будет определен следующим образом:

def pytest_config_file_default_value():
    return "config.yaml"

По желанию можно использовать хуки из сторонних плагинов

Использование новых хуков из плагинов, как объяснялось выше, может быть немного сложным из-за стандартного validation mechanism: если вы зависите от плагина, который не установлен, валидация не пройдет, а сообщение об ошибке не будет иметь большого смысла для ваших пользователей.

Один из подходов - отложить реализацию хука в новый плагин вместо того, чтобы, например, объявлять функции хука непосредственно в модуле вашего плагина:

# contents of myplugin.py


class DeferPlugin:
    """Simple plugin to defer pytest-xdist hook functions."""

    def pytest_testnodedown(self, node, error):
        """standard xdist hook function."""


def pytest_configure(config):
    if config.pluginmanager.hasplugin("xdist"):
        config.pluginmanager.register(DeferPlugin())

Это дает возможность условно устанавливать хуки в зависимости от того, какие плагины установлены.

Хранение данных об элементах через функции hook

Плагинам часто требуется хранить данные о Itemв одной реализации хука, а обращаться к ним в другой. Одно из распространенных решений - просто присвоить некоторый приватный атрибут непосредственно элементу, но такие программы проверки типов, как mypy, не одобряют этого, к тому же это может вызвать конфликты с другими плагинами. Поэтому pytest предлагает лучший способ сделать это, item.stash.

Чтобы использовать «тайник» в своих плагинах, сначала создайте «ключи тайника» где-нибудь на верхнем уровне вашего плагина:

been_there_key = pytest.StashKey[bool]()
done_that_key = pytest.StashKey[str]()

затем использовать ключи для хранения данных в определенный момент:

def pytest_runtest_setup(item: pytest.Item) -> None:
    item.stash[been_there_key] = True
    item.stash[done_that_key] = "no"

и извлекать их в другой момент:

def pytest_runtest_teardown(item: pytest.Item) -> None:
    if not item.stash[been_there_key]:
        print("Oh?")
    item.stash[done_that_key] = "yes!"

Тайники доступны на всех типах узлов (таких как Class, Session), а также на Config, если это необходимо.

Вернуться на верх