Skip to content

Commit e62df00

Browse files
author
Sylvain MARIE
committed
Following [Sup3rGeo](https://github.com/Sup3rGeo)'s proposal, introduced two helper methods to create simple "parameter fixtures". Fixes #31
1 parent cbbb0f3 commit e62df00

File tree

3 files changed

+133
-4
lines changed

3 files changed

+133
-4
lines changed

pytest_cases/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
pass
77

88
from pytest_cases.main import cases_data, CaseDataGetter, cases_fixture, pytest_fixture_plus, \
9-
unfold_expected_err, get_all_cases, THIS_MODULE, get_pytest_parametrize_args
9+
unfold_expected_err, get_all_cases, THIS_MODULE, get_pytest_parametrize_args, param_fixtures, param_fixture
1010

1111
__all__ = [
1212
# the 3 submodules
1313
'main', 'case_funcs', 'common',
1414
# all symbols imported above
1515
'cases_data', 'CaseData', 'CaseDataGetter', 'cases_fixture', 'pytest_fixture_plus',
16-
'unfold_expected_err', 'get_all_cases', 'get_pytest_parametrize_args',
16+
'unfold_expected_err', 'get_all_cases', 'get_pytest_parametrize_args', 'param_fixtures', 'param_fixture',
1717
'case_name', 'Given', 'ExpectedNormal', 'ExpectedError',
1818
'test_target', 'case_tags', 'THIS_MODULE', 'cases_generator', 'MultipleStepsCaseData'
1919
]

pytest_cases/main.py

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import sys
55
from abc import abstractmethod, ABCMeta
66
from distutils.version import LooseVersion
7-
from inspect import getmembers, isgeneratorfunction
7+
from inspect import getmembers, isgeneratorfunction, getmodule, currentframe
88
from itertools import product
99
from warnings import warn
1010

@@ -146,6 +146,92 @@ def get(self, *args, **kwargs):
146146
"""Marker that can be used instead of a module name to indicate that the module is the current one"""
147147

148148

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+
149235
@function_decorator
150236
def cases_fixture(cases=None, # type: Union[Callable[[Any], Any], Iterable[Callable[[Any], Any]]]
151237
module=None, # type: Union[ModuleType, Iterable[ModuleType]]
@@ -246,7 +332,7 @@ def pytest_fixture_plus(scope="function",
246332
can see it. If False (the default) then an explicit
247333
reference is needed to activate the fixture.
248334
: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
250336
which it is defined, the function name of the fixture will be
251337
shadowed by the function arg that requests the fixture; one way
252338
to resolve this is to name the decorated function
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import pytest
2+
from pytest_cases import param_fixture, param_fixtures
3+
4+
5+
# create a single parameter fixture
6+
my_parameter = param_fixture("my_parameter", [1, 2, 3, 4])
7+
8+
9+
@pytest.fixture
10+
def fixture_uses_param(my_parameter):
11+
return my_parameter
12+
13+
14+
def test_uses_param(my_parameter, fixture_uses_param):
15+
# check that the parameter injected in both is the same
16+
assert my_parameter == fixture_uses_param
17+
18+
19+
# -----
20+
# create a 2-tuple parameter fixture
21+
arg1, arg2 = param_fixtures("arg1, arg2", [(1, 2), (3, 4)])
22+
23+
24+
@pytest.fixture
25+
def fixture_uses_param2(arg2):
26+
return arg2
27+
28+
29+
def test_uses_param2(arg1, arg2, fixture_uses_param2):
30+
# check that the parameter injected in both is the same
31+
assert arg2 == fixture_uses_param2
32+
assert arg1, arg2 in [(1, 2), (3, 4)]
33+
34+
35+
def test_synthesis(module_results_dct):
36+
"""Use pytest-harvest to check that the list of executed tests is correct """
37+
38+
assert list(module_results_dct) == ['test_uses_param[1]',
39+
'test_uses_param[2]',
40+
'test_uses_param[3]',
41+
'test_uses_param[4]',
42+
'test_uses_param2[1-2]',
43+
'test_uses_param2[3-4]']

0 commit comments

Comments
 (0)