Source code for testsuite.fixture_markers

"""
Utilities for attaching typed marks to pytest fixture functions
and querying those marks during a test session.

Use :func:`mark` to store an info object on the original function.
Use :func:`get_infos` to retrieve fixtures tagged with a particular
info type among those visible to a request.

Marks are independent of ``@pytest.fixture``: apply the mark to the
original function, then wrap with ``@pytest.fixture``.
"""

from __future__ import annotations

import inspect
from collections.abc import Callable, Iterator, Sequence
from typing import Any, TypeVar, cast

import pytest

__all__ = [
    'get_infos',
    'mark',
]

I = TypeVar('I')  # noqa: E741

_MARKS_ATTR = '_testsuite_fixture_marks'
_SCOPE_RANK = {
    'function': 0,
    'class': 1,
    'module': 2,
    'package': 3,
    'session': 4,
}


[docs] def mark( func: Callable[..., object], info: object, /, ) -> Callable[..., object]: """ Attach *info* to the original function *func*. Creates the marks dict if it is missing, otherwise updates it. The *info* type is the lookup key for :func:`get_infos`. Each type may be attached at most once. *func* must be the original function, not a ``@pytest.fixture`` wrapper. Apply the mark under ``@pytest.fixture``. See :doc:`fixture_markers` for usage examples. :param func: The original fixture function. :param info: Metadata instance to store. :returns: *func*, unchanged. """ if _is_pytest_fixture_wrapper(func): name = getattr(func, '__qualname__', func.__name__) raise ValueError( f'{name!r} is already wrapped by @pytest.fixture; ' 'apply the mark under @pytest.fixture, not above it', ) marks = getattr(func, _MARKS_ATTR, None) if marks is None: marks = {} setattr(func, _MARKS_ATTR, marks) info_type = type(info) if (existing := marks.get(info_type)) is not None: name = getattr(func, '__qualname__', func.__name__) raise ValueError( f'{name!r} already has a {info_type.__name__} mark ' f'({existing!r}); cannot attach another ({info!r})', ) marks[info_type] = info return func
[docs] def get_infos( request: pytest.FixtureRequest, info_type: type[I], /, ) -> dict[str, I]: """ Return marked fixtures of *info_type* that are visible to a request. Walks fixture definitions known to pytest, keeps those applicable to the requesting test, and skips fixtures whose scope is narrower than ``request.scope``. The winning fixture definition is the one pytest would call. If it has no mark of *info_type*, the mark is inherited from the nearest overridden definition that has one. A mark on the winner replaces the inherited mark. There is no way to drop an inherited mark. See :doc:`fixture_markers` for usage examples. :param request: The pytest fixture request object. :param info_type: The info class whose tagged fixtures you want to look up. :returns: ``dict[str, I]`` mapping each tagged fixture's name to the *info* object that was passed to :func:`mark`. The dict is a fresh copy; mutating it has no effect on stored data. """ collected: dict[str, I] = {} for fixturedef in _iter_visible_fixtures(request): name = fixturedef.argname info = _info_for_name(request, name, info_type) if info is not None: collected[name] = info return collected
def _is_pytest_fixture_wrapper(func: object) -> bool: if getattr(func, '_pytestfixturefunction', None): return True return type(func).__name__ == 'FixtureFunctionDefinition' def _get_fixturedefs( fixture_manager: Any, name: str, request: pytest.FixtureRequest, ) -> Sequence[pytest.FixtureDef[object]] | None: item = request._pyfuncitem params = inspect.signature(fixture_manager.getfixturedefs).parameters key = item if list(params)[1] == 'node' else item.nodeid return fixture_manager.getfixturedefs(name, key) def _iter_visible_fixtures( request: pytest.FixtureRequest, ) -> Iterator[pytest.FixtureDef[object]]: fixture_manager = request.session._fixturemanager invoking_rank = _SCOPE_RANK[request.scope] for name in fixture_manager._arg2fixturedefs: matched = _get_fixturedefs(fixture_manager, name, request) if not matched: continue winning = matched[-1] if _SCOPE_RANK[winning.scope] < invoking_rank: continue yield winning def _info_for_name( request: pytest.FixtureRequest, name: str, info_type: type[I], ) -> I | None: fixture_manager = request.session._fixturemanager matched = _get_fixturedefs(fixture_manager, name, request) if not matched: return None return _inherited_info(matched, info_type) def _inherited_info( matched: Sequence[pytest.FixtureDef[object]], info_type: type[I], ) -> I | None: # matched is ordered from the least specific definition to the winner. for fixturedef in reversed(matched): info = _mark_info(fixturedef, info_type) if info is not None: return info return None def _mark_info( fixturedef: pytest.FixtureDef[object], info_type: type[I], ) -> I | None: marks = getattr(fixturedef.func, _MARKS_ATTR, None) if not marks: return None info = marks.get(info_type) if type(info) is info_type: return cast(I, info) return None