Skip to content

Commit fa643de

Browse files
authored
Merge pull request #12129 from WarrenTheRabbit/exclude-psuedo-fixtures-from-fixtures-per-test-output
improve(fixtures-per-test): exclude pseudo fixtures from output #11295
2 parents c26c145 + eff72f7 commit fa643de

6 files changed

Lines changed: 153 additions & 33 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)

src/_pytest/python.py

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
from _pytest.fixtures import _resolve_args_directness
5656
from _pytest.fixtures import FixtureDef
5757
from _pytest.fixtures import FixtureRequest
58+
from _pytest.fixtures import FixtureValue
5859
from _pytest.fixtures import FuncFixtureInfo
5960
from _pytest.fixtures import get_scope_node
6061
from _pytest.main import Session
@@ -1095,8 +1096,7 @@ class CallSpec2:
10951096
and stored in item.callspec.
10961097
"""
10971098

1098-
# arg name -> arg value which will be passed to a fixture or pseudo-fixture
1099-
# of the same name. (indirect or direct parametrization respectively)
1099+
# arg name -> arg value which will be passed to a fixture of the same name.
11001100
params: dict[str, object] = dataclasses.field(default_factory=dict)
11011101
# arg name -> arg index.
11021102
indices: dict[str, int] = dataclasses.field(default_factory=dict)
@@ -1153,8 +1153,30 @@ def get_direct_param_fixture_func(request: FixtureRequest) -> Any:
11531153
return request.param
11541154

11551155

1156-
# Used for storing pseudo fixturedefs for direct parametrization.
1157-
name2pseudofixturedef_key = StashKey[dict[str, FixtureDef[Any]]]()
1156+
class DirectParamFixtureDef(FixtureDef[FixtureValue]):
1157+
"""A custom FixtureDef for direct parametrization fixtures.
1158+
1159+
Each parameter in direct parametrization is desugared to a parametrized
1160+
fixture which returns the direct parameterization value as its param.
1161+
We use this custom type as a "marker" for this type of FixtureDef, but
1162+
usually behaves like any other FixtureDef.
1163+
"""
1164+
1165+
def __init__(self, *, config: Config, argname: str, scope: Scope) -> None:
1166+
super().__init__(
1167+
config=config,
1168+
baseid="",
1169+
argname=argname,
1170+
func=get_direct_param_fixture_func,
1171+
scope=scope,
1172+
params=None,
1173+
ids=None,
1174+
_ispytest=True,
1175+
)
1176+
1177+
1178+
# Used for storing fixturedefs for direct parametrization.
1179+
name2directparamfixturedef_key = StashKey[dict[str, DirectParamFixtureDef[object]]]()
11581180

11591181

11601182
@final
@@ -1333,14 +1355,14 @@ def parametrize(
13331355
self._params_directness.update(arg_directness)
13341356

13351357
# Add direct parametrizations as fixturedefs to arg2fixturedefs by
1336-
# registering artificial "pseudo" FixtureDef's such that later at test
1358+
# registering artificial DirectParamFixtureDef's such that later at test
13371359
# setup time we can rely on FixtureDefs to exist for all argnames.
13381360
node = None
1339-
# For scopes higher than function, a "pseudo" FixtureDef might have
1361+
# For scopes higher than function, a DirectParamFixtureDef might have
13401362
# already been created for the scope. We thus store and cache the
1341-
# FixtureDef on the node related to the scope.
1363+
# DirectParamFixtureDef on the node related to the scope.
13421364
if scope_ is Scope.Function:
1343-
name2pseudofixturedef = None
1365+
name2directparamfixturedef = None
13441366
else:
13451367
collector = self.definition.parent
13461368
assert collector is not None
@@ -1357,28 +1379,26 @@ def parametrize(
13571379
node = collector.session
13581380
else:
13591381
assert False, f"Unhandled missing scope: {scope}"
1360-
default: dict[str, FixtureDef[Any]] = {}
1361-
name2pseudofixturedef = node.stash.setdefault(
1362-
name2pseudofixturedef_key, default
1382+
default: dict[str, DirectParamFixtureDef[object]] = {}
1383+
name2directparamfixturedef = node.stash.setdefault(
1384+
name2directparamfixturedef_key, default
13631385
)
13641386
for argname in argnames:
13651387
if arg_directness[argname] == "indirect":
13661388
continue
1367-
if name2pseudofixturedef is not None and argname in name2pseudofixturedef:
1368-
fixturedef = name2pseudofixturedef[argname]
1389+
if (
1390+
name2directparamfixturedef is not None
1391+
and argname in name2directparamfixturedef
1392+
):
1393+
fixturedef = name2directparamfixturedef[argname]
13691394
else:
1370-
fixturedef = FixtureDef(
1395+
fixturedef = DirectParamFixtureDef(
13711396
config=self.config,
1372-
baseid="",
13731397
argname=argname,
1374-
func=get_direct_param_fixture_func,
13751398
scope=scope_,
1376-
params=None,
1377-
ids=None,
1378-
_ispytest=True,
13791399
)
1380-
if name2pseudofixturedef is not None:
1381-
name2pseudofixturedef[argname] = fixturedef
1400+
if name2directparamfixturedef is not None:
1401+
name2directparamfixturedef[argname] = fixturedef
13821402
self._arg2fixturedefs[argname] = [fixturedef]
13831403

13841404
# Create the new calls: if we are parametrize() multiple times (by applying the decorator

testing/python/metafunc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -801,7 +801,7 @@ def func(x, y):
801801
metafunc = self.Metafunc(func)
802802
metafunc.parametrize("x, y", [("a", "b")], indirect=["x"])
803803
assert metafunc._calls[0].params == dict(x="a", y="b")
804-
# Since `y` is a direct parameter, its pseudo-fixture would
804+
# Since `y` is a direct parameter, its DirectParamFixtureDef would
805805
# be registered.
806806
assert list(metafunc._arg2fixturedefs.keys()) == ["y"]
807807

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)