3939 pass
4040
4141from 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
4344from 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+
46134def 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):
311399def 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
0 commit comments