Skip to content

Commit 492bf80

Browse files
author
Sylvain MARIE
committed
@parametrize: custom ids are now correctly taken into account when a single lazy_valueis used for a tuple of parameters. This issue could be seen also with @parametrize_with_cases: idgen does not seem to be taken into account when cases are unpacked into a tuple. Fixes #144
docstring of `lazy_value` was clarified accordingly.
1 parent 03f6813 commit 492bf80

File tree

6 files changed

+282
-69
lines changed

6 files changed

+282
-69
lines changed

docs/api_reference.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,8 @@ A reference to a value getter (an argvalue-providing callable), to be used in [`
315315

316316
A `lazy_value` is the same thing than a function-scoped fixture, except that the value getter function is not a fixture and therefore can neither be parametrized nor depend on fixtures. It should have no mandatory argument.
317317

318+
By default the associated id is the name of the `valuegetter` callable, but a specific `id` can be provided otherwise. Note that this `id` does not take precedence over custom `ids` or `idgen` passed to `@parametrize`.
319+
318320
Note that a `lazy_value` can be included in a `pytest.param` without problem. In that case the id defined by `pytest.param` will take precedence over the one defined in `lazy_value` if any. The marks, however, will all be kept wherever they are defined.
319321

320322
**Parameters**
@@ -323,6 +325,13 @@ Note that a `lazy_value` can be included in a `pytest.param` without problem. In
323325
- `id`: an optional id. Otherwise `valuegetter.__name__` will be used by default
324326
- `marks`: optional marks. `valuegetter` marks will also be preserved.
325327

328+
### `is_lazy`
329+
330+
```python
331+
def is_lazy(argval) -> bool
332+
```
333+
334+
Return `True` if `argval` is the outcome of processing a `lazy_value` through `@parametrize`. This encompasses parameters that are items of lazy tuples that are created when parametrizing several argnames with the same `lazy_value()`.
326335

327336
### `fixture_ref`
328337

pytest_cases/common_pytest_lazy_values.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,9 @@ def lazy_value(valuegetter, # type: Callable[[], Any]
384384
A `lazy_value` is the same thing than a function-scoped fixture, except that the value getter function is not a
385385
fixture and therefore can neither be parametrized nor depend on fixtures. It should have no mandatory argument.
386386
387+
By default the associated id is the name of the `valuegetter` callable, but a specific `id` can be provided
388+
otherwise. Note that this `id` does not take precedence over custom `ids` or `idgen` passed to @parametrize.
389+
387390
Note that a `lazy_value` can be included in a `pytest.param` without problem. In that case the id defined by
388391
`pytest.param` will take precedence over the one defined in `lazy_value` if any. The marks, however,
389392
will all be kept wherever they are defined.

pytest_cases/fixture_parametrize_plus.py

Lines changed: 86 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,8 @@ def _make_ids(**args):
416416
# extract all marks and custom ids.
417417
# Do not check consistency of sizes argname/argvalue as a fixture_ref can stand for several argvalues.
418418
marked_argvalues = argvalues
419-
p_ids, p_marks, argvalues, fixture_indices = _process_argvalues(argnames, marked_argvalues, nb_params)
419+
has_cust_ids = (idgen is not None) or (ids is not None)
420+
p_ids, p_marks, argvalues, fixture_indices = _process_argvalues(argnames, marked_argvalues, nb_params, has_cust_ids)
420421

421422
# generate id
422423
if idgen is not None:
@@ -867,7 +868,7 @@ def gen_id_using_str_formatter(**params):
867868
return ids
868869

869870

870-
def _process_argvalues(argnames, marked_argvalues, nb_params):
871+
def _process_argvalues(argnames, marked_argvalues, nb_params, has_custom_ids):
871872
"""Internal method to use in _pytest_parametrize_plus
872873
873874
Processes the provided marked_argvalues (possibly marked with pytest.param) and returns
@@ -878,6 +879,8 @@ def _process_argvalues(argnames, marked_argvalues, nb_params):
878879
:param argnames:
879880
:param marked_argvalues:
880881
:param nb_params:
882+
:param has_custom_ids: a boolean indicating if custom ids are provided separately in `ids` or `idgen` (see
883+
@parametrize)
881884
:return:
882885
"""
883886
p_ids, p_marks, argvalues = extract_parameterset_info(argnames, marked_argvalues, check_nb=False)
@@ -887,80 +890,110 @@ def _process_argvalues(argnames, marked_argvalues, nb_params):
887890
if nb_params == 1:
888891
for i, v in enumerate(argvalues):
889892
if is_lazy_value(v):
890-
# Note: no need to modify the id, it will be ok thanks to the lazy_value class design
891-
# handle marks
893+
# --- A lazy value is used for several parameters at the same time ---
894+
# Users can declare custom marks in the lazy value API, we have to take these into account
895+
# (1) if there was a pytest.param around it, we have to merge the marks from the lazy value into it
896+
# (2) if there was no pytest.param around it and there are marks, we have to create the pytest.param
897+
# Note: a custom id in lazy value does not require such processing as it does not need to take
898+
# precedence over `ids` or `idgen`
899+
900+
# are there any marks in lazy_value ?
892901
_mks = v.get_marks(as_decorators=True)
893902
if len(_mks) > 0:
894-
# merge with the mark decorators possibly already present with pytest.param
895-
if p_marks[i] is None:
896-
p_marks[i] = []
897-
p_marks[i] = list(p_marks[i]) + _mks
903+
# update/create the pytest.param marks on this value
904+
p_marks[i] = (list(p_marks[i]) + _mks) if p_marks[i] is not None else list(_mks)
898905

899-
# update the marked_argvalues
906+
# update the original marked_argvalues
900907
marked_argvalues[i] = ParameterSet(values=(argvalues[i],), id=p_ids[i], marks=p_marks[i])
901-
del _mks
902908

903-
if isinstance(v, fixture_ref):
909+
elif isinstance(v, fixture_ref):
904910
fixture_indices.append((i, None))
911+
905912
elif nb_params > 1:
906913
for i, v in enumerate(argvalues):
914+
915+
# A/ First analyze what is the case at hand
916+
_lazyvalue_used_as_tuple = False
917+
_fixtureref_used_as_tuple = False
907918
if is_lazy_value(v):
908-
# a lazy value is used for several parameters at the same time, and is NOT between pytest.param()
919+
_lazyvalue_used_as_tuple = True
920+
elif isinstance(v, fixture_ref):
921+
_fixtureref_used_as_tuple = True
922+
elif len(v) == 1 and is_lazy_value(v[0]):
923+
# same than above but it was in a pytest.param
924+
argvalues[i] = v = v[0]
925+
_lazyvalue_used_as_tuple = True
926+
elif len(v) == 1 and isinstance(v[0], fixture_ref):
927+
# same than above but it was in a pytest.param
928+
_fixtureref_used_as_tuple = True
929+
argvalues[i] = v = v[0]
930+
931+
# B/ Now process it
932+
if _lazyvalue_used_as_tuple:
933+
# --- A lazy value is used for several parameters at the same time ---
934+
935+
# Since users have the possibility in the lazy value API to declare a custom id or custom marks,
936+
# we have to take these into account.
937+
# MARKS:
938+
# (1) if there was a pytest.param around it, we have to merge the marks from the lazy value into it
939+
# (2) if there was no pytest.param around it and there are marks, we have to create the pytest.param
940+
# IDS:
941+
# As opposed to the case of nb_params=1, we can not let pytest generate the id as it would create a
942+
# tuple of LazyTupleItem ids (e.g. <id>[0]-<id>[1]-...). So
943+
# (1) if there is a custom id list or generator, do not care about this.
944+
# (2) if there is a pytest.param with a custom id, do not care about this
945+
# (3) if there is nothing OR if there is a pytest.param with no id, we should create a pytest.param with
946+
# the id.
947+
948+
# in this particular case we have to modify the initial list
909949
argvalues[i] = v.as_lazy_tuple(nb_params)
910950

911-
# TUPLE usage: we HAVE to set an id to prevent too early access to the value by _idmaker
912-
# note that on pytest 2 we cannot set an id here, the lazy value wont be too lazy
913-
assert p_ids[i] is None
914-
_id = v.get_id()
915-
if not has_pytest_param:
916-
warn("The custom id %r in `lazy_value` will be ignored as this version of pytest is too old to"
917-
" support `pytest.param`." % _id)
918-
_id = None
951+
# TUPLE usage: if the id is not provided elsewhere we HAVE to set an id to avoid <id>[0]-<id>[1]...
952+
if p_ids[i] is None and not has_custom_ids:
953+
if not has_pytest_param:
954+
if v._id is not None:
955+
# (on pytest 2 we cannot do it since pytest.param does not exist)
956+
warn("The custom id %r in `lazy_value` will be ignored as this version of pytest is too old"
957+
" to support `pytest.param`." % v._id)
958+
else:
959+
pass # no warning, but no p_id update
960+
else:
961+
# update/create the pytest.param id on this value
962+
p_ids[i] = v.get_id()
919963

920964
# handle marks
921965
_mks = v.get_marks(as_decorators=True)
922966
if len(_mks) > 0:
923-
# merge with the mark decorators possibly already present with pytest.param
924-
assert p_marks[i] is None
925-
p_marks[i] = _mks
926-
927-
# note that here argvalues[i] IS a tuple-like so we do not create a tuple around it
928-
marked_argvalues[i] = ParameterSet(values=argvalues[i], id=_id, marks=_mks)
929-
p_ids[i] = _id
930-
del _id, _mks
967+
# update/create the pytest.param marks on this value
968+
p_marks[i] = (list(p_marks[i]) + _mks) if p_marks[i] is not None else list(_mks)
969+
970+
# update the marked_argvalues
971+
# - at least with the unpacked lazytuple if no pytest.param is there or needs to be created
972+
# - with a pytest.param if one is needed
973+
if p_ids[i] is None and p_marks[i] is None:
974+
marked_argvalues[i] = argvalues[i]
975+
else:
976+
# note that here argvalues[i] IS a tuple-like so we do not create a tuple around it
977+
marked_argvalues[i] = ParameterSet(values=argvalues[i], id=p_ids[i], marks=p_marks[i] or ())
931978

932-
elif isinstance(v, fixture_ref):
979+
elif _fixtureref_used_as_tuple:
933980
# a fixture ref is used for several parameters at the same time
934981
fixture_indices.append((i, None))
935-
936-
elif len(v) == 1 and is_lazy_value(v[0]):
937-
# same than above but it was in a pytest.mark
938-
# valueref_indices.append((i, None))
939-
argvalues[i] = v[0].as_lazy_tuple(nb_params) # unpack it
940-
if p_ids[i] is None:
941-
# force-use the id from the lazy value (do not have pytest request for it, that would unpack it)
942-
p_ids[i] = v[0].get_id()
943-
# handle marks
944-
_mks = v[0].get_marks(as_decorators=True)
945-
if len(_mks) > 0:
946-
# merge with the mark decorators possibly already present with pytest.param
947-
if p_marks[i] is None:
948-
p_marks[i] = []
949-
p_marks[i] = list(p_marks[i]) + _mks
950-
del _mks
951-
marked_argvalues[i] = ParameterSet(values=argvalues[i], id=p_ids[i], marks=p_marks[i])
952-
953-
elif len(v) == 1 and isinstance(v[0], fixture_ref):
954-
# same than above but it was in a pytest.mark
955-
fixture_indices.append((i, None))
956-
argvalues[i] = v[0] # unpack it
957982
else:
958-
# check for consistency
983+
# Tuple: check nb params for consistency
959984
if len(v) != len(argnames):
960985
raise ValueError("Inconsistent number of values in pytest parametrize: %s items found while the "
961986
"number of parameters is %s: %s." % (len(v), len(argnames), v))
962987

963-
# let's dig into the tuple
988+
# let's dig into the tuple to check if there are fixture_refs or lazy_values
989+
lv_pos_list = [j for j, _pval in enumerate(v) if is_lazy_value(_pval)]
990+
if len(lv_pos_list) > 0:
991+
_mks = [mk for _lv in lv_pos_list for mk in v[_lv].get_marks(as_decorators=True)]
992+
if len(_mks) > 0:
993+
# update/create the pytest.param marks on this value
994+
p_marks[i] = (list(p_marks[i]) + _mks) if p_marks[i] is not None else list(_mks)
995+
marked_argvalues[i] = ParameterSet(values=argvalues[i], id=p_ids[i], marks=p_marks[i] or ())
996+
964997
fix_pos_list = [j for j, _pval in enumerate(v) if isinstance(_pval, fixture_ref)]
965998
if len(fix_pos_list) > 0:
966999
# there is at least one fixture ref inside the tuple
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from pytest_cases import parametrize_with_cases, lazy_value, parametrize
2+
from pytest_cases.common_pytest_marks import has_pytest_param
3+
4+
5+
def case_dumb():
6+
return 1, 2
7+
8+
9+
@parametrize("a,b", [(1, object()), lazy_value(case_dumb)])
10+
def test_foo(a, b):
11+
pass
12+
13+
14+
@parametrize_with_cases('a,b', cases='.')
15+
def test_tuples_no_id(a, b):
16+
assert True
17+
18+
19+
# --------- now we do the same with an id generator
20+
21+
22+
@parametrize("a,b", [(1, object()), lazy_value(case_dumb)], ids=["hello", "world"])
23+
def test_foo2(a, b):
24+
pass
25+
26+
27+
def generate_id(**args):
28+
return "hello"
29+
30+
31+
@parametrize_with_cases('a,b', cases='.', idgen=generate_id)
32+
def test_tuples(a, b):
33+
assert True
34+
35+
36+
def test_synthesis(module_results_dct):
37+
if has_pytest_param:
38+
assert list(module_results_dct) == [
39+
"test_foo[1-b0]",
40+
"test_foo[case_dumb]",
41+
'test_tuples_no_id[dumb]',
42+
"test_foo2[hello]",
43+
"test_foo2[world]",
44+
'test_tuples[hello]',
45+
]
46+
else:
47+
# no pytest.param exists in this old pytest so the ids can not all be fixed
48+
assert list(module_results_dct) == [
49+
"test_foo[1-b0]",
50+
"test_foo[case_dumb[0]-case_dumb[1]]",
51+
'test_tuples_no_id[dumb[0]-dumb[1]]',
52+
"test_foo2[hello]",
53+
"test_foo2[world]",
54+
'test_tuples[hello]',
55+
]

pytest_cases/tests/pytest_extension/parametrize_plus/test_lazy_value.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# License: 3-clause BSD, <https://github.com/smarie/python-pytest-cases/blob/master/LICENSE>
55
import pytest
66

7-
from pytest_cases import parametrize_plus, lazy_value
7+
from pytest_cases import parametrize, lazy_value
88

99

1010
has_pytest_param = hasattr(pytest, 'param')
@@ -24,16 +24,16 @@ def val():
2424

2525

2626
if not has_pytest_param:
27-
@parametrize_plus("a", [lazy_value(val),
28-
lazy_value(val_skipped_on_old_pytest),
29-
lazy_value(val, id='A')])
27+
@parametrize("a", [lazy_value(val),
28+
lazy_value(val_skipped_on_old_pytest),
29+
lazy_value(val, id='A')])
3030
def test_foo_single(a):
3131
"""here the fixture is used for both parameters at the same time"""
3232
assert a == 1
3333

3434

35-
@parametrize_plus("a,b", [lazy_value(valtuple),
36-
(1, lazy_value(val))])
35+
@parametrize("a,b", [lazy_value(valtuple),
36+
(1, lazy_value(val))])
3737
def test_foo_multi(a, b):
3838
"""here the fixture is used for both parameters at the same time"""
3939
assert (a, b) == (1, 2) or (a, b) == (1, 1)
@@ -48,11 +48,11 @@ def test_synthesis(module_results_dct):
4848
'test_foo_multi[1-val]']
4949

5050
else:
51-
@parametrize_plus("a", [lazy_value(val),
52-
pytest.param(lazy_value(val_skipped_on_old_pytest), marks=pytest.mark.skip),
53-
pytest.param(lazy_value(val), id='B'),
54-
pytest.param(lazy_value(val, id='ignored'), id='C'),
55-
lazy_value(val, id='A')])
51+
@parametrize("a", [lazy_value(val),
52+
pytest.param(lazy_value(val_skipped_on_old_pytest), marks=pytest.mark.skip),
53+
pytest.param(lazy_value(val), id='B'),
54+
pytest.param(lazy_value(val, id='ignored'), id='C'),
55+
lazy_value(val, id='A')])
5656
def test_foo_single(a):
5757
"""here the fixture is used for both parameters at the same time"""
5858
assert a == 1
@@ -70,11 +70,14 @@ def valtuple_toskip():
7070
return 15, 2
7171

7272

73-
@parametrize_plus("a,b", [lazy_value(valtuple),
74-
pytest.param(lazy_value(valtuple_only_right_when_lazy), id='A'),
75-
pytest.param(lazy_value(valtuple_toskip, marks=(pytest.mark.xfail,)), id='Wrong', marks=pytest.mark.skip),
76-
(1, lazy_value(val)),
77-
pytest.param(1, lazy_value(val), id='B')])
73+
@parametrize("a,b", [lazy_value(valtuple),
74+
lazy_value(valtuple, id="hello"),
75+
lazy_value(valtuple_toskip, marks=pytest.mark.skip),
76+
(1, lazy_value(valtuple_toskip, marks=pytest.mark.skip)),
77+
pytest.param(lazy_value(valtuple_only_right_when_lazy), id='A'),
78+
pytest.param(lazy_value(valtuple_toskip, marks=(pytest.mark.xfail,)), id='Wrong', marks=pytest.mark.skip),
79+
(1, lazy_value(val)),
80+
pytest.param(1, lazy_value(val), id='B')])
7881
def test_foo_multi(a, b):
7982
"""here the fixture is used for both parameters at the same time"""
8083
global flag
@@ -88,6 +91,7 @@ def test_synthesis2(module_results_dct):
8891
'test_foo_single[C]',
8992
'test_foo_single[A]',
9093
'test_foo_multi[valtuple]',
94+
'test_foo_multi[hello]',
9195
'test_foo_multi[A]',
9296
'test_foo_multi[1-val]',
9397
'test_foo_multi[B]']

0 commit comments

Comments
 (0)