Skip to content

Commit f78c051

Browse files
author
Sylvain MARIE
committed
You can now unpack a fixture iterable into several individual fixtures using unpack_fixture or using @pytest_fixture_plus(unpack_into=). Fixed #50
Added associated tests.
1 parent bc29b7b commit f78c051

File tree

5 files changed

+164
-6
lines changed

5 files changed

+164
-6
lines changed

pytest_cases/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from pytest_cases.case_funcs import case_name, test_target, case_tags, cases_generator
22

33
from pytest_cases.main_fixtures import cases_fixture, pytest_fixture_plus, param_fixtures, param_fixture, \
4-
fixture_union, NOT_USED, pytest_parametrize_plus, fixture_ref
4+
fixture_union, NOT_USED, pytest_parametrize_plus, fixture_ref, unpack_fixture
55

66
from pytest_cases.main_params import cases_data, CaseDataGetter, unfold_expected_err, get_all_cases, THIS_MODULE, \
77
get_pytest_parametrize_args
@@ -14,7 +14,7 @@
1414
'case_name', 'test_target', 'case_tags', 'cases_generator',
1515
# --main_fixtures
1616
'cases_fixture', 'pytest_fixture_plus', 'param_fixtures', 'param_fixture', # 'pytest_parametrize_plus',
17-
'fixture_union', 'NOT_USED', 'pytest_parametrize_plus', 'fixture_ref',
17+
'fixture_union', 'NOT_USED', 'pytest_parametrize_plus', 'fixture_ref', 'unpack_fixture',
1818
# --main params
1919
'cases_data', 'CaseDataGetter', 'THIS_MODULE', 'unfold_expected_err', 'get_all_cases',
2020
'get_pytest_parametrize_args',

pytest_cases/common.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,22 @@ def get_fixture_name(fixture_fun):
4646
return str(fixture_fun)
4747

4848

