Skip to content
Open
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 changelog/14287.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Dynamic fixture dependencies via :func:`request.getfixturevalue() <pytest.FixtureRequest.getfixturevalue>` are now shown in ``--setuponly`` output.
9 changes: 7 additions & 2 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,9 +377,12 @@ def __init__(
# collection. Dynamically requested fixtures (using
# `request.getfixturevalue("foo")`) are added dynamically.
self._arg2fixturedefs: Final = arg2fixturedefs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewing this inspired me to do a cleanup here: #14290

# The evaluated argnames so far, mapping to the FixtureDef they resolved
# to.
# The argnames evaluated in the current test item, mapping to the FixtureDef
# they resolved to.
Comment on lines +380 to +381
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"in the current test item" is a good addition, but let's keep the "so far" to make it clear it's a mutable dict that is filled as fixtures are evaluated.

Suggested change
# The argnames evaluated in the current test item, mapping to the FixtureDef
# they resolved to.
# The argnames evaluated in the current test item so far, mapping to the FixtureDef
# they resolved to. Shared by the TopRequest and all of its SubRequests.

self._fixture_defs: Final = fixture_defs
# FixtureDefs requested through this specific `request` object.
# Allows tracking dependencies on fixtures.
self._own_fixture_defs: Final[dict[str, FixtureDef[object]]] = {}
# Notes on the type of `param`:
# -`request.param` is only defined in parametrized fixtures, and will raise
# AttributeError otherwise. Python typing has no notion of "undefined", so
Expand Down Expand Up @@ -555,6 +558,7 @@ def _get_active_fixturedef(self, argname: str) -> FixtureDef[object]:
fixturedef = self._fixture_defs.get(argname)
if fixturedef is not None:
self._check_scope(fixturedef, fixturedef._scope)
self._own_fixture_defs[argname] = fixturedef
return fixturedef

# Find the appropriate fixturedef.
Expand Down Expand Up @@ -616,6 +620,7 @@ def _get_active_fixturedef(self, argname: str) -> FixtureDef[object]:
self, scope, param, param_index, fixturedef, _ispytest=True
)

self._own_fixture_defs[argname] = fixturedef
# Make sure the fixture value is cached, running it if it isn't
fixturedef.execute(request=subrequest)

Expand Down
15 changes: 8 additions & 7 deletions src/_pytest/setuponly.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def pytest_fixture_setup(
else:
param = request.param
fixturedef.cached_param = param # type: ignore[attr-defined]
_show_fixture_action(fixturedef, request.config, "SETUP")
_show_fixture_action(fixturedef, request, "SETUP")


def pytest_fixture_post_finalizer(
Expand All @@ -56,14 +56,15 @@ def pytest_fixture_post_finalizer(
if fixturedef.cached_result is not None:
config = request.config
if config.option.setupshow:
_show_fixture_action(fixturedef, request.config, "TEARDOWN")
_show_fixture_action(fixturedef, request, "TEARDOWN")
if hasattr(fixturedef, "cached_param"):
del fixturedef.cached_param


def _show_fixture_action(
fixturedef: FixtureDef[object], config: Config, msg: str
fixturedef: FixtureDef[object], request: SubRequest, msg: str
) -> None:
config = request.config
capman = config.pluginmanager.getplugin("capturemanager")
if capman:
capman.suspend_global_capture()
Expand All @@ -77,14 +78,14 @@ def _show_fixture_action(
scopename = fixturedef.scope[0].upper()
tw.write(f"{msg:<8} {scopename} {fixturedef.argname}")

if hasattr(fixturedef, "cached_param"):
tw.write(f"[{saferepr(fixturedef.cached_param, maxsize=42)}]")

if msg == "SETUP":
deps = sorted(arg for arg in fixturedef.argnames if arg != "request")
deps = sorted(request._own_fixture_defs.keys())
if deps:
tw.write(" (fixtures used: {})".format(", ".join(deps)))

if hasattr(fixturedef, "cached_param"):
tw.write(f"[{saferepr(fixturedef.cached_param, maxsize=42)}]")

tw.flush()

if capman:
Expand Down
39 changes: 39 additions & 0 deletions testing/test_setuponly.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,40 @@ def test_arg1(arg_other):
)


def test_show_fixtures_with_parameters_and_deps(pytester: Pytester) -> None:
p = pytester.makepyfile(
"""
import pytest

@pytest.fixture
def static_dep():
pass

@pytest.fixture
def dynamic_dep():
pass

@pytest.fixture
def param_fixture(static_dep, request):
request.getfixturevalue('dynamic_dep')

@pytest.mark.parametrize('param_fixture', [1], indirect=True)
def test_indirect(param_fixture):
pass
"""
)

result = pytester.runpytest("--setup-only", p)
assert result.ret == 0

result.stdout.fnmatch_lines(
[
" SETUP F param_fixture[1] (fixtures used: dynamic_dep, static_dep)",
" TEARDOWN F param_fixture[1]",
]
)


def test_show_fixtures_with_parameter_ids(pytester: Pytester, mode) -> None:
pytester.makeconftest(
'''
Expand Down Expand Up @@ -215,12 +249,15 @@ def test_dynamic_fixture_request(pytester: Pytester) -> None:
p = pytester.makepyfile(
"""
import pytest

@pytest.fixture()
def dynamically_requested_fixture():
pass

@pytest.fixture()
def dependent_fixture(request):
request.getfixturevalue('dynamically_requested_fixture')

def test_dyn(dependent_fixture):
pass
"""
Expand All @@ -232,6 +269,8 @@ def test_dyn(dependent_fixture):
result.stdout.fnmatch_lines(
[
"*SETUP F dynamically_requested_fixture",
"*SETUP F dependent_fixture (fixtures used: dynamically_requested_fixture)",
"*TEARDOWN F dependent_fixture",
"*TEARDOWN F dynamically_requested_fixture",
]
)
Expand Down
Loading