userver: en/testsuite/fixture_markers.py Source File
Loading...
Searching...
No Matches
fixture_markers.py
1"""
2Utilities for attaching typed marks to pytest fixture functions
3and querying those marks during a test session.
4
5Use :func:`mark` to store an info object on the original function.
6Use :func:`get_infos` to retrieve fixtures tagged with a particular
7info type among those visible to a request.
8
9Marks are independent of ``@pytest.fixture``: apply the mark to the
10original function, then wrap with ``@pytest.fixture``.
11"""
12
13from __future__ import annotations
14
15import inspect
16from collections.abc import Callable, Iterator, Sequence
17from typing import Any, TypeVar, cast
18
19import pytest
20
21__all__ = [
22 'get_infos',
23 'mark',
24]
25
26I = TypeVar('I') # noqa: E741
27
28_MARKS_ATTR = '_testsuite_fixture_marks'
29_SCOPE_RANK = {
30 'function': 0,
31 'class': 1,
32 'module': 2,
33 'package': 3,
34 'session': 4,
35}
36
37
38def mark(
39 func: Callable[..., object],
40 info: object,
41 /,
42) -> Callable[..., object]:
43 """
44 Attach *info* to the original function *func*.
45
46 Creates the marks dict if it is missing, otherwise updates it.
47 The *info* type is the lookup key for :func:`get_infos`. Each type
48 may be attached at most once.
49
50 *func* must be the original function, not a ``@pytest.fixture``
51 wrapper. Apply the mark under ``@pytest.fixture``.
52
53 See :doc:`fixture_markers` for usage examples.
54
55 :param func: The original fixture function.
56 :param info: Metadata instance to store.
57 :returns: *func*, unchanged.
58 """
59 if _is_pytest_fixture_wrapper(func):
60 name = getattr(func, '__qualname__', func.__name__)
61 raise ValueError(
62 f'{name!r} is already wrapped by @pytest.fixture; '
63 'apply the mark under @pytest.fixture, not above it',
64 )
65
66 marks = getattr(func, _MARKS_ATTR, None)
67 if marks is None:
68 marks = {}
69 setattr(func, _MARKS_ATTR, marks)
70 info_type = type(info)
71 if (existing := marks.get(info_type)) is not None:
72 name = getattr(func, '__qualname__', func.__name__)
73 raise ValueError(
74 f'{name!r} already has a {info_type.__name__} mark '
75 f'({existing!r}); cannot attach another ({info!r})',
76 )
77 marks[info_type] = info
78 return func
79
80
82 request: pytest.FixtureRequest,
83 info_type: type[I],
84 /,
85) -> dict[str, I]:
86 """
87 Return marked fixtures of *info_type* that are visible to a request.
88
89 Walks fixture definitions known to pytest, keeps those applicable to
90 the requesting test, and skips fixtures whose scope is narrower than
91 ``request.scope``.
92
93 Only the winning fixture definition is inspected. An override must
94 carry its own mark; a parent mark is not inherited.
95
96 See :doc:`fixture_markers` for usage examples.
97
98 :param request: The pytest fixture request object.
99 :param info_type: The info class whose tagged fixtures you want
100 to look up.
101 :returns: ``dict[str, I]`` mapping each tagged fixture's name to the
102 *info* object that was passed to :func:`mark`.
103 The dict is a fresh copy; mutating it has no effect on stored
104 data.
105 """
106 collected: dict[str, I] = {}
107 for fixturedef in _iter_visible_fixtures(request):
108 marks = getattr(fixturedef.func, _MARKS_ATTR, None)
109 if not marks:
110 continue
111 info = marks.get(info_type)
112 if type(info) is info_type:
113 collected[fixturedef.argname] = cast(I, info)
114 return collected
115
116
117def _is_pytest_fixture_wrapper(func: object) -> bool:
118 if getattr(func, '_pytestfixturefunction', None):
119 return True
120 return type(func).__name__ == 'FixtureFunctionDefinition'
121
122
123def _get_fixturedefs(
124 fixture_manager: Any,
125 name: str,
126 request: pytest.FixtureRequest,
127) -> Sequence[Any] | None:
128 item = request._pyfuncitem
129 params = inspect.signature(fixture_manager.getfixturedefs).parameters
130 key = item if list(params)[1] == 'node' else item.nodeid
131 return fixture_manager.getfixturedefs(name, key)
132
133
134def _iter_visible_fixtures(
135 request: pytest.FixtureRequest,
136) -> Iterator[pytest.FixtureDef[Any]]:
137 fixture_manager = request.session._fixturemanager
138 invoking_rank = _SCOPE_RANK[request.scope]
139
140 for name in fixture_manager._arg2fixturedefs:
141 matched = _get_fixturedefs(fixture_manager, name, request)
142 if not matched:
143 continue
144 winning = matched[-1]
145 if _SCOPE_RANK[winning.scope] < invoking_rank:
146 continue
147 yield winning