Skip to content

Commit 870397d

Browse files
authored
Merge pull request #88 from smarie/fix_issue_85_87
Fixed issues 85 (`fixture_union` when using the same fixture twice in it) and 87 (`ids` precedence order when using `pytest.mark.parametrize` in a `fixture_plus`)
2 parents 0fad070 + 664a207 commit 870397d

File tree

8 files changed

+188
-93
lines changed

8 files changed

+188
-93
lines changed

ci_tools/generate-junit-badge.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import sys
2+
from math import floor
3+
24
try:
35
# python 3
46
from urllib.parse import quote_plus
@@ -34,7 +36,7 @@ def get_test_stats(junit_xml='reports/junit/junit.xml' # type: str
3436
failed = len(tr.failures)
3537
success = runned - failed
3638

37-
success_percentage = round(success * 100 / runned)
39+
success_percentage = floor(success * 100 / runned)
3840

3941
return TestStats(success_percentage, success, runned, skipped)
4042

docs/changelog.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# Changelog
22

3-
### 1.13.2 - in progress - bugfixes and hook feature
3+
### 1.14.0 - bugfixes and hook feature
4+
5+
- Fixed `ids` precedence order when using `pytest.mark.parametrize` in a `fixture_plus`. Fixed [#87](https://github.com/smarie/python-pytest-cases/issues/87)
6+
7+
- Fixed issue with `fixture_union` when using the same fixture twice in it. Fixes [#85](https://github.com/smarie/python-pytest-cases/issues/85)
48

59
- Added the possibility to pass a `hook` function in all API where fixtures are created behind the scenes, so as to ease debugging and/or save fixtures (with `stored_fixture` from pytest harvest). Fixes [#83](https://github.com/smarie/python-pytest-cases/issues/83)
610

pytest_cases/common.py

Lines changed: 73 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -232,12 +232,15 @@ def get_pytest_parametrize_marks(f):
232232
# mark_info.args contains a list of (name, values)
233233
if len(mark_info.args) % 2 != 0:
234234
raise ValueError("internal pytest compatibility error - please report")
235-
nb_parameters = len(mark_info.args) // 2
236-
if nb_parameters > 1 and len(mark_info.kwargs) > 0:
235+
nb_parametrize_decorations = len(mark_info.args) // 2
236+
if nb_parametrize_decorations > 1 and len(mark_info.kwargs) > 0:
237237
raise ValueError("Unfortunately with this old pytest version it is not possible to have several "
238-
"parametrization decorators")
238+
"parametrization decorators while specifying **kwargs, as all **kwargs are "
239+
"merged, leading to inconsistent results. Either upgrade pytest, remove the **kwargs,"
240+
"or merge all the @parametrize decorators into a single one. **kwargs: %s"
241+
% mark_info.kwargs)
239242
res = []
240-
for i in range(nb_parameters):
243+
for i in range(nb_parametrize_decorations):
241244
param_name, param_values = mark_info.args[2*i:2*(i+1)]
242245
res.append(_ParametrizationMark(_LegacyMark(param_name, param_values, **mark_info.kwargs)))
243246
return tuple(res)
@@ -259,6 +262,18 @@ def get_parametrize_signature():
259262

260263

261264
# ---------- test ids utils ---------
265+
def combine_ids(paramid_tuples):
266+
"""
267+
Receives a list of tuples containing ids for each parameterset.
268+
Returns the final ids, that are obtained by joining the various param ids by '-' for each test node
269+
270+
:param paramid_tuples:
271+
:return:
272+
"""
273+
#
274+
return ['-'.join(pid for pid in testid) for testid in paramid_tuples]
275+
276+
262277
def get_test_ids_from_param_values(param_names,
263278
param_values,
264279
):
@@ -285,43 +300,81 @@ def get_test_ids_from_param_values(param_names,
285300

286301

287302
# ---- ParameterSet api ---
288-
def extract_parameterset_info(pnames, pmark):
303+
def analyze_parameter_set(pmark=None, argnames=None, argvalues=None, ids=None):
289304
"""
305+
analyzes a parameter set passed either as a pmark or as distinct
306+
(argnames, argvalues, ids) to extract/construct the various ids, marks, and
307+
values
290308
291-
:param pnames: the names in this parameterset
292-
:param pmark: the parametrization mark (a _ParametrizationMark)
309+
:return: ids, marks, values
310+
"""
311+
if pmark is not None:
312+
if any(a is not None for a in (argnames, argvalues, ids)):
313+
raise ValueError("Either provide a pmark OR the details")
314+
argnames = pmark.param_names
315+
argvalues = pmark.param_values
316+
ids = pmark.param_ids
317+
318+
# extract all parameters that have a specific configuration (pytest.param())
319+
custom_pids, p_marks, p_values = extract_parameterset_info(argnames, argvalues)
320+
321+
# Create the proper id for each test
322+
if ids is not None:
323+
# overridden at global pytest.mark.parametrize level - this takes precedence.
324+
try: # an explicit list of ids ?
325+
p_ids = list(ids)
326+
except TypeError: # a callable to apply on the values
327+
p_ids = list(ids(v) for v in p_values)
328+
else:
329+
# default: values-based
330+
p_ids = get_test_ids_from_param_values(argnames, p_values)
331+
332+
# Finally, local pytest.param takes precedence over everything else
333+
for i, _id in enumerate(custom_pids):
334+
if _id is not None:
335+
p_ids[i] = _id
336+
337+
return p_ids, p_marks, p_values
338+
339+
340+
def extract_parameterset_info(argnames, argvalues):
341+
"""
342+
343+
:param argnames: the names in this parameterset
344+
:param argvalues: the values in this parameterset
293345
:return:
294346
"""
295-
_pids = []
296-
_pmarks = []
297-
_pvalues = []
298-
for v in pmark.param_values:
347+
pids = []
348+
pmarks = []
349+
pvalues = []
350+
for v in argvalues:
299351
if is_marked_parameter_value(v):
300352
# --id
301353
id = get_marked_parameter_id(v)
302-
_pids.append(id)
354+
pids.append(id)
303355
# --marks
304356
marks = get_marked_parameter_marks(v)
305-
_pmarks.append(marks) # note: there might be several
357+
pmarks.append(marks) # note: there might be several
306358
# --value(a tuple if this is a tuple parameter)
307359
vals = get_marked_parameter_values(v)
308-
if len(vals) != len(pnames):
360+
if len(vals) != len(argnames):
309361
raise ValueError("Internal error - unsupported pytest parametrization+mark combination. Please "
310362
"report this issue")
311363
if len(vals) == 1:
312-
_pvalues.append(vals[0])
364+
pvalues.append(vals[0])
313365
else:
314-
_pvalues.append(vals)
366+
pvalues.append(vals)
315367
else:
316-
_pids.append(None)
317-
_pmarks.append(None)
318-
_pvalues.append(v)
368+
pids.append(None)
369+
pmarks.append(None)
370+
pvalues.append(v)
319371

320-
return _pids, _pmarks, _pvalues
372+
return pids, pmarks, pvalues
321373

322374

323375
try: # pytest 3.x+
324376
from _pytest.mark import ParameterSet
377+
325378
def is_marked_parameter_value(v):
326379
return isinstance(v, ParameterSet)
327380

pytest_cases/main_fixtures.py

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
pass
4141

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

@@ -579,34 +579,21 @@ def _decorate_fixture_plus(fixture_func,
579579
params_ids = []
580580
params_marks = []
581581
for pmark in parametrizer_marks:
582+
# -- pmark is a single @pytest.parametrize mark. --
583+
582584
# check number of parameter names in this parameterset
583585
if len(pmark.param_names) < 1:
584586
raise ValueError("Fixture function '%s' decorated with '@fixture_plus' has an empty parameter "
585587
"name in a @pytest.mark.parametrize mark")
586588

587-
# remember
589+
# remember the argnames
588590
params_names_or_name_combinations.append(pmark.param_names)
589591

590-
# extract all parameters that have a specific configuration (pytest.param())
591-
_pids, _pmarks, _pvalues = extract_parameterset_info(pmark.param_names, pmark)
592-
593-
# Create the proper id for each test
594-
if pmark.param_ids is not None:
595-
# overridden at global pytest.mark.parametrize level - this takes precedence.
596-
try: # an explicit list of ids ?
597-
paramids = list(pmark.param_ids)
598-
except TypeError: # a callable to apply on the values
599-
paramids = list(pmark.param_ids(v) for v in _pvalues)
600-
else:
601-
# default: values-based...
602-
paramids = get_test_ids_from_param_values(pmark.param_names, _pvalues)
603-
# ...but local pytest.param takes precedence
604-
for i, _id in enumerate(_pids):
605-
if _id is not None:
606-
paramids[i] = _id
592+
# analyse contents, extract marks and custom ids, apply custom ids
593+
_paramids, _pmarks, _pvalues = analyze_parameter_set(pmark)
607594

608595
# Finally store the ids, marks, and values for this parameterset
609-
params_ids.append(paramids)
596+
params_ids.append(_paramids)
610597
params_marks.append(tuple(_pmarks))
611598
params_values.append(tuple(_pvalues))
612599

@@ -623,7 +610,7 @@ def _decorate_fixture_plus(fixture_func,
623610
final_values[i] = make_marked_parameter_value(final_values[i], marks=marks)
624611
else:
625612
final_values = list(product(*params_values))
626-
final_ids = get_test_ids_from_param_values(params_names_or_name_combinations, product(*params_ids))
613+
final_ids = combine_ids(product(*params_ids))
627614
final_marks = tuple(product(*params_marks))
628615

629616
# reapply the marks
@@ -972,9 +959,17 @@ def _fixture_union(caller_module,
972959
if len(f_names) < 1:
973960
raise ValueError("Empty fixture unions are not permitted")
974961

962+
# remove duplicates in the fixture arguments
963+
f_names_args = []
964+
for _fname in f_names:
965+
if _fname in f_names_args:
966+
warn("Creating a fixture union %r where two alternatives are the same fixture %r." % (name, _fname))
967+
else:
968+
f_names_args.append(_fname)
969+
975970
# then generate the body of our union fixture. It will require all of its dependent fixtures and receive as
976971
# a parameter the name of the fixture to use
977-
@with_signature("%s(%s, request)" % (name, ', '.join(f_names)))
972+
@with_signature("%s(%s, request)" % (name, ', '.join(f_names_args)))
978973
def _new_fixture(request, **all_fixtures):
979974
if not is_used_request(request):
980975
return NOT_USED

pytest_cases/plugin.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -982,8 +982,11 @@ def _process_node(self, current_node, pending, calls):
982982
ids=p_to_apply.ids,
983983
scope=p_to_apply.scope, **p_to_apply.kwargs)
984984

985-
# Change the ids
985+
# Change the ids by applying the style defined in the corresponding alternative
986986
for callspec in calls:
987+
# TODO right now only the idstyle defined in the first alternative is used. But it cant be
988+
# different from the other ones for now because of the way fixture_union is built.
989+
# Maybe the best would be to remove this and apply the id style when fixture is created.
987990
callspec._idlist[-1] = apply_id_style(callspec._idlist[-1],
988991
p_to_apply.union_fixture_name,
989992
p_to_apply.alternative_names[0].idstyle)

pytest_cases/tests/fixtures/test_fixtures_parametrize.py

Lines changed: 60 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,6 @@
33
from pytest_cases import pytest_fixture_plus
44
import pytest
55

6-
# pytest.param - not available in all versions
7-
if LooseVersion(pytest.__version__) >= LooseVersion('3.0.0'):
8-
pytest_param = pytest.param
9-
else:
10-
def pytest_param(*args, **kwargs):
11-
return args
12-
136

147
@pytest_fixture_plus(scope="module")
158
@pytest.mark.parametrize("arg1", ["one", "two"])
@@ -24,48 +17,69 @@ def test_one(myfix):
2417
print(myfix)
2518

2619

27-
@pytest_fixture_plus
28-
@pytest.mark.parametrize("arg1, arg2", [
29-
(1, 2),
30-
pytest_param(3, 4, id="p_a"),
31-
pytest_param(5, 6, id="skipped", marks=pytest.mark.skip)
32-
])
33-
def myfix2(arg1, arg2):
34-
return arg1, arg2
35-
36-
37-
def test_two(myfix2):
38-
assert myfix2 in {(1, 2), (3, 4), (5, 6)}
39-
print(myfix2)
40-
41-
42-
@pytest_fixture_plus
43-
@pytest.mark.parametrize("arg1, arg2", [
44-
pytest_param(5, 6, id="ignored_id")
45-
], ids=['a'])
46-
def myfix3(arg1, arg2):
47-
return arg1, arg2
48-
49-
50-
def test_two(myfix2, myfix3):
51-
assert myfix2 in {(1, 2), (3, 4), (5, 6)}
52-
print(myfix2)
53-
54-
5520
def test_synthesis(module_results_dct):
5621
"""Use pytest-harvest to check that the list of executed tests is correct """
5722

58-
if LooseVersion(pytest.__version__) >= LooseVersion('3.0.0'):
59-
id_of_last_tests = ['p_a', 'skipped']
60-
extra_test = []
61-
else:
62-
id_of_last_tests = ['3-4', '5-6']
63-
extra_test = ['test_two[%s-a]' % id_of_last_tests[1]]
64-
6523
assert list(module_results_dct) == ['test_one[one-one]',
6624
'test_one[one-two]',
6725
'test_one[two-one]',
68-
'test_one[two-two]',
69-
'test_two[1-2-a]',
70-
'test_two[%s-a]' % id_of_last_tests[0],
71-
] + extra_test
26+
'test_one[two-two]']
27+
28+
29+
# pytest.param - not available in all versions
30+
if LooseVersion(pytest.__version__) < LooseVersion('3.2.0'):
31+
# with pytest < 3.2.0 we
32+
# - would have to merge all parametrize marks if we wish to pass a kwarg (here, ids)
33+
# - cannot use pytest.param as it is not taken into account
34+
# > no go
35+
36+
def test_warning_pytest2():
37+
with pytest.raises(ValueError) as exc_info:
38+
@pytest_fixture_plus
39+
@pytest.mark.parametrize("arg2", [0], ids=str)
40+
@pytest.mark.parametrize("arg1", [1])
41+
def a(arg1, arg2):
42+
return arg1, arg2
43+
assert "Unfortunately with this old pytest version it" in str(exc_info.value)
44+
45+
else:
46+
@pytest_fixture_plus
47+
@pytest.mark.parametrize("arg3", [pytest.param(0, id='!0!')], ids=str)
48+
@pytest.mark.parametrize("arg1, arg2", [
49+
(1, 2),
50+
pytest.param(3, 4, id="p_a"),
51+
pytest.param(5, 6, id="skipped", marks=pytest.mark.skip)
52+
])
53+
def myfix2(arg1, arg2, arg3):
54+
return arg1, arg2, arg3
55+
56+
57+
def test_two(myfix2):
58+
assert myfix2 in {(1, 2, 0), (3, 4, 0), (5, 6, 0)}
59+
print(myfix2)
60+
61+
62+
@pytest_fixture_plus
63+
@pytest.mark.parametrize("arg1, arg2", [
64+
pytest.param(5, 6, id="a")
65+
], ids=['ignored_id'])
66+
def myfix3(arg1, arg2):
67+
return arg1, arg2
68+
69+
70+
def test_three(myfix2, myfix3):
71+
assert myfix2 in {(1, 2, 0), (3, 4, 0), (5, 6, 0)}
72+
print(myfix2)
73+
74+
75+
def test_synthesis(module_results_dct):
76+
"""Use pytest-harvest to check that the list of executed tests is correct """
77+
78+
assert list(module_results_dct) == ['test_one[one-one]',
79+
'test_one[one-two]',
80+
'test_one[two-one]',
81+
'test_one[two-two]',
82+
'test_two[1-2-!0!]',
83+
'test_two[p_a-!0!]',
84+
'test_three[1-2-!0!-a]',
85+
'test_three[p_a-!0!-a]']
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pytest
22

33
from pytest_cases.main_fixtures import InvalidParamsList
4-
from pytest_cases import pytest_parametrize_plus, fixture_ref
4+
from pytest_cases import parametrize_plus, fixture_ref
55

66

77
@pytest.fixture
@@ -11,6 +11,6 @@ def test():
1111

1212
def test_invalid_argvalues():
1313
with pytest.raises(InvalidParamsList):
14-
@pytest_parametrize_plus('main_msg', fixture_ref(test))
14+
@parametrize_plus('main_msg', fixture_ref(test))
1515
def test_prints(main_msg):
1616
print(main_msg)

0 commit comments

Comments
 (0)