Skip to content

Commit 6b73294

Browse files
author
Sylvain MARIE
committed
Fixed issue where fixtures get called with NOT_USED as a parameter when using a fixture_union. This issue is actually only fixed in @pytest_fixture_plus, if you use @pytest.fixture you have to handle it manually. Fixes #37
1 parent bdab6f2 commit 6b73294

File tree

6 files changed

+120
-34
lines changed

6 files changed

+120
-34
lines changed

docs/index.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -240,14 +240,13 @@ The topic has been largely discussed in [pytest-dev](https://github.com/pytest-d
240240
`fixture_union` is an implementation of this proposal.
241241

242242
```python
243-
import pytest
244-
from pytest_cases import fixture_union
243+
from pytest_cases import pytest_fixture_plus, fixture_union
245244

246-
@pytest.fixture
245+
@pytest_fixture_plus
247246
def first():
248247
return 'hello'
249248

250-
@pytest.fixture(params=['a', 'b'])
249+
@pytest_fixture_plus(params=['a', 'b'])
251250
def second(request):
252251
return request.param
253252

@@ -271,6 +270,7 @@ PASSED
271270

272271
This feature has been tested in very complex cases (several union fixtures, fixtures that are not selected by a given union but that is requested by the test function, etc.). But if you find some strange behaviour don't hesitate to report it in the [issues](https://github.com/smarie/python-pytest-cases/issues) page !
273272

273+
**IMPORTANT** if you do not use `@pytest_fixture_plus` but only `@pytest.fixture`, then you will see that your fixtures are called even when they are not used, with a parameter `NOT_USED`. This symbol is automatically ignored if you use `@pytest_fixture_plus`, otherwise you have to handle it.
274274

275275
!!! note "fixture unions vs. cases"
276276
If you're familiar with `pytest-cases` already, you might note `@cases_data` is not so different than a fixture union: we do a union of all case functions. If one day union fixtures are directly supported by `pytest`, we will probably refactor this lib to align all the concepts.

pytest_cases/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from pytest_cases.case_funcs import case_name, test_target, case_tags, cases_generator
22

33
from pytest_cases.main_fixtures import cases_fixture, pytest_fixture_plus, param_fixtures, param_fixture, \
4-
fixture_union # pytest_parametrize_plus
4+
fixture_union, NOT_USED # pytest_parametrize_plus
55

66
from pytest_cases.main_params import cases_data, CaseDataGetter, unfold_expected_err, get_all_cases, THIS_MODULE, \
77
get_pytest_parametrize_args
@@ -14,7 +14,7 @@
1414
'case_name', 'test_target', 'case_tags', 'cases_generator',
1515
# --main_fixtures
1616
'cases_fixture', 'pytest_fixture_plus', 'param_fixtures', 'param_fixture', # 'pytest_parametrize_plus',
17-
'fixture_union',
17+
'fixture_union', 'NOT_USED',
1818
# --main params
1919
'cases_data', 'CaseDataGetter', 'THIS_MODULE', 'unfold_expected_err', 'get_all_cases',
2020
'get_pytest_parametrize_args',

pytest_cases/main_fixtures.py

Lines changed: 81 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from warnings import warn
88

99
from decopatch import function_decorator, DECORATED
10-
from makefun import with_signature, add_signature_parameters, remove_signature_parameters
10+
from makefun import with_signature, add_signature_parameters, remove_signature_parameters, wraps
1111

1212
import pytest
1313

@@ -274,9 +274,12 @@ def pytest_fixture_plus(scope="function",
274274
# (1) Collect all @pytest.mark.parametrize markers (including those created by usage of @cases_data)
275275
parametrizer_marks = get_pytest_parametrize_marks(fixture_func)
276276
if len(parametrizer_marks) < 1:
277-
# -- no parametrization: shortcut
278-
fix_creator = pytest.fixture if not isgeneratorfunction(fixture_func) else yield_fixture
279-
return fix_creator(scope=scope, autouse=autouse, **kwargs)(fixture_func)
277+
return _create_fixture_without_marks(fixture_func, scope, autouse, kwargs)
278+
else:
279+
if 'params' in kwargs:
280+
raise ValueError(
281+
"With `pytest_fixture_plus` you cannot mix usage of the keyword argument `params` and of "
282+
"the pytest.mark.parametrize marks")
280283

281284
# (2) create the huge "param" containing all params combined
282285
# --loop (use the same order to get it right)
@@ -355,14 +358,12 @@ def pytest_fixture_plus(scope="function",
355358
func_needs_request = 'request' in old_sig.parameters
356359
if not func_needs_request:
357360
new_sig = add_signature_parameters(new_sig, first=Parameter('request', kind=Parameter.POSITIONAL_OR_KEYWORD))
358-
else:
359-
new_sig = new_sig
360361

361362
# --common routine used below. Fills kwargs with the appropriate names and values from fixture_params
362363
def _get_arguments(*args, **kwargs):
363-
# kwargs contains request
364364
request = kwargs['request'] if func_needs_request else kwargs.pop('request')
365-
365+
if request.param is NOT_USED:
366+
return NOT_USED
366367
# populate the parameters
367368
if len(params_names_or_name_combinations) == 1:
368369
_params = [request.param] # remove the simplification
@@ -383,28 +384,93 @@ def _get_arguments(*args, **kwargs):
383384
# --Finally create the fixture function, a wrapper of user-provided fixture with the new signature
384385
if not isgeneratorfunction(fixture_func):
385386
# normal function with return statement
386-
@with_signature(new_sig)
387+
@wraps(fixture_func, new_sig=new_sig)
387388
def wrapped_fixture_func(*args, **kwargs):
388-
args, kwargs = _get_arguments(*args, **kwargs)
389-
return fixture_func(*args, **kwargs)
389+
out = _get_arguments(*args, **kwargs)
390+
if out is not NOT_USED:
391+
args, kwargs = out
392+
return fixture_func(*args, **kwargs)
390393

391394
# transform the created wrapper into a fixture
392395
fixture_decorator = pytest.fixture(scope=scope, params=final_values, autouse=autouse, ids=final_ids, **kwargs)
393396
return fixture_decorator(wrapped_fixture_func)
394397

395398
else:
396399
# generator function (with a yield statement)
397-
@with_signature(new_sig)
400+
@wraps(fixture_func, new_sig=new_sig)
398401
def wrapped_fixture_func(*args, **kwargs):
399-
args, kwargs = _get_arguments(*args, **kwargs)
400-
for res in fixture_func(*args, **kwargs):
401-
yield res
402+
out = _get_arguments(*args, **kwargs)
403+
if out is not NOT_USED:
404+
args, kwargs = out
405+
for res in fixture_func(*args, **kwargs):
406+
yield res
402407

403408
# transform the created wrapper into a fixture
404409
fixture_decorator = yield_fixture(scope=scope, params=final_values, autouse=autouse, ids=final_ids, **kwargs)
405410
return fixture_decorator(wrapped_fixture_func)
406411

407412

413+
def _create_fixture_without_marks(fixture_func, scope, autouse, kwargs):
414+
"""
415+
creates a fixture for decorated fixture function `fixture_func`.
416+
417+
:param fixture_func:
418+
:param scope:
419+
:param autouse:
420+
:param kwargs:
421+
:return:
422+
"""
423+
if 'params' not in kwargs:
424+
# -- no parametrization: shortcut
425+
fix_creator = pytest.fixture if not isgeneratorfunction(fixture_func) else yield_fixture
426+
return fix_creator(scope=scope, autouse=autouse, **kwargs)(fixture_func)
427+
else:
428+
# --create a wrapper where we will be able to auto-detect
429+
# TODO we could put this in a dedicated wrapper 'ignore_unsused'..
430+
431+
old_sig = signature(fixture_func)
432+
# add request if needed
433+
func_needs_request = 'request' in old_sig.parameters
434+
if not func_needs_request:
435+
new_sig = add_signature_parameters(old_sig,
436+
first=Parameter('request', kind=Parameter.POSITIONAL_OR_KEYWORD))
437+
else:
438+
new_sig = old_sig
439+
if not isgeneratorfunction(fixture_func):
440+
# normal function with return statement
441+
@wraps(fixture_func, new_sig=new_sig)
442+
def wrapped_fixture_func(*args, **kwargs):
443+
request = kwargs['request'] if func_needs_request else kwargs.pop('request')
444+
if request.param is not NOT_USED:
445+
return fixture_func(*args, **kwargs)
446+
447+
# transform the created wrapper into a fixture
448+
fixture_decorator = pytest.fixture(scope=scope, autouse=autouse, **kwargs)
449+
return fixture_decorator(wrapped_fixture_func)
450+
451+
else:
452+
# generator function (with a yield statement)
453+
@wraps(fixture_func, new_sig=new_sig)
454+
def wrapped_fixture_func(*args, **kwargs):
455+
request = kwargs['request'] if func_needs_request else kwargs.pop('request')
456+
if request.param is not NOT_USED:
457+
for res in fixture_func(*args, **kwargs):
458+
yield res
459+
460+
# transform the created wrapper into a fixture
461+
fixture_decorator = yield_fixture(scope=scope, autouse=autouse, **kwargs)
462+
return fixture_decorator(wrapped_fixture_func)
463+
464+
465+
class _NotUsed:
466+
def __repr__(self):
467+
return "<not_used>"
468+
469+
470+
NOT_USED = _NotUsed()
471+
"""Object representing a fixture value when the fixture is not used"""
472+
473+
408474
class UnionFixtureConfig:
409475
def __init__(self, fixtures):
410476
self.fixtures = fixtures

pytest_cases/plugin.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import pytest
55

6-
from pytest_cases.main_fixtures import UnionFixtureConfig
6+
from pytest_cases.main_fixtures import UnionFixtureConfig, NOT_USED
77

88
try: # python 3.3+
99
from inspect import signature
@@ -473,15 +473,6 @@ def discard_last_id(self):
473473
callspec._idlist.pop(-1)
474474

475475

476-
class _NotUsed:
477-
def __repr__(self):
478-
return "<not_used>"
479-
480-
481-
NOT_USED = _NotUsed()
482-
"""Object representing a fixture value when the fixture is not used"""
483-
484-
485476
class FilteredLeafNode(LeafNode):
486477
"""
487478
Represents a set of calls for which at least one union fixture has been parametrized.

pytest_cases/tests/fixtures/test_fixtures_union_1simple.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1-
from pytest_cases import param_fixture, fixture_union
2-
1+
from pytest_cases import param_fixture, fixture_union, pytest_fixture_plus
32

43
a = param_fixture('a', ['x', 'y'])
5-
b = param_fixture('b', [1, 2])
4+
5+
6+
@pytest_fixture_plus(params=[1, 2])
7+
def b(request):
8+
# make sure that if this is called, then it is for a good reason
9+
assert request.param in [1, 2]
10+
return request.param
11+
12+
613
c = fixture_union('c', [a, b])
714

815

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import pytest
2+
3+
from pytest_cases import fixture_union, pytest_fixture_plus, NOT_USED
4+
5+
6+
@pytest_fixture_plus(params=[1, 2, 3])
7+
def lower(request):
8+
return "i" * request.param
9+
10+
11+
@pytest.fixture(params=[1, 2])
12+
def upper(request):
13+
# this fixture does not use pytest_fixture_plus so we have to explicitly discard the 'not used' cases
14+
if request.param is not NOT_USED:
15+
return "I" * request.param
16+
17+
18+
fixture_union('all', ['lower', 'upper'])
19+
20+
21+
def test_all(all):
22+
print(all)

0 commit comments

Comments
 (0)