Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,7 @@ Vlad Radziuk
Vladyslav Rachek
Volodymyr Kochetkov
Volodymyr Piskun
Warren Markham
Wei Lin
Wil Cooley
Will Riley
Expand Down
1 change: 1 addition & 0 deletions changelog/11295.improvement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Improved output of ``--fixtures-per-test`` by excluding internal-implementation fixtures generated by ``@pytest.mark.parametrize`` and similar.
46 changes: 36 additions & 10 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -1977,6 +1977,36 @@ def _pretty_fixture_path(invocation_dir: Path, func) -> str:
return bestrelpath(invocation_dir, loc)


def _get_fixtures_per_test(test: nodes.Item) -> Iterator[FixtureDef[object]]:
"""Returns all fixtures used by the test item except for those created by
direct parametrization and those requested dynamically with
``request.getfixturevalue``.

The justification for excluding fixtures created by direct parametrization
is that for users, they are internal implementation detail.

Dynamically requested fixtures are excluded because they are not known
statically.
"""
from _pytest.python import DirectParamFixtureDef

# Custom Items may not have _fixtureinfo attribute.
fixture_info: FuncFixtureInfo | None = getattr(test, "_fixtureinfo", None)
if fixture_info is None:
return # pragma: no cover

# dict key not used in loop but needed for sorting.
for argname, fixturedefs in sorted(fixture_info.name2fixturedefs.items()):
if not fixturedefs:
# Not supposed to be empty, but for safety.
continue # pragma: no cover
# Last item is expected to be the one directly used by the test item.
fixturedef = fixturedefs[-1]
if isinstance(fixturedef, DirectParamFixtureDef):
continue
yield fixturedef


def _show_fixtures_per_test(config: Config, session: Session) -> None:
import _pytest.config

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

def write_item(item: nodes.Item) -> None:
# Not all items have _fixtureinfo attribute.
info: FuncFixtureInfo | None = getattr(item, "_fixtureinfo", None)
if info is None or not info.name2fixturedefs:
fixturedefs = list(_get_fixtures_per_test(item))
if not fixturedefs:
# This test item does not use any fixtures.
return

tw.line()
tw.sep("-", f"fixtures used by {item.name}")
# TODO: Fix this type ignore.
tw.sep("-", f"({get_best_relpath(item.function)})") # type: ignore[attr-defined]
# dict key not used in loop but needed for sorting.
for _, fixturedefs in sorted(info.name2fixturedefs.items()):
assert fixturedefs is not None
if not fixturedefs:
continue
# Last item is expected to be the one used by the test item.
write_fixture(fixturedefs[-1])

for fixturedef in fixturedefs:
write_fixture(fixturedef)

for session_item in session.items:
write_item(session_item)
Expand Down
62 changes: 41 additions & 21 deletions src/_pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
from _pytest.fixtures import _resolve_args_directness
from _pytest.fixtures import FixtureDef
from _pytest.fixtures import FixtureRequest
from _pytest.fixtures import FixtureValue
from _pytest.fixtures import FuncFixtureInfo
from _pytest.fixtures import get_scope_node
from _pytest.main import Session
Expand Down Expand Up @@ -1095,8 +1096,7 @@ class CallSpec2:
and stored in item.callspec.
"""

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


# Used for storing pseudo fixturedefs for direct parametrization.
name2pseudofixturedef_key = StashKey[dict[str, FixtureDef[Any]]]()
class DirectParamFixtureDef(FixtureDef[FixtureValue]):
"""A custom FixtureDef for direct parametrization fixtures.

