Skip to content

Commit ec58ae5

Browse files
authored
Merge pull request #7736 from nicoddemus/extend-fixture-parametrize-1953
2 parents 389e302 + e36adba commit ec58ae5

File tree

3 files changed

+188
-26
lines changed

3 files changed

+188
-26
lines changed

changelog/1953.bugfix.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Fix error when overwriting a parametrized fixture, while also reusing the super fixture value.
2+
3+
.. code-block:: python
4+
5+
# conftest.py
6+
import pytest
7+
8+
9+
@pytest.fixture(params=[1, 2])
10+
def foo(request):
11+
return request.param
12+
13+
14+
# test_foo.py
15+
import pytest
16+
17+
18+
@pytest.fixture
19+
def foo(foo):
20+
return foo * 2

src/_pytest/fixtures.py

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
from _pytest.config import Config
4848
from _pytest.config.argparsing import Parser
4949
from _pytest.deprecated import FILLFUNCARGS
50+
from _pytest.mark import Mark
5051
from _pytest.mark import ParameterSet
5152
from _pytest.outcomes import fail
5253
from _pytest.outcomes import TEST_OUTCOME
@@ -1529,34 +1530,49 @@ def sort_by_scope(arg_name: str) -> int:
15291530
return initialnames, fixturenames_closure, arg2fixturedefs
15301531

15311532
def pytest_generate_tests(self, metafunc: "Metafunc") -> None:
1533+
"""Generate new tests based on parametrized fixtures used by the given metafunc"""
1534+
1535+
def get_parametrize_mark_argnames(mark: Mark) -> Sequence[str]:
1536+
args, _ = ParameterSet._parse_parametrize_args(*mark.args, **mark.kwargs)
1537+
return args
1538+
15321539
for argname in metafunc.fixturenames:
1533-
faclist = metafunc._arg2fixturedefs.get(argname)
1534-
if faclist:
1535-
fixturedef = faclist[-1]
1540+
# Get the FixtureDefs for the argname.
1541+
fixture_defs = metafunc._arg2fixturedefs.get(argname)
1542+
if not fixture_defs:
1543+
# Will raise FixtureLookupError at setup time if not parametrized somewhere
1544+
# else (e.g @pytest.mark.parametrize)
1545+
continue
1546+
1547+
# If the test itself parametrizes using this argname, give it
1548+
# precedence.
1549+
if any(
1550+
argname in get_parametrize_mark_argnames(mark)
1551+
for mark in metafunc.definition.iter_markers("parametrize")
1552+
):
1553+
continue
1554+
1555+
# In the common case we only look at the fixture def with the
1556+
# closest scope (last in the list). But if the fixture overrides
1557+
# another fixture, while requesting the super fixture, keep going
1558+
# in case the super fixture is parametrized (#1953).
1559+
for fixturedef in reversed(fixture_defs):
1560+
# Fixture is parametrized, apply it and stop.
15361561
if fixturedef.params is not None:
1537-
markers = list(metafunc.definition.iter_markers("parametrize"))
1538-
for parametrize_mark in markers:
1539-
if "argnames" in parametrize_mark.kwargs:
1540-
argnames = parametrize_mark.kwargs["argnames"]
1541-
else:
1542-
argnames = parametrize_mark.args[0]
1543-
1544-
if not isinstance(argnames, (tuple, list)):
1545-
argnames = [
1546-
x.strip() for x in argnames.split(",") if x.strip()
1547-
]
1548-
if argname in argnames:
1549-
break
1550-
else:
1551-
metafunc.parametrize(
1552-
argname,
1553-
fixturedef.params,
1554-
indirect=True,
1555-
scope=fixturedef.scope,
1556-
ids=fixturedef.ids,
1557-
)
1558-
else:
1559-
continue # Will raise FixtureLookupError at setup time.
1562+
metafunc.parametrize(
1563+
argname,
1564+
fixturedef.params,
1565+
indirect=True,
1566+
scope=fixturedef.scope,
1567+
ids=fixturedef.ids,
1568+
)
1569+
break
1570+
1571+
# Not requesting the overridden super fixture, stop.
1572+
if argname not in fixturedef.argnames:
1573+
break
1574+
1575+
# Try next super fixture, if any.
15601576

