|
4 | 4 | import sys
|
5 | 5 | from abc import abstractmethod, ABCMeta
|
6 | 6 | from distutils.version import LooseVersion
|
7 |
| -from inspect import getmembers, isgeneratorfunction |
| 7 | +from inspect import getmembers, isgeneratorfunction, getmodule, currentframe |
8 | 8 | from itertools import product
|
9 | 9 | from warnings import warn
|
10 | 10 |
|
@@ -146,6 +146,92 @@ def get(self, *args, **kwargs):
|
146 | 146 | """Marker that can be used instead of a module name to indicate that the module is the current one"""
|
147 | 147 |
|
148 | 148 |
|
| 149 | +def param_fixture(argname, argvalues, ids=None, scope="function"): |
| 150 | + """ |
| 151 | + Identical to `param_fixtures` but for a single parameter name. |
| 152 | +
|
| 153 | + :param argname: |
| 154 | + :param argvalues: |
| 155 | + :param ids: |
| 156 | + :param scope: the scope of created fixtures |
| 157 | + :return: |
| 158 | + """ |
| 159 | + if "," in argname: |
| 160 | + raise ValueError("`param_fixture` is an alias for `param_fixtures` that can only be used for a single " |
| 161 | + "parameter name. Use `param_fixtures` instead - but note that it creates several fixtures.") |
| 162 | + |
| 163 | + # create the fixture |
| 164 | + def _param_fixture(request): |
| 165 | + return request.param |
| 166 | + _param_fixture.__name__ = argname |
| 167 | + |
| 168 | + return pytest.fixture(scope=scope, params=argvalues, ids=ids)(_param_fixture) |
| 169 | + |
| 170 | + |
| 171 | +def param_fixtures(argnames, argvalues, ids=None): |
| 172 | + """ |
| 173 | + Creates one or several "parameters" fixtures - depending on the number or coma-separated names in `argnames`. |
| 174 | +
|
| 175 | + Note that the (argnames, argvalues, ids) signature is similar to `@pytest.mark.parametrize` for consistency, |
| 176 | + see https://docs.pytest.org/en/latest/reference.html?highlight=pytest.param#pytest-mark-parametrize |
| 177 | +
|
| 178 | + :param argnames: |
| 179 | + :param argvalues: |
| 180 | + :param ids: |
| 181 | + :return: |
| 182 | + """ |
| 183 | + created_fixtures = [] |
| 184 | + argnames_lst = argnames.replace(' ', '').split(',') |
| 185 | + |
| 186 | + # create the root fixture that will contain all parameter values |
| 187 | + root_fixture_name = "param_fixtures_root__%s" % ('_'.join(argnames_lst)) |
| 188 | + |
| 189 | + # Add the fixture dynamically: we have to add it to the corresponding module as explained in |
| 190 | + # https://github.com/pytest-dev/pytest/issues/2424 |
| 191 | + # grab context from the caller frame |
| 192 | + frame = _get_callerframe() |
| 193 | + module = getmodule(frame) |
| 194 | + |
| 195 | + # find a non-used fixture name |
| 196 | + if root_fixture_name in dir(module): |
| 197 | + root_fixture_name += '_1' |
| 198 | + i = 1 |
| 199 | + while root_fixture_name in dir(module): |
| 200 | + i += 1 |
| 201 | + root_fixture_name[-1] += str(i) |
| 202 | + |
| 203 | + @pytest_fixture_plus(name=root_fixture_name) |
| 204 | + @pytest.mark.parametrize(argnames, argvalues, ids=ids) |
| 205 | + @with_signature("(%s)" % argnames) |
| 206 | + def _root_fixture(**kwargs): |
| 207 | + return tuple(kwargs[k] for k in argnames_lst) |
| 208 | + |
| 209 | + setattr(module, root_fixture_name, _root_fixture) |
| 210 | + |
| 211 | + # finally create the sub-fixtures |
| 212 | + for param_idx, argname in enumerate(argnames_lst): |
| 213 | + # create the fixture |
| 214 | + @pytest_fixture_plus(name=argname) |
| 215 | + @with_signature("(%s)" % root_fixture_name) |
| 216 | + def _param_fixture(**kwargs): |
| 217 | + params = kwargs.pop(root_fixture_name) |
| 218 | + return params[param_idx] if len(argnames_lst) > 1 else params |
| 219 | + |
| 220 | + created_fixtures.append(_param_fixture) |
| 221 | + |
| 222 | + return created_fixtures |
| 223 | + |
| 224 | + |
| 225 | +def _get_callerframe(offset=0): |
| 226 | + # inspect.stack is extremely slow, the fastest is sys._getframe or inspect.currentframe(). |
| 227 | + # See https://gist.github.com/JettJones/c236494013f22723c1822126df944b12 |
| 228 | + # frame = sys._getframe(2 + offset) |
| 229 | + frame = currentframe() |
| 230 | + for _ in range(2 + offset): |
| 231 | + frame = frame.f_back |
| 232 | + return frame |
| 233 | + |
| 234 | + |
149 | 235 | @function_decorator
|
150 | 236 | def cases_fixture(cases=None, # type: Union[Callable[[Any], Any], Iterable[Callable[[Any], Any]]]
|
151 | 237 | module=None, # type: Union[ModuleType, Iterable[ModuleType]]
|
@@ -246,7 +332,7 @@ def pytest_fixture_plus(scope="function",
|
246 | 332 | can see it. If False (the default) then an explicit
|
247 | 333 | reference is needed to activate the fixture.
|
248 | 334 | :param name: the name of the fixture. This defaults to the name of the
|
249 |
| - decorated function. If a fixture is used in the same module in |
| 335 | + decorated function. Note: If a fixture is used in the same module in |
250 | 336 | which it is defined, the function name of the fixture will be
|
251 | 337 | shadowed by the function arg that requests the fixture; one way
|
252 | 338 | to resolve this is to name the decorated function
|
|
0 commit comments