49+
def get_fixture_scope(fixture_fun):
50+
"""
51+
Internal utility to retrieve the fixture scope corresponding to the given fixture function .
52+
Indeed there is currently no pytest API to do this.
53+
54+
:param fixture_fun:
55+
:return:
56+
"""
57+
# try:
58+
# # pytest 3
59+
return fixture_fun._pytestfixturefunction.scope
60+
# except AttributeError:
61+
# # pytest 2
62+
# return fixture_fun.func_scope
63+
64+
4965
def get_param_argnames_as_list(argnames):
5066
"""
5167
pytest parametrize accepts both coma-separated names and list/tuples.

pytest_cases/main_fixtures.py

Lines changed: 109 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,98 @@
3939
pass
4040

4141
from pytest_cases.common import yield_fixture, get_pytest_parametrize_marks, get_test_ids_from_param_values, \
42-
make_marked_parameter_value, extract_parameterset_info, get_fixture_name, get_param_argnames_as_list
42+
make_marked_parameter_value, extract_parameterset_info, get_fixture_name, get_param_argnames_as_list, \
43+
get_fixture_scope
4344
from pytest_cases.main_params import cases_data
4445

4546

47+
def unpack_fixture(argnames, fixture):
48+
"""
49+
Creates several fixtures with names `argnames` from the source `fixture`. Created fixtures will correspond to
50+
elements unpacked from `fixture` in order. For example if `fixture` is a tuple of length 2, `argnames="a,b"` will
51+
create two fixtures containing the first and second element respectively.
52+
53+
The created fixtures are automatically registered into the callers' module, but you may wish to assign them to
54+
variables for convenience. In that case make sure that you use the same names,
55+
e.g. `a, b = unpack_fixture('a,b', 'c')`.
56+
57+
```python
58+
import pytest
59+
from pytest_cases import unpack_fixture, pytest_fixture_plus
60+
61+
@pytest_fixture_plus
62+
@pytest.mark.parametrize("o", ['hello', 'world'])
63+
def c(o):
64+
return o, o[0]
65+
66+
a, b = unpack_fixture("a,b", c)
67+
68+
def test_function(a, b):
69+
assert a[0] == b
70+
```
71+
72+
:param argnames: same as `@pytest.mark.parametrize` `argnames`.
73+
:param fixture: a fixture name string or a fixture symbol. If a fixture symbol is provided, the created fixtures
74+
will have the same scope. If a name is provided, they will have scope='function'. Note that in practice the
75+
performance loss resulting from using `function` rather than a higher scope is negligible since the created
76+
fixtures' body is a one-liner.
77+
:return: the created fixtures.
78+
"""
79+
# get caller module to create the symbols
80+
caller_module = get_caller_module()
81+
return _unpack_fixture(caller_module, argnames, fixture)
82+
83+
84+
def _unpack_fixture(caller_module, argnames, fixture):
85+
"""
86+
87+
:param caller_module:
88+
:param argnames:
89+
:param fixture:
90+
:return:
91+
"""
92+
# unpack fixture names to create if needed
93+
argnames_lst = get_param_argnames_as_list(argnames)
94+
95+
# possibly get the source fixture name if the fixture symbol was provided
96+
if not isinstance(fixture, str):
97+
source_f_name = get_fixture_name(fixture)
98+
scope = get_fixture_scope(fixture)
99+
else:
100+
source_f_name = fixture
101+
# we dont have a clue about the real scope, so lets use function scope
102+
scope = 'function'
103+
104+
# finally create the sub-fixtures
105+
created_fixtures = []
106+
for value_idx, argname in enumerate(argnames_lst):
107+
# create the fixture
108+
# To fix late binding issue with `value_idx` we add an extra layer of scope: a factory function
109+
# See https://stackoverflow.com/questions/3431676/creating-functions-in-a-loop
110+
def _create_fixture(value_idx):
111+
# no need to autouse=True: this fixture does not bring any added value in terms of setup.
112+
@pytest_fixture_plus(name=argname, scope=scope, autouse=False)
113+
@with_signature("(%s)" % source_f_name)
114+
def _param_fixture(**kwargs):
115+
source_fixture_value = kwargs.pop(source_f_name)
116+
# unpack
117+
return source_fixture_value[value_idx]
118+
119+
return _param_fixture
120+
121+
# create it
122+
fix = _create_fixture(value_idx)
123+
124+
# add to module
125+
check_name_available(caller_module, argname, if_name_exists=WARN, caller=unpack_fixture)
126+
setattr(caller_module, argname, fix)
127+
128+
# collect to return the whole list eventually
129+
created_fixtures.append(fix)
130+
131+
return created_fixtures
132+
133+
46134
def param_fixture(argname, argvalues, autouse=False, ids=None, scope="function", **kwargs):
47135
"""
48136
Identical to `param_fixtures` but for a single parameter name, so that you can assign its output to a single
@@ -311,14 +399,19 @@ def foo_fixture(request):
311399
def pytest_fixture_plus(scope="function",
312400
autouse=False,
313401
name=None,
402+
unpack_into=None,
314403
fixture_func=DECORATED,
315404
**kwargs):
316405
""" decorator to mark a fixture factory function.
317406
318-
Identical to `@pytest.fixture` decorator, except that it supports multi-parametrization with
319-
`@pytest.mark.parametrize` as requested in https://github.com/pytest-dev/pytest/issues/3960.
407+
Identical to `@pytest.fixture` decorator, except that
320408
321-
As a consequence it does not support the `params` and `ids` arguments anymore.
409+
- it supports multi-parametrization with `@pytest.mark.parametrize` as requested in
410+
https://github.com/pytest-dev/pytest/issues/3960. As a consequence it does not support the `params` and `ids`
411+
arguments anymore.
412+
413+
- it supports a new argument `unpack_into` where you can provide names for fixtures where to unpack this fixture
414+
into.
322415
323416
:param scope: the scope for which this fixture is shared, one of
324417
"function" (default), "class", "module" or "session".
@@ -332,6 +425,8 @@ def pytest_fixture_plus(scope="function",
332425
to resolve this is to name the decorated function
333426
``fixture_<fixturename>`` and then use
334427
``@pytest.fixture(name='<fixturename>')``.
428+
:param unpack_into: an optional iterable of names, or string containing coma-separated names, for additional
429+
fixtures to create to represent parts of this fixture. See `unpack_fixture` for details.
335430
:param kwargs: other keyword arguments for `@pytest.fixture`
336431
"""
337432
# Compatibility for the 'name' argument
@@ -451,6 +546,16 @@ def _get_arguments(*args, **kwargs):
451546

452547
return args, kwargs
453548

549+
# if unpacking is requested, do it here
550+
if unpack_into is not None:
551+
# get the future fixture name if needed
552+
if name is None:
553+
name = fixture_func.__name__
554+
555+
# get caller module to create the symbols
556+
caller_module = get_caller_module(frame_offset=2)
557+
_unpack_fixture(caller_module, unpack_into, name)
558+
454559
# --Finally create the fixture function, a wrapper of user-provided fixture with the new signature
455560
if not isgeneratorfunction(fixture_func):
456561
# normal function with return statement
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import pytest
2+
from pytest_cases import unpack_fixture, pytest_fixture_plus
3+
4+
5+
@pytest_fixture_plus
6+
@pytest.mark.parametrize("o", ['hello', 'world'])
7+
def c(o):
8+
return o, o[0]
9+
10+
11+
a, b = unpack_fixture("a,b", c)
12+
13+
14+
def test_function(a, b):
15+
assert a[0] == b
16+
17+
18+
def test_synthesis(module_results_dct):
19+
assert list(module_results_dct) == ['test_function[hello]',
20+
'test_function[world]']
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import pytest
2+
from pytest_cases import pytest_fixture_plus
3+
4+
5+
@pytest_fixture_plus(unpack_into="a,b")
6+
@pytest.mark.parametrize("o", ['hello', 'world'])
7+
def c(o):
8+
return o, o[0]
9+
10+
11+
def test_function(a, b):
12+
assert a[0] == b
13+
14+
15+
def test_synthesis(module_results_dct):
16+
assert list(module_results_dct) == ['test_function[hello]',
17+
'test_function[world]']

0 commit comments

Comments
 (0)