15611577
def pytest_collection_modifyitems(self, items: "List[nodes.Item]") -> None:
15621578
# Separate parametrized setups.

testing/python/fixtures.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,132 @@ def test_spam(spam):
396396
result = testdir.runpytest(testfile)
397397
result.stdout.fnmatch_lines(["*3 passed*"])
398398

399+
def test_override_fixture_reusing_super_fixture_parametrization(self, testdir):
400+
"""Override a fixture at a lower level, reusing the higher-level fixture that
401+
is parametrized (#1953).
402+
"""
403+
testdir.makeconftest(
404+
"""
405+
import pytest
406+
407+
@pytest.fixture(params=[1, 2])
408+
def foo(request):
409+
return request.param
410+
"""
411+
)
412+
testdir.makepyfile(
413+
"""
414+
import pytest
415+
416+
@pytest.fixture
417+
def foo(foo):
418+
return foo * 2
419+
420+
def test_spam(foo):
421+
assert foo in (2, 4)
422+
"""
423+
)
424+
result = testdir.runpytest()
425+
result.stdout.fnmatch_lines(["*2 passed*"])
426+
427+
def test_override_parametrize_fixture_and_indirect(self, testdir):
428+
"""Override a fixture at a lower level, reusing the higher-level fixture that
429+
is parametrized, while also using indirect parametrization.
430+
"""
431+
testdir.makeconftest(
432+
"""
433+
import pytest
434+
435+
@pytest.fixture(params=[1, 2])
436+
def foo(request):
437+
return request.param
438+
"""
439+
)
440+
testdir.makepyfile(
441+
"""
442+
import pytest
443+
444+
@pytest.fixture
445+
def foo(foo):
446+
return foo * 2
447+
448+
@pytest.fixture
449+
def bar(request):
450+
return request.param * 100
451+
452+
@pytest.mark.parametrize("bar", [42], indirect=True)
453+
def test_spam(bar, foo):
454+
assert bar == 4200
455+
assert foo in (2, 4)
456+
"""
457+
)
458+
result = testdir.runpytest()
459+
result.stdout.fnmatch_lines(["*2 passed*"])
460+
461+
def test_override_top_level_fixture_reusing_super_fixture_parametrization(
462+
self, testdir
463+
):
464+
"""Same as the above test, but with another level of overwriting."""
465+
testdir.makeconftest(
466+
"""
467+
import pytest
468+
469+
@pytest.fixture(params=['unused', 'unused'])
470+
def foo(request):
471+
return request.param
472+
"""
473+
)
474+
testdir.makepyfile(
475+
"""
476+
import pytest
477+
478+
@pytest.fixture(params=[1, 2])
479+
def foo(request):
480+
return request.param
481+
482+
class Test:
483+
484+
@pytest.fixture
485+
def foo(self, foo):
486+
return foo * 2
487+
488+
def test_spam(self, foo):
489+
assert foo in (2, 4)
490+
"""
491+
)
492+
result = testdir.runpytest()
493+
result.stdout.fnmatch_lines(["*2 passed*"])
494+
495+
def test_override_parametrized_fixture_with_new_parametrized_fixture(self, testdir):
496+
"""Overriding a parametrized fixture, while also parametrizing the new fixture and
497+
simultaneously requesting the overwritten fixture as parameter, yields the same value
498+
as ``request.param``.
499+
"""
500+
testdir.makeconftest(
501+
"""
502+
import pytest
503+
504+
@pytest.fixture(params=['ignored', 'ignored'])
505+
def foo(request):
506+
return request.param
507+
"""
508+
)
509+
testdir.makepyfile(
510+
"""
511+
import pytest
512+
513+
@pytest.fixture(params=[10, 20])
514+
def foo(foo, request):
515+
assert request.param == foo
516+
return foo * 2
517+
518+
def test_spam(foo):
519+
assert foo in (20, 40)
520+
"""
521+
)
522+
result = testdir.runpytest()
523+
result.stdout.fnmatch_lines(["*2 passed*"])
524+
399525
def test_autouse_fixture_plugin(self, testdir):
400526
# A fixture from a plugin has no baseid set, which screwed up
401527
# the autouse fixture handling.

0 commit comments

Comments
 (0)