Skip to content

Commit eff72f7

Browse files
WarrenTheRabbitbluetech
authored andcommitted
fixtures: exclude direct-param fixtures from --fixtures-per-test output
Fix #11295 by excluding from the --fixtures-per-test output any 'pseudo fixture' that results from directy parametrizing a test e.g. with ``@pytest.mark.parametrize``. The justification for removing these fixtures from the report is that a) They are unintuitive. Their appearance in the fixtures-per-test report confuses new users because the fixtures created via ``@pytest.mark.parametrize`` do not confrom to the expectations established in the documentation; namely, that fixtures are - richly reusable - provide setup/teardown features - created via the ``@pytest.fixture` decorator b) They are an internal implementation detail. It is not the explicit goal of the direct parametrization mark to create a fixture; instead, pytest's internals leverages the fixture system to achieve the explicit goal: a succinct batch execution syntax. Consequently, exposing the fixtures that implement the batch execution behaviour reveal more about pytest's internals than they do about the user's own design choices and test dependencies.
1 parent 6d03e6f commit eff72f7

4 files changed

Lines changed: 111 additions & 11 deletions

File tree

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,7 @@ Vlad Radziuk
495495
Vladyslav Rachek
496496
Volodymyr Kochetkov
497497
Volodymyr Piskun
498+
Warren Markham
498499
Wei Lin
499500
Wil Cooley
500501
Will Riley

changelog/11295.improvement.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improved output of ``--fixtures-per-test`` by excluding internal-implementation fixtures generated by ``@pytest.mark.parametrize`` and similar.

src/_pytest/fixtures.py

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1977,6 +1977,36 @@ def _pretty_fixture_path(invocation_dir: Path, func) -> str:
19771977
return bestrelpath(invocation_dir, loc)
19781978

19791979

1980+
def _get_fixtures_per_test(test: nodes.Item) -> Iterator[FixtureDef[object]]:
1981+
"""Returns all fixtures used by the test item except for those created by
1982+
direct parametrization and those requested dynamically with
1983+
``request.getfixturevalue``.
1984+
1985+
The justification for excluding fixtures created by direct parametrization
1986+
is that for users, they are internal implementation detail.
1987+
1988+
Dynamically requested fixtures are excluded because they are not known
1989+
statically.
1990+
"""
1991+
from _pytest.python import DirectParamFixtureDef
1992+
1993+
# Custom Items may not have _fixtureinfo attribute.
1994+
fixture_info: FuncFixtureInfo | None = getattr(test, "_fixtureinfo", None)
1995+
if fixture_info is None:
1996+
return # pragma: no cover
1997+
1998+
# dict key not used in loop but needed for sorting.
1999+
for argname, fixturedefs in sorted(fixture_info.name2fixturedefs.items()):
2000+
if not fixturedefs:
2001+
# Not supposed to be empty, but for safety.
2002+
continue # pragma: no cover
2003+
# Last item is expected to be the one directly used by the test item.
2004+
fixturedef = fixturedefs[-1]
2005+
if isinstance(fixturedef, DirectParamFixtureDef):
2006+
continue
2007+
yield fixturedef
2008+
2009+
19802010
def _show_fixtures_per_test(config: Config, session: Session) -> None:
19812011
import _pytest.config
19822012

@@ -2009,22 +2039,18 @@ def write_fixture(fixture_def: FixtureDef[object]) -> None:
20092039
tw.line(" no docstring available", red=True)
20102040

20112041
def write_item(item: nodes.Item) -> None:
2012-
# Not all items have _fixtureinfo attribute.
2013-
info: FuncFixtureInfo | None = getattr(item, "_fixtureinfo", None)
2014-
if info is None or not info.name2fixturedefs:
2042+
fixturedefs = list(_get_fixtures_per_test(item))
2043+
if not fixturedefs:
20152044
# This test item does not use any fixtures.
20162045
return
2046+
20172047
tw.line()
20182048
tw.sep("-", f"fixtures used by {item.name}")
20192049
# TODO: Fix this type ignore.
20202050
tw.sep("-", f"({get_best_relpath(item.function)})") # type: ignore[attr-defined]
2021-
# dict key not used in loop but needed for sorting.
2022-
for _, fixturedefs in sorted(info.name2fixturedefs.items()):
2023-
assert fixturedefs is not None
2024-
if not fixturedefs:
2025-
continue
2026-
# Last item is expected to be the one used by the test item.
2027-
write_fixture(fixturedefs[-1])
2051+
2052+
for fixturedef in fixturedefs:
2053+
write_fixture(fixturedef)
20282054

