Skip to content

Commit 8386a41

Browse files
authored
Merge pull request #95 from smarie/fix_issue_92
New `lazy_value` for parametrize_plus
2 parents 9be81c4 + d34c622 commit 8386a41

27 files changed

+766
-48
lines changed

docs/index.md

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,21 @@
88

99
!!! success "You can now use `pytest.param` in the argvalues provided to `fixture_union`, `param_fixture[s]` and `parametrize_plus`, just as you do in `pytest`. See [pytest documentation](https://docs.pytest.org/en/stable/example/parametrize.html#set-marks-or-test-id-for-individual-parametrized-test)"
1010

11+
!!! success "New `lazy_value` feature for parametrize, [check it out](#parametrize_plus) !"
12+
1113
!!! warning "Test execution order"
1214
Installing pytest-cases now has effects on the order of `pytest` tests execution, even if you do not use its features. One positive side effect is that it fixed [pytest#5054](https://github.com/pytest-dev/pytest/issues/5054). But if you see less desirable ordering please [report it](https://github.com/smarie/python-pytest-cases/issues).
1315

14-
!!! warning "New aliases"
15-
`pytest_fixture_plus` and `pytest_parametrize_plus` were renamed to `fixture_plus` and `parametrize_plus` in order for pytest (pluggy) not to think they were hooks. Old aliases will stay around for a few versions, with a deprecation warning. See [#71](https://github.com/smarie/python-pytest-cases/issues/71).
16-
1716
Did you ever think that most of your test functions were actually *the same test code*, but with *different data inputs* and expected results/exceptions ?
1817

1918
`pytest-cases` leverages `pytest` and its great `@pytest.mark.parametrize` decorator, so that you can **separate your test cases from your test functions**. For example with `pytest-cases` you can now write your tests with the following pattern:
2019

2120
* on one hand, the usual `test_xxxx.py` file containing your test functions
2221
* on the other hand, a new `test_xxxx_cases.py` containing your cases functions
2322

24-
`pytest-cases` is fully compliant with [pytest-steps](https://smarie.github.io/python-pytest-steps/) so you can create test suites with several steps and send each case on the full suite. See [usage page for details](./usage/advanced/#test-suites-several-steps-on-each-case).
25-
2623
In addition, `pytest-cases` improves `pytest`'s fixture mechanism to support "fixture unions". This is a **major change** in the internal `pytest` engine, unlocking many possibilities such as using fixture references as parameter values in a test function. See [below](#fixture_union).
2724

25+
`pytest-cases` is fully compliant with [pytest-steps](https://smarie.github.io/python-pytest-steps/) so you can create test suites with several steps and send each case on the full suite. See [usage page for details](./usage/advanced/#test-suites-several-steps-on-each-case).
2826

2927
## Installing
3028

@@ -363,27 +361,35 @@ Fixture unions are a **major change** in the internal pytest engine, as fixture
363361

364362
### `@parametrize_plus`
365363

366-
`@parametrize_plus` is a replacement for `@pytest.mark.parametrize` that allows you to include references to fixtures in the parameter values. Simply use `fixture_ref(<fixture>)` in the parameter values, where `<fixture>` can be the fixture name or fixture function.
364+
`@parametrize_plus` is a replacement for `@pytest.mark.parametrize` that allows you to include references to fixtures and to value-generating functions in the parameter values.
367365

368-
For example:
366+
- Simply use `fixture_ref(<fixture>)` in the parameter values, where `<fixture>` can be the fixture name or fixture function.
367+
- if you do not wish to create a fixture, you can also use `lazy_value(<function>)`
368+
- Note that when parametrizing several argnames, both `fixture_ref` and `lazy_value` can be used *as* the tuple, or *in* the tuple. Several `fixture_ref` and/or `lazy_value` can be used in the same tuple, too.
369+
370+
For example, with a single argument:
369371

370372
```python
371373
import pytest
372-
from pytest_cases import parametrize_plus, fixture_plus, fixture_ref
374+
from pytest_cases import parametrize_plus, fixture_plus, fixture_ref, lazy_value
373375

374376
@pytest.fixture
375377
def world_str():
376378
return 'world'
377379

380+
def whatfun():
381+
return 'what'
382+
378383
@fixture_plus
379384
@parametrize_plus('who', [fixture_ref(world_str),
380-
'you'])
385+
'you'])
381386
def greetings(who):
382387
return 'hello ' + who
383388

384389
@parametrize_plus('main_msg', ['nothing',
385-
fixture_ref(world_str),
386-
fixture_ref(greetings)])
390+
fixture_ref(world_str),
391+
lazy_value(whatfun),
392+
fixture_ref(greetings)])
387393
@pytest.mark.parametrize('ending', ['?', '!'])
388394
def test_prints(main_msg, ending):
389395
print(main_msg + ending)
@@ -393,22 +399,24 @@ yields the following
393399

394400
```bash
395401
> pytest -s -v
396-
collected 9 items
397-
test_prints[test_prints_main_msg_is_0-nothing-?] nothing? PASSED
398-
test_prints[test_prints_main_msg_is_0-nothing-!] nothing! PASSED
399-
test_prints[test_prints_main_msg_is_world_str-?] world? PASSED
400-
test_prints[test_prints_main_msg_is_world_str-!] world! PASSED
401-
test_prints[test_prints_main_msg_is_greetings-greetings_who_is_world_str-?] hello world? PASSED
402-
test_prints[test_prints_main_msg_is_greetings-greetings_who_is_world_str-!] hello world! PASSED
403-
test_prints[test_prints_main_msg_is_greetings-greetings_who_is_1-you-?] hello you? PASSED
404-
test_prints[test_prints_main_msg_is_greetings-greetings_who_is_1-you-!] hello you! PASSED
402+
collected 10 items
403+
test_prints[main_msg_is_nothing-?] PASSED [ 10%]nothing?
404+
test_prints[main_msg_is_nothing-!] PASSED [ 20%]nothing!
405+
test_prints[main_msg_is_world_str-?] PASSED [ 30%]world?
406+
test_prints[main_msg_is_world_str-!] PASSED [ 40%]world!
407+
test_prints[main_msg_is_whatfun-?] PASSED [ 50%]what?
408+
test_prints[main_msg_is_whatfun-!] PASSED [ 60%]what!
409+
test_prints[main_msg_is_greetings-who_is_world_str-?] PASSED [ 70%]hello world?
410+
test_prints[main_msg_is_greetings-who_is_world_str-!] PASSED [ 80%]hello world!
411+
test_prints[main_msg_is_greetings-who_is_you-?] PASSED [ 90%]hello you?
412+
test_prints[main_msg_is_greetings-who_is_you-!] PASSED [100%]hello you!
405413
```
406414

407-
As you can see, the ids are a bit more explicit than usual. As opposed to `fixture_union`, the style of these ids is not configurable for now but feel free to propose alternatives in the [issues page](https://github.com/smarie/python-pytest-cases/issues).
408-
409415
You can also mark any of the argvalues with `pytest.mark` to pass a custom id or a custom "skip" or "fail" mark, just as you do in `pytest`. See [pytest documentation](https://docs.pytest.org/en/stable/example/parametrize.html#set-marks-or-test-id-for-individual-parametrized-test).
410416

411-
Note: for this to be performed, the parameters are replaced with a union fixture. Therefore the relative priority order of these parameters with other standard `pytest.mark.parametrize` parameters that you would place on the same function, will get impacted. You may solve this by replacing your other `@pytest.mark.parametrize` calls with `param_fixture`s (see [above](#param_fixtures).)
417+
As you can see in the example above, the default ids are a bit more explicit than usual when you use at least one `fixture_ref`. This is because the parameters need to be replaced with a fixture union that will "switch" between alternative groups of parameters, and the appropriate fixtures referenced. As opposed to `fixture_union`, the style of these ids is not configurable for now, but feel free to propose alternatives in the [issues page](https://github.com/smarie/python-pytest-cases/issues). Note that this does not happen if you only use `lazy_value`s, as they do not require to create a fixture union behind the scenes.
418+
419+
Another consequence of using `fixture_ref` is that the priority order of the parameters, relative to other standard `pytest.mark.parametrize` parameters that you would place on the same function, will get impacted. You may solve this by replacing your other `@pytest.mark.parametrize` calls with `param_fixture`s so that all the parameters are fixtures (see [above](#param_fixtures).)
412420

413421
### passing a `hook`
414422

pytest_cases/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from .fixture_core1_unions import fixture_union, NOT_USED, unpack_fixture, ignore_unused
22
from .fixture_core2 import pytest_fixture_plus, fixture_plus, param_fixtures, param_fixture
3-
from .fixture_parametrize_plus import pytest_parametrize_plus, parametrize_plus, fixture_ref
3+
from .fixture_parametrize_plus import pytest_parametrize_plus, parametrize_plus, fixture_ref, lazy_value
44

55
from .case_funcs import case_name, test_target, case_tags, cases_generator
66
from .case_parametrizer import cases_data, CaseDataGetter, unfold_expected_err, get_all_cases, THIS_MODULE, \
@@ -24,6 +24,7 @@
2424
'case_funcs', 'case_parametrizer', 'fixture_core1_unions', 'fixture_core2', 'fixture_parametrize_plus',
2525
# all symbols imported above
2626
# --cases_funcs
27+
'lazy_value',
2728
'case_name', 'test_target', 'case_tags', 'cases_generator',
2829
# --main_fixtures
2930
'cases_fixture', 'pytest_fixture_plus', 'fixture_plus', 'param_fixtures', 'param_fixture', 'ignore_unused',

pytest_cases/common_pytest.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,17 @@ def get_marked_parameter_id(v):
467467
except ImportError: # pytest 2.x
468468
from _pytest.mark import MarkDecorator
469469

470+
def ParameterSet(values, id, marks):
471+
""" Dummy function (not a class) used only by parametrize_plus """
472+
if id is not None:
473+
raise ValueError("This should not happen as `pytest.param` does not exist in pytest 2")
474+
for m in marks:
475+
values = pytest.mark()
476+
raise ValueError("TODO")
477+
478+
# smart unpack is required for compatibility
479+
return values[0] if len(values) == 1 else values
480+
470481
def is_marked_parameter_value(v):
471482
return isinstance(v, MarkDecorator)
472483

pytest_cases/fixture_core1_unions.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ def is_fixture_union_params(params):
122122
if len(params) < 1:
123123
return False
124124
else:
125+
if getattr(params, '__module__', '').startswith('pytest_cases'):
126+
# a value_ref_tuple or another proxy object created somewhere in our code, not a list
127+
return False
125128
p0 = params[0]
126129
if is_marked_parameter_value(p0):
127130
p0 = get_marked_parameter_values(p0)[0]

pytest_cases/fixture_core2.py

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def param_fixture(argname, # type: str
3333
ids=None, # type: Union[Callable, List[str]]
3434
scope="function", # type: str
3535
hook=None, # type: Callable[[Callable], Callable]
36+
debug=False, # type: bool
3637
**kwargs):
3738
"""
3839
Identical to `param_fixtures` but for a single parameter name, so that you can assign its output to a single
@@ -62,6 +63,7 @@ def test_uses_param(my_parameter, fixture_uses_param):
6263
will be called everytime a fixture is about to be created. It will receive a single argument (the function
6364
implementing the fixture) and should return the function to use. For example you can use `saved_fixture` from
6465
`pytest-harvest` as a hook in order to save all such created fixtures in the fixture store.
66+
:param debug: print debug messages on stdout to analyze fixture creation (use pytest -s to see them)
6567
:param kwargs: any other argument for 'fixture'
6668
:return: the create fixture
6769
"""
@@ -74,7 +76,7 @@ def test_uses_param(my_parameter, fixture_uses_param):
7476
caller_module = get_caller_module()
7577

7678
return _create_param_fixture(caller_module, argname, argvalues, autouse=autouse, ids=ids, scope=scope,
77-
hook=hook, **kwargs)
79+
hook=hook, debug=debug, **kwargs)
7880

7981

8082
def _create_param_fixture(caller_module,
@@ -85,6 +87,7 @@ def _create_param_fixture(caller_module,
8587
scope="function", # type: str
8688
hook=None, # type: Callable[[Callable], Callable]
8789
auto_simplify=False,
90+
debug=False,
8891
**kwargs):
8992
""" Internal method shared with param_fixture and param_fixtures """
9093

@@ -99,13 +102,19 @@ def _create_param_fixture(caller_module,
99102
def __param_fixture():
100103
return argvalue_to_return
101104

105+
if debug:
106+
print("Creating unparametrized fixture %r returning %r" % (argname, argvalue_to_return))
107+
102108
fix = fixture_plus(name=argname, scope=scope, autouse=autouse, ids=ids, hook=hook, **kwargs)(__param_fixture)
103109
else:
104110
# create the fixture - set its name so that the optional hook can read it easily
105111
@with_signature("%s(request)" % argname)
106112
def __param_fixture(request):
107113
return request.param
108114

115+
if debug:
116+
print("Creating parametrized fixture %r returning %r" % (argname, argvalues))
117+
109118
fix = fixture_plus(name=argname, scope=scope, autouse=autouse, params=argvalues, ids=ids,
110119
hook=hook, **kwargs)(__param_fixture)
111120

@@ -122,6 +131,7 @@ def param_fixtures(argnames,
122131
ids=None, # type: Union[Callable, List[str]]
123132
scope="function", # type: str
124133
hook=None, # type: Callable[[Callable], Callable]
134+
debug=False, # type: bool
125135
**kwargs):
126136
"""
127137
Creates one or several "parameters" fixtures - depending on the number or coma-separated names in `argnames`. The
@@ -156,17 +166,33 @@ def test_uses_param2(arg1, arg2, fixture_uses_param2):
156166
will be called everytime a fixture is about to be created. It will receive a single argument (the function
157167
implementing the fixture) and should return the function to use. For example you can use `saved_fixture` from
158168
`pytest-harvest` as a hook in order to save all such created fixtures in the fixture store.
169+
:param debug: print debug messages on stdout to analyze fixture creation (use pytest -s to see them)
159170
:param kwargs: any other argument for the created 'fixtures'
160171
:return: the created fixtures
161172
"""
162-
created_fixtures = []
163173
argnames_lst = get_param_argnames_as_list(argnames)
164174

165175
caller_module = get_caller_module()
166176

167177
if len(argnames_lst) < 2:
168178
return _create_param_fixture(caller_module, argnames, argvalues, autouse=autouse, ids=ids, scope=scope,
169-
hook=hook, **kwargs)
179+
hook=hook, debug=debug, **kwargs)
180+
else:
181+
return _create_params_fixture(caller_module, argnames_lst, argvalues, autouse=autouse, ids=ids, scope=scope,
182+
hook=hook, debug=debug, **kwargs)
183+
184+
185+
def _create_params_fixture(caller_module,
186+
argnames_lst, # type: Sequence[str]
187+
argvalues, # type: Sequence[Any]
188+
autouse=False, # type: bool
189+
ids=None, # type: Union[Callable, List[str]]
190+
scope="function", # type: str
191+
hook=None, # type: Callable[[Callable], Callable]
192+
debug=False, # type: bool
193+
**kwargs):
194+
argnames = ','.join(argnames_lst)
195+
created_fixtures = []
170196

171197
# create the root fixture that will contain all parameter values
172198
# note: we sort the list so that the first in alphabetical order appears first. Indeed pytest uses this order.
@@ -176,6 +202,9 @@ def test_uses_param2(arg1, arg2, fixture_uses_param2):
176202
root_fixture_name = check_name_available(caller_module, root_fixture_name, if_name_exists=CHANGE,
177203
caller=param_fixtures)
178204

205+
if debug:
206+
print("Creating parametrized 'root' fixture %r returning %r" % (root_fixture_name, argvalues))
207+
179208
@fixture_plus(name=root_fixture_name, autouse=autouse, scope=scope, hook=hook, **kwargs)
180209
@pytest.mark.parametrize(argnames, argvalues, ids=ids)
181210
@with_signature("%s(%s)" % (root_fixture_name, argnames))
@@ -191,11 +220,16 @@ def _root_fixture(**_kwargs):
191220
# To fix late binding issue with `param_idx` we add an extra layer of scope: a factory function
192221
# See https://stackoverflow.com/questions/3431676/creating-functions-in-a-loop
193222
def _create_fixture(_param_idx):
223+
224+
if debug:
225+
print("Creating nonparametrized 'view' fixture %r returning %r[%s]" % (argname, root_fixture_name, _param_idx))
226+
194227
@fixture_plus(name=argname, scope=scope, autouse=autouse, hook=hook, **kwargs)
195228
@with_signature("%s(%s)" % (argname, root_fixture_name))
196229
def _param_fixture(**_kwargs):
197230
params = _kwargs.pop(root_fixture_name)
198231
return params[_param_idx]
232+
199233
return _param_fixture
200234

201235
# create it
@@ -406,6 +440,8 @@ def _decorate_fixture_plus(fixture_func,
406440

407441
# --common routine used below. Fills kwargs with the appropriate names and values from fixture_params
408442
def _map_arguments(*_args, **_kwargs):
443+
# todo better...
444+
from .fixture_parametrize_plus import handle_lazy_args
409445
request = _kwargs['request'] if func_needs_request else _kwargs.pop('request')
410446

411447
# populate the parameters
@@ -416,12 +452,12 @@ def _map_arguments(*_args, **_kwargs):
416452
for p_names, fixture_param_value in zip(params_names_or_name_combinations, _params):
417453
if len(p_names) == 1:
418454
# a single parameter for that generated fixture (@pytest.mark.parametrize with a single name)
419-
_kwargs[p_names[0]] = fixture_param_value
455+
_kwargs[p_names[0]] = handle_lazy_args(fixture_param_value)
420456
else:
421457
# several parameters for that generated fixture (@pytest.mark.parametrize with several names)
422458
# unpack all of them and inject them in the kwargs
423459
for old_p_name, old_p_value in zip(p_names, fixture_param_value):
424-
_kwargs[old_p_name] = old_p_value
460+
_kwargs[old_p_name] = handle_lazy_args(old_p_value)
425461

426462
return _args, _kwargs
427463

0 commit comments

Comments
 (0)