Skip to content

Commit afd8792

Browse files
author
Sylvain MARIE
committed
fixture_ref can now be used inside tuples, leading to cross-products. Fixes #47
1 parent 81bd5b3 commit afd8792

File tree

3 files changed

+192
-16
lines changed

3 files changed

+192
-16
lines changed

pytest_cases/common.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@
1717
yield_fixture = pytest.fixture
1818

1919

20+
def remove_duplicates(lst):
21+
dset = set()
22+
# relies on the fact that dset.add() always returns None.
23+
return [item for item in lst
24+
if item not in dset and not dset.add(item)]
25+
26+
2027
def get_fixture_name(fixture_fun):
2128
"""
2229
Internal utility to retrieve the fixture name corresponding to the given fixture function .

pytest_cases/main_fixtures.py

Lines changed: 140 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040

4141
from pytest_cases.common import yield_fixture, get_pytest_parametrize_marks, get_test_ids_from_param_values, \
4242
make_marked_parameter_value, extract_parameterset_info, get_fixture_name, get_param_argnames_as_list, \
43-
get_fixture_scope
43+
get_fixture_scope, remove_duplicates
4444
from pytest_cases.main_params import cases_data
4545

4646

@@ -886,6 +886,76 @@ def _new_fixture(request, **all_fixtures):
886886
return fix
887887

888888

889+
def _fixture_product(caller_module, name, fixtures_or_values, fixture_positions,
890+
scope="function", ids=fixture_alternative_to_str,
891+
unpack_into=None, autouse=False, **kwargs):
892+
"""
893+
Internal implementation for fixture products created by pytest parametrize plus.
894+
895+
:param caller_module:
896+
:param name:
897+
:param fixtures_or_values:
898+
:param fixture_positions:
899+
:param idstyle:
900+
:param scope:
901+
:param ids:
902+
:param unpack_into:
903+
:param autouse:
904+
:param kwargs:
905+
:return:
906+
"""
907+
# test the `fixtures` argument to avoid common mistakes
908+
if not isinstance(fixtures_or_values, (tuple, set, list)):
909+
raise TypeError("fixture_product: the `fixtures_or_values` argument should be a tuple, set or list")
910+
911+
_tuple_size = len(fixtures_or_values)
912+
913+
# first get all required fixture names
914+
f_names = [None] * _tuple_size
915+
for f_pos in fixture_positions:
916+
# possibly get the fixture name if the fixture symbol was provided
917+
f = fixtures_or_values[f_pos]
918+
# and remember the position in the tuple
919+
f_names[f_pos] = get_fixture_name(f) if not isinstance(f, str) else f
920+
921+
# remove duplicates by making it an ordered set
922+
all_names = remove_duplicates((n for n in f_names if n is not None))
923+
if len(all_names) < 1:
924+
raise ValueError("Empty fixture products are not permitted")
925+
926+
def _tuple_generator(all_fixtures):
927+
for i in range(_tuple_size):
928+
fix_at_pos_i = f_names[i]
929+
if fix_at_pos_i is None:
930+
# fixed value
931+
yield fixtures_or_values[i]
932+
else:
933+
# fixture value
934+
yield all_fixtures[fix_at_pos_i]
935+
936+
# then generate the body of our product fixture. It will require all of its dependent fixtures
937+
@with_signature("(%s)" % ', '.join(all_names))
938+
def _new_fixture(**all_fixtures):
939+
return tuple(_tuple_generator(all_fixtures))
940+
941+
_new_fixture.__name__ = name
942+
943+
# finally create the fixture per se.
944+
# WARNING we do not use pytest.fixture but pytest_fixture_plus so that NOT_USED is discarded
945+
f_decorator = pytest_fixture_plus(scope=scope, autouse=autouse, ids=ids, **kwargs)
946+
fix = f_decorator(_new_fixture)
947+
948+
# Dynamically add fixture to caller's module as explained in https://github.com/pytest-dev/pytest/issues/2424
949+
check_name_available(caller_module, name, if_name_exists=WARN, caller=param_fixture)
950+
setattr(caller_module, name, fix)
951+
952+
# if unpacking is requested, do it here
953+
if unpack_into is not None:
954+
_unpack_fixture(caller_module, argnames=unpack_into, fixture=name)
955+
956+
return fix
957+
958+
889959
class fixture_ref:
890960
"""
891961
A reference to a fixture, to be used in `pytest_parametrize_plus`.
@@ -920,21 +990,46 @@ def pytest_parametrize_plus(argnames, argvalues, indirect=False, ids=None, scope
920990
except TypeError:
921991
raise InvalidParamsList(argvalues)
922992

993+
# get the param names
994+
all_param_names = get_param_argnames_as_list(argnames)
995+
nb_params = len(all_param_names)
996+
923997
# find if there are fixture references in the values provided
924998
fixture_indices = []
925-
for i, v in enumerate(argvalues):
926-
if isinstance(v, fixture_ref):
927-
fixture_indices.append(i)
999+
if nb_params == 1:
1000+
for i, v in enumerate(argvalues):
1001+
if isinstance(v, fixture_ref):
1002+
fixture_indices.append((i, None))
1003+
elif nb_params > 1:
1004+
for i, v in enumerate(argvalues):
1005+
try:
1006+
j = 0
1007+
fix_pos = []
1008+
for j, _pval in enumerate(v):
1009+
if isinstance(_pval, fixture_ref):
1010+
fix_pos.append(j)
1011+
if len(fix_pos) > 0:
1012+
fixture_indices.append((i, fix_pos))
1013+
if j+1 != nb_params:
1014+
raise ValueError("Invalid parameter values containing %s items while the number of parameters is %s: "
1015+
"%s." % (j+1, nb_params, v))
1016+
except TypeError:
1017+
# a fixture ref is
1018+
if isinstance(v, fixture_ref):
1019+
fixture_indices.append((i, None))
1020+
else:
1021+
raise ValueError(
1022+
"Invalid parameter values containing %s items while the number of parameters is %s: "
1023+
"%s." % (1, nb_params, v))
9281024

9291025
if len(fixture_indices) == 0:
9301026
# no fixture reference: do as usual
9311027
return pytest.mark.parametrize(argnames, argvalues, indirect=indirect, ids=ids, scope=scope, **kwargs)
9321028
else:
9331029
# there are fixture references: we have to create a specific decorator
9341030
caller_module = get_caller_module()
935-
all_param_names = get_param_argnames_as_list(argnames)
9361031

937-
def create_param_fixture(from_i, to_i, p_fix_name):
1032+
def _create_param_fixture(from_i, to_i, p_fix_name):
9381033
""" Routine that will be used to create a parameter fixture for argvalues between prev_i and i"""
9391034
selected_argvalues = argvalues[from_i:to_i]
9401035
try:
@@ -944,14 +1039,32 @@ def create_param_fixture(from_i, to_i, p_fix_name):
9441039
# a callable to create the ids
9451040
selected_ids = ids
9461041

1042+
# default behaviour is not the same betwee pytest params and pytest fixtures
1043+
if selected_ids is None:
1044+
# selected_ids = ['-'.join([str(_v) for _v in v]) for v in selected_argvalues]
1045+
selected_ids = get_test_ids_from_param_values(all_param_names, selected_argvalues)
1046+
9471047
if to_i == from_i + 1:
9481048
p_fix_name = "%s_is_%s" % (p_fix_name, from_i)
9491049
else:
9501050
p_fix_name = "%s_is_%sto%s" % (p_fix_name, from_i, to_i - 1)
951-
p_fix_name = check_name_available(caller_module, p_fix_name, if_name_exists=CHANGE, caller=pytest_parametrize_plus)
952-
param_fix = _param_fixture(caller_module, p_fix_name, selected_argvalues, selected_ids)
1051+
p_fix_name = check_name_available(caller_module, p_fix_name, if_name_exists=CHANGE,
1052+
caller=pytest_parametrize_plus)
1053+
param_fix = _param_fixture(caller_module, argname=p_fix_name, argvalues=selected_argvalues,
1054+
ids=selected_ids)
9531055
return param_fix
9541056

1057+
def _create_fixture_product(argvalue_i, fixture_ref_positions, base_name):
1058+
# do not use base name - we dont care if there is another in the same module, it will still be more readable
1059+
p_fix_name = "fixtureproduct__%s" % (argvalue_i, )
1060+
p_fix_name = check_name_available(caller_module, p_fix_name, if_name_exists=CHANGE,
1061+
caller=pytest_parametrize_plus)
1062+
# unpack the fixture references
1063+
_vtuple = argvalues[argvalue_i]
1064+
fixtures_or_values = tuple(v.fixture if i in fixture_ref_positions else v for i, v in enumerate(_vtuple))
1065+
product_fix = _fixture_product(caller_module, p_fix_name, fixtures_or_values, fixture_ref_positions)
1066+
return product_fix
1067+
9551068
# then create the decorator
9561069
def parametrize_plus_decorate(test_func):
9571070
"""
@@ -981,21 +1094,32 @@ def parametrize_plus_decorate(test_func):
9811094
fixtures_to_union = []
9821095
fixtures_to_union_names_for_ids = []
9831096
prev_i = -1
984-
for i in fixture_indices:
1097+
for i, j_list in fixture_indices:
9851098
if i > prev_i + 1:
986-
param_fix = create_param_fixture(prev_i + 1, i, base_name)
1099+
# there was a non-empty group of 'normal' parameters before this fixture_ref.
1100+
# create a new fixture parametrized with all of that consecutive group.
1101+
param_fix = _create_param_fixture(prev_i + 1, i, base_name)
9871102
fixtures_to_union.append(param_fix)
9881103
fixtures_to_union_names_for_ids.append(get_fixture_name(param_fix))
9891104

990-
fixtures_to_union.append(argvalues[i].fixture)
991-
id_for_fixture = apply_id_style(get_fixture_name(argvalues[i].fixture), base_name, IdStyle.explicit)
992-
fixtures_to_union_names_for_ids.append(id_for_fixture)
1105+
if j_list is None:
1106+
# add the fixture referenced with `fixture_ref`
1107+
referenced_fixture = argvalues[i].fixture
1108+
fixtures_to_union.append(referenced_fixture)
1109+
id_for_fixture = apply_id_style(get_fixture_name(referenced_fixture), base_name, IdStyle.explicit)
1110+
fixtures_to_union_names_for_ids.append(id_for_fixture)
1111+
else:
1112+
# create a fixture refering to all the fixtures required in the tuple
1113+
prod_fix = _create_fixture_product(i, j_list, base_name)
1114+
fixtures_to_union.append(prod_fix)
1115+
id_for_fixture = apply_id_style(get_fixture_name(prod_fix), base_name, IdStyle.explicit)
1116+
fixtures_to_union_names_for_ids.append(id_for_fixture)
9931117
prev_i = i
9941118

995-
# last bit if any
1119+
# handle last consecutive group of normal parameters, if any
9961120
i = len(argvalues)
9971121
if i > prev_i + 1:
998-
param_fix = create_param_fixture(prev_i + 1, i, base_name)
1122+
param_fix = _create_param_fixture(prev_i + 1, i, base_name)
9991123
fixtures_to_union.append(param_fix)
10001124
fixtures_to_union_names_for_ids.append(get_fixture_name(param_fix))
10011125

@@ -1016,7 +1140,7 @@ def replace_paramfixture_with_values(kwargs):
10161140
# remove the created fixture value
10171141
encompassing_fixture = kwargs.pop(base_name)
10181142
# and add instead the parameter values
1019-
if len(all_param_names) > 1:
1143+
if nb_params > 1:
10201144
for i, p in enumerate(all_param_names):
10211145
kwargs[p] = encompassing_fixture[i]
10221146
else:
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import pytest
2+
from pytest_cases import pytest_parametrize_plus, fixture_ref, pytest_fixture_plus
3+
4+
5+
@pytest_fixture_plus
6+
@pytest.mark.parametrize('val', ['b', 'c'])
7+
def myfix(val):
8+
return val
9+
10+
11+
@pytest_fixture_plus
12+
@pytest.mark.parametrize('val', [0, -1])
13+
def myfix2(val):
14+
return val
15+
16+
17+
@pytest_fixture_plus
18+
@pytest.mark.parametrize('val', [('d', 3),
19+
('e', 4)])
20+
def my_tuple(val):
21+
return val
22+
23+
24+
@pytest_parametrize_plus('p,q', [('a', 1),
25+
(fixture_ref(myfix), 2),
26+
(fixture_ref(myfix), fixture_ref(myfix2)),
27+
(fixture_ref(myfix), fixture_ref(myfix)),
28+
fixture_ref(my_tuple)])
29+
def test_prints(p, q):
30+
print(p, q)
31+
32+
33+
def test_synthesis(module_results_dct):
34+
assert list(module_results_dct) == ['test_prints[test_prints_p_q_is_0-a-1]',
35+
'test_prints[test_prints_p_q_is_fixtureproduct__1-b]',
36+
'test_prints[test_prints_p_q_is_fixtureproduct__1-c]',
37+
'test_prints[test_prints_p_q_is_fixtureproduct__2-b-0]',
38+
'test_prints[test_prints_p_q_is_fixtureproduct__2-b--1]',
39+
'test_prints[test_prints_p_q_is_fixtureproduct__2-c-0]',
40+
'test_prints[test_prints_p_q_is_fixtureproduct__2-c--1]',
41+
'test_prints[test_prints_p_q_is_fixtureproduct__3-b]',
42+
'test_prints[test_prints_p_q_is_fixtureproduct__3-c]',
43+
"test_prints[test_prints_p_q_is_my_tuple-('d', 3)]",
44+
"test_prints[test_prints_p_q_is_my_tuple-('e', 4)]"
45+
]

0 commit comments

Comments
 (0)