20292055
for session_item in session.items:
20302056
write_item(session_item)

testing/python/show_fixtures_per_test.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from _pytest.pytester import Pytester
44

55

6-
def test_no_items_should_not_show_output(pytester: Pytester) -> None:
6+
def test_should_show_no_output_when_zero_items(pytester: Pytester) -> None:
77
result = pytester.runpytest("--fixtures-per-test")
88
result.stdout.no_fnmatch_line("*fixtures used by*")
99
assert result.ret == 0
@@ -254,3 +254,75 @@ def test_arg1(arg1):
254254
" Docstring content that extends into a third paragraph.",
255255
]
256256
)
257+
258+
259+
def test_should_not_show_direct_param_fixtures(pytester: Pytester) -> None:
260+
"""A direct-param fixture is a helper fixture created as an implementation
261+
detail of direct parametrization.
262+
263+
These fixtures should not be included in the output because they don't
264+
satisfy user expectations for how fixtures are created and used (#11295).
265+
"""
266+
pytester.makepyfile(
267+
"""
268+
import pytest
269+
270+
@pytest.mark.parametrize("x", [1])
271+
def test_pseudo_fixture(x):
272+
pass
273+
"""
274+
)
275+
result = pytester.runpytest("--fixtures-per-test")
276+
result.stdout.no_fnmatch_line("*fixtures used by*")
277+
assert result.ret == 0
278+
279+
280+
def test_should_show_parametrized_fixtures_used_by_test(pytester: Pytester) -> None:
281+
"""A fixture with parameters should be included if it was created using
282+
the @pytest.fixture decorator, including those that are indirectly
283+
parametrized."""
284+
pytester.makepyfile(
285+
'''
286+
import pytest
287+
288+
@pytest.fixture(params=['a', 'b'])
289+
def directly(request):
290+
"""parametrized fixture"""
291+
return request.param
292+
293+
@pytest.fixture
294+
def indirectly(request):
295+
"""indirectly parametrized fixture"""
296+
return request.param
297+
298+
def test_directly_parametrized_fixture(directly):
299+
pass
300+
301+
@pytest.mark.parametrize("indirectly", ["a", "b"], indirect=True)
302+
def test_indirectly_parametrized_fixture(indirectly):
303+
pass
304+
'''
305+
)
306+
result = pytester.runpytest("--fixtures-per-test")
307+
assert result.ret == 0
308+
309+
result.stdout.fnmatch_lines(
310+
[
311+
"*fixtures used by test_directly_parametrized_fixture*",
312+
"*(test_should_show_parametrized_fixtures_used_by_test.py:14)*",
313+
"directly -- test_should_show_parametrized_fixtures_used_by_test.py:4",
314+
" parametrized fixture",
315+
"*fixtures used by test_directly_parametrized_fixture*",
316+
"*(test_should_show_parametrized_fixtures_used_by_test.py:14)*",
317+
"directly -- test_should_show_parametrized_fixtures_used_by_test.py:4",
318+
" parametrized fixture",
319+
"*fixtures used by test_indirectly_parametrized_fixture*",
320+
"*(test_should_show_parametrized_fixtures_used_by_test.py:17)*",
321+
"indirectly -- test_should_show_parametrized_fixtures_used_by_test.py:9",
322+
" indirectly parametrized fixture",
323+
"*fixtures used by test_indirectly_parametrized_fixture*",
324+
"*(test_should_show_parametrized_fixtures_used_by_test.py:17)*",
325+
"indirectly -- test_should_show_parametrized_fixtures_used_by_test.py:9",
326+
" indirectly parametrized fixture",
327+
]
328+
)

0 commit comments

Comments
 (0)