Skip to content

Commit 12558b5

Browse files
authored
Merge pull request #14295 from bluetech/deprecate-getfixturevalue-teardown
fixtures: deprecate calling request.getfixturevalue() during teardown
2 parents 291b90a + e96f81f commit 12558b5

File tree

6 files changed

+176
-12
lines changed

6 files changed

+176
-12
lines changed

changelog/12882.deprecation.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Calling :meth:`request.getfixturevalue() <pytest.FixtureRequest.getfixturevalue>` during teardown to request a fixture that was not already requested is now deprecated and will become an error in pytest 10.
2+
3+
See :ref:`dynamic-fixture-request-during-teardown` for details.

doc/en/deprecations.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,29 @@ Below is a complete list of all pytest features which are considered deprecated.
1515
:class:`~pytest.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.
1616

1717

18+
.. _dynamic-fixture-request-during-teardown:
19+
20+
``request.getfixturevalue()`` during fixture teardown
21+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
22+
23+
.. deprecated:: 9.1
24+
25+
Calling :meth:`request.getfixturevalue() <pytest.FixtureRequest.getfixturevalue>`
26+
during teardown to request a fixture that was not already requested is deprecated.
27+
28+
This pattern is brittle because teardown runs after pytest has started unwinding active scopes.
29+
Depending on the requested fixture's scope and the current teardown order, the lookup may appear
30+
to work, or it may fail.
31+
32+
In pytest 10, first-time fixture requests made during teardown will become an error.
33+
If teardown logic needs another fixture, request it before teardown begins, either by
34+
declaring it in the fixture signature or by calling ``request.getfixturevalue()`` before
35+
the fixture yields.
36+
37+
Fixtures that were already requested before teardown started are unaffected and may still
38+
be retrieved while they remain active, though this is discouraged.
39+
40+
1841
.. _config-inicfg:
1942

2043
``config.inicfg``

src/_pytest/deprecated.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,14 @@
6767
"See https://docs.pytest.org/en/stable/deprecations.html#config-inicfg"
6868
)
6969

70+
FIXTURE_GETFIXTUREVALUE_DURING_TEARDOWN = UnformattedWarning(
71+
PytestRemovedIn10Warning,
72+
'Calling request.getfixturevalue("{argname}") during teardown is deprecated.\n'
73+
"Please request the fixture before teardown begins, either by declaring it in the fixture signature "
74+
"or by calling request.getfixturevalue() before the fixture yields.\n"
75+
"See https://docs.pytest.org/en/stable/deprecations.html#dynamic-fixture-request-during-teardown",
76+
)
77+
7078
# You want to make some `__init__` or function "private".
7179
#
7280
# def my_private_function(some, args):

src/_pytest/fixtures.py

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
from _pytest.config import ExitCode
5555
from _pytest.config.argparsing import Parser
5656
from _pytest.deprecated import check_ispytest
57+
from _pytest.deprecated import FIXTURE_GETFIXTUREVALUE_DURING_TEARDOWN
5758
from _pytest.deprecated import YIELD_FIXTURE
5859
from _pytest.main import Session
5960
from _pytest.mark import ParameterSet
@@ -506,6 +507,16 @@ def raiseerror(self, msg: str | None) -> NoReturn:
506507
"""
507508
raise FixtureLookupError(None, self, msg)
508509

510+
def _raise_teardown_lookup_error(self, argname: str) -> NoReturn:
511+
msg = (
512+
f'The fixture value for "{argname}" is not available during teardown '
513+
"because it was not previously requested.\n"
514+
"Only fixtures that were already active can be retrieved during teardown.\n"
515+
"Request the fixture before teardown begins by declaring it in the fixture "
516+
"signature or by calling request.getfixturevalue() before the fixture yields."
517+
)
518+
raise FixtureLookupError(argname, self, msg)
519+
509520
def getfixturevalue(self, argname: str) -> Any:
510521
"""Dynamically run a named fixture function.
511522
@@ -515,8 +526,12 @@ def getfixturevalue(self, argname: str) -> Any:
515526
or test function body.
516527
517528
This method can be used during the test setup phase or the test run
518-
phase, but during the test teardown phase a fixture's value may not
519-
be available.
529+
phase. Avoid using it during the teardown phase.
530+
531+
.. versionchanged:: 9.1
532+
Calling ``request.getfixturevalue()`` during teardown to request a
533+
fixture that was not already requested
534+
:ref:`is deprecated <dynamic-fixture-request-during-teardown>`.
520535
521536
:param argname:
522537
The fixture name.
@@ -613,6 +628,16 @@ def _get_active_fixturedef(self, argname: str) -> FixtureDef[object]:
613628
self, scope, param, param_index, fixturedef, _ispytest=True
614629
)
615630

631+
if not self.session._setupstate.is_node_active(self.node):
632+
# TODO(pytest10.1): Remove the `warn` and `if` and call
633+
# _raise_teardown_lookup_error unconditionally.
634+
warnings.warn(
635+
FIXTURE_GETFIXTUREVALUE_DURING_TEARDOWN.format(argname=argname),
636+
stacklevel=3,
637+
)
638+
if subrequest.node not in self.session._setupstate.stack:
639+
self._raise_teardown_lookup_error(argname)
640+
616641
# Make sure the fixture value is cached, running it if it isn't
617642
fixturedef.execute(request=subrequest)
618643

@@ -805,16 +830,7 @@ def formatrepr(self) -> FixtureLookupErrorRepr:
805830
stack = [self.request._pyfuncitem.obj]
806831
stack.extend(map(lambda x: x.func, self.fixturestack))
807832
msg = self.msg
808-
# This function currently makes an assumption that a non-None msg means we
809-
# have a non-empty `self.fixturestack`. This is currently true, but if
810-
# somebody at some point want to extend the use of FixtureLookupError to
811-
# new cases it might break.
812-
# Add the assert to make it clearer to developer that this will fail, otherwise
813-
# it crashes because `fspath` does not get set due to `stack` being empty.
814-
assert self.msg is None or self.fixturestack, (
815-
"formatrepr assumptions broken, rewrite it to handle it"
816-
)
817-
if msg is not None:
833+
if msg is not None and len(stack) > 1:
818834
# The last fixture raise an error, let's present
819835
# it at the requesting side.
820836
stack = stack[:-1]

src/_pytest/runner.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,11 @@ def __init__(self) -> None:
507507
],
508508
] = {}
509509

510+
def is_node_active(self, node: Node) -> bool:
511+
"""Check if a node is currently active in the stack -- set up and not
512+
torn down yet."""
513+
return node in self.stack
514+
510515
def setup(self, item: Item) -> None:
511516
"""Setup objects along the collector chain to the item."""
512517
needed_collectors = item.listchain()

testing/python/fixtures.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,115 @@ def test_func(resource):
871871
result = pytester.runpytest()
872872
result.stdout.fnmatch_lines(["* 2 passed in *"])
873873

874+
def test_getfixturevalue_teardown_previously_requested_does_not_warn(
875+
self, pytester: Pytester
876+
) -> None:
877+
"""Test that requesting a fixture during teardown that was previously
878+
requested is OK (#12882).
879+
880+
Note: this is still kinda dubious so don't let this test lock you in to
881+
allowing this behavior forever...
882+
"""
883+
pytester.makepyfile(
884+
"""
885+
import pytest
886+
887+
@pytest.fixture
888+
def fix(request, tmp_path):
889+
yield
890+
assert request.getfixturevalue("tmp_path") == tmp_path
891+
892+
def test_it(fix):
893+
pass
894+
"""
895+
)
896+
result = pytester.runpytest("-Werror")
897+
result.assert_outcomes(passed=1)
898+
899+
def test_getfixturevalue_teardown_new_fixture_deprecated(
900+
self, pytester: Pytester
901+
) -> None:
902+
"""Test that requesting a fixture during teardown that was not
903+
previously requested raises a deprecation warning (#12882).
904+
905+
Note: this is a case that previously worked but will become a hard
906+
error after the deprecation is completed.
907+
"""
908+
pytester.makepyfile(
909+
"""
910+
import pytest
911+
912+
@pytest.fixture(scope="session")
913+
def resource():
914+
return "value"
915+
916+
@pytest.fixture
917+
def fix(request):
918+
yield
919+
with pytest.warns(
920+
pytest.PytestRemovedIn10Warning,
921+
match=r'Calling request\\.getfixturevalue\\("resource"\\) during teardown is deprecated',
922+
):
923+
assert request.getfixturevalue("resource") == "value"
924+
925+
def test_it(fix):
926+
pass
927+
"""
928+
)
929+
result = pytester.runpytest()
930+
result.assert_outcomes(passed=1)
931+
932+
def test_getfixturevalue_teardown_new_inactive_fixture_errors(
933+
self, pytester: Pytester
934+
) -> None:
935+
"""Test that requesting a fixture during teardown that was not
936+
previously requested raises an error (#12882)."""
937+
pytester.makepyfile(
938+
"""
939+
import pytest
940+
941+
@pytest.fixture
942+
def fix(request):
943+
yield
944+
request.getfixturevalue("tmp_path")
945+
946+
def test_it(fix):
947+
pass
948+
"""
949+
)
950+
result = pytester.runpytest()
951+
result.assert_outcomes(passed=1, errors=1)
952+
result.stdout.fnmatch_lines(
953+
[
954+
(
955+
'*The fixture value for "tmp_path" is not available during '
956+
"teardown because it was not previously requested.*"
957+
),
958+
]
959+
)
960+
961+
def test_getfixturevalue_teardown_new_inactive_fixture_errors_top_request(
962+
self, pytester: Pytester
963+
) -> None:
964+
"""Test that requesting a fixture during teardown that was not
965+
previously requested raises an error (tricky case) (#12882)."""
966+
pytester.makepyfile(
967+
"""
968+
def test_it(request):
969+
request.addfinalizer(lambda: request.getfixturevalue("tmp_path"))
970+
"""
971+
)
972+
result = pytester.runpytest()
973+
result.assert_outcomes(passed=1, errors=1)
974+
result.stdout.fnmatch_lines(
975+
[
976+
(
977+
'*The fixture value for "tmp_path" is not available during '
978+
"teardown because it was not previously requested.*"
979+
),
980+
]
981+
)
982+
874983
def test_getfixturevalue(self, pytester: Pytester) -> None:
875984
item = pytester.getitem(
876985
"""

0 commit comments

Comments
 (0)