Each parameter in direct parametrization is desugared to a parametrized
fixture which returns the direct parameterization value as its param.
We use this custom type as a "marker" for this type of FixtureDef, but
usually behaves like any other FixtureDef.
"""

def __init__(self, *, config: Config, argname: str, scope: Scope) -> None:
super().__init__(
config=config,
baseid="",
argname=argname,
func=get_direct_param_fixture_func,
scope=scope,
params=None,
ids=None,
_ispytest=True,
)


# Used for storing fixturedefs for direct parametrization.
name2directparamfixturedef_key = StashKey[dict[str, DirectParamFixtureDef[object]]]()


@final
Expand Down Expand Up @@ -1333,14 +1355,14 @@ def parametrize(
self._params_directness.update(arg_directness)

# Add direct parametrizations as fixturedefs to arg2fixturedefs by
# registering artificial "pseudo" FixtureDef's such that later at test
# registering artificial DirectParamFixtureDef's such that later at test
# setup time we can rely on FixtureDefs to exist for all argnames.
node = None
# For scopes higher than function, a "pseudo" FixtureDef might have
# For scopes higher than function, a DirectParamFixtureDef might have
# already been created for the scope. We thus store and cache the
# FixtureDef on the node related to the scope.
# DirectParamFixtureDef on the node related to the scope.
if scope_ is Scope.Function:
name2pseudofixturedef = None
name2directparamfixturedef = None
else:
collector = self.definition.parent
assert collector is not None
Expand All @@ -1357,28 +1379,26 @@ def parametrize(
node = collector.session
else:
assert False, f"Unhandled missing scope: {scope}"
default: dict[str, FixtureDef[Any]] = {}
name2pseudofixturedef = node.stash.setdefault(
name2pseudofixturedef_key, default
default: dict[str, DirectParamFixtureDef[object]] = {}
name2directparamfixturedef = node.stash.setdefault(
name2directparamfixturedef_key, default
)
for argname in argnames:
if arg_directness[argname] == "indirect":
continue
if name2pseudofixturedef is not None and argname in name2pseudofixturedef:
fixturedef = name2pseudofixturedef[argname]
if (
name2directparamfixturedef is not None
and argname in name2directparamfixturedef
):
fixturedef = name2directparamfixturedef[argname]
else:
fixturedef = FixtureDef(
fixturedef = DirectParamFixtureDef(
config=self.config,
baseid="",
argname=argname,
func=get_direct_param_fixture_func,
scope=scope_,
params=None,
ids=None,
_ispytest=True,
)
if name2pseudofixturedef is not None:
name2pseudofixturedef[argname] = fixturedef
if name2directparamfixturedef is not None:
name2directparamfixturedef[argname] = fixturedef
self._arg2fixturedefs[argname] = [fixturedef]

# Create the new calls: if we are parametrize() multiple times (by applying the decorator
Expand Down
2 changes: 1 addition & 1 deletion testing/python/metafunc.py
Original file line number Diff line number Diff line change
Expand Up @@ -801,7 +801,7 @@ def func(x, y):
metafunc = self.Metafunc(func)
metafunc.parametrize("x, y", [("a", "b")], indirect=["x"])
assert metafunc._calls[0].params == dict(x="a", y="b")
# Since `y` is a direct parameter, its pseudo-fixture would
# Since `y` is a direct parameter, its DirectParamFixtureDef would
# be registered.
assert list(metafunc._arg2fixturedefs.keys()) == ["y"]

Expand Down
74 changes: 73 additions & 1 deletion testing/python/show_fixtures_per_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from _pytest.pytester import Pytester


def test_no_items_should_not_show_output(pytester: Pytester) -> None:
def test_should_show_no_output_when_zero_items(pytester: Pytester) -> None:
result = pytester.runpytest("--fixtures-per-test")
result.stdout.no_fnmatch_line("*fixtures used by*")
assert result.ret == 0
Expand Down Expand Up @@ -254,3 +254,75 @@ def test_arg1(arg1):
" Docstring content that extends into a third paragraph.",
]
)


def test_should_not_show_direct_param_fixtures(pytester: Pytester) -> None:
"""A direct-param fixture is a helper fixture created as an implementation
detail of direct parametrization.

These fixtures should not be included in the output because they don't
satisfy user expectations for how fixtures are created and used (#11295).
"""
pytester.makepyfile(
"""
import pytest

@pytest.mark.parametrize("x", [1])
def test_pseudo_fixture(x):
pass
"""
)
result = pytester.runpytest("--fixtures-per-test")
result.stdout.no_fnmatch_line("*fixtures used by*")
assert result.ret == 0


def test_should_show_parametrized_fixtures_used_by_test(pytester: Pytester) -> None:
"""A fixture with parameters should be included if it was created using
the @pytest.fixture decorator, including those that are indirectly
parametrized."""
pytester.makepyfile(
'''
import pytest

@pytest.fixture(params=['a', 'b'])
def directly(request):
"""parametrized fixture"""
return request.param

@pytest.fixture
def indirectly(request):
"""indirectly parametrized fixture"""
return request.param

def test_directly_parametrized_fixture(directly):
pass

@pytest.mark.parametrize("indirectly", ["a", "b"], indirect=True)
def test_indirectly_parametrized_fixture(indirectly):
pass
'''
)
result = pytester.runpytest("--fixtures-per-test")
assert result.ret == 0

result.stdout.fnmatch_lines(
[
"*fixtures used by test_directly_parametrized_fixture*",
"*(test_should_show_parametrized_fixtures_used_by_test.py:14)*",
"directly -- test_should_show_parametrized_fixtures_used_by_test.py:4",
" parametrized fixture",
"*fixtures used by test_directly_parametrized_fixture*",
"*(test_should_show_parametrized_fixtures_used_by_test.py:14)*",
"directly -- test_should_show_parametrized_fixtures_used_by_test.py:4",
" parametrized fixture",
"*fixtures used by test_indirectly_parametrized_fixture*",
"*(test_should_show_parametrized_fixtures_used_by_test.py:17)*",
"indirectly -- test_should_show_parametrized_fixtures_used_by_test.py:9",
" indirectly parametrized fixture",
"*fixtures used by test_indirectly_parametrized_fixture*",
"*(test_should_show_parametrized_fixtures_used_by_test.py:17)*",
"indirectly -- test_should_show_parametrized_fixtures_used_by_test.py:9",
" indirectly parametrized fixture",
]
)