Skip to content

Commit d88ccd9

Browse files
committed
Move finalizers management to SetupState
1 parent 60f4286 commit d88ccd9

File tree

3 files changed

+93
-88
lines changed

3 files changed

+93
-88
lines changed

src/_pytest/fixtures.py

Lines changed: 22 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,6 @@
6969
from _pytest.warning_types import PytestWarning
7070

7171

72-
if sys.version_info < (3, 11):
73-
from exceptiongroup import BaseExceptionGroup
74-
75-
7672
if TYPE_CHECKING:
7773
from _pytest.python import CallSpec2
7874
from _pytest.python import Function
@@ -1035,7 +1031,6 @@ def __init__(
10351031
# If the fixture was executed, the current value of the fixture.
10361032
# Can change if the fixture is executed with different parameters.
10371033
self.cached_result: _FixtureCachedResult[FixtureValue] | None = None
1038-
self._finalizers: Final[nodes.FinalizerStorage] = {}
10391034
# The request object with which the fixture was set up.
10401035
self._cached_request: SubRequest | None = None
10411036
# Handles to remove our finalizer from various scopes.
@@ -1050,39 +1045,20 @@ def scope(self) -> _ScopeName:
10501045
return self._scope.value
10511046

10521047
def addfinalizer(self, finalizer: Callable[[], object]) -> Callable[[], None]:
1053-
return nodes.append_finalizer(self._finalizers, finalizer)
1048+
assert self._cached_request is not None
1049+
setupstate = self._cached_request.session._setupstate
1050+
return setupstate.fixture_addfinalizer(finalizer, self)
10541051

10551052
def finish(self, request: SubRequest) -> None:
1056-
exceptions: list[BaseException] = []
1057-
while self._finalizers:
1058-
_, fin = self._finalizers.popitem()
1059-
try:
1060-
fin()
1061-
except TEST_OUTCOME as e:
1062-
exceptions.append(e)
1063-
node = request.node
10641053
try:
1065-
node.ihook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
1066-
except TEST_OUTCOME as e:
1067-
exceptions.append(e)
1068-
1069-
# Even if finalization fails, we invalidate the cached fixture
1070-
# value and remove all finalizers because they may be bound methods
1071-
# which will keep instances alive.
1072-
self.cached_result = None
1073-
self._finalizers.clear()
1074-
request._fixturemanager._on_fixture_finished(self)
1075-
self._cached_request = None
1076-
# Avoid accumulating garbage finalizers in nodes and fixturedefs (#4871).
1077-
for handle in self._self_finalizer_handles:
1078-
handle()
1079-
self._self_finalizer_handles.clear()
1080-
1081-
if len(exceptions) == 1:
1082-
raise exceptions[0]
1083-
elif len(exceptions) > 1:
1084-
msg = f'errors while tearing down fixture "{self.argname}" of {node}'
1085-
raise BaseExceptionGroup(msg, exceptions[::-1])
1054+
request.session._setupstate.fixture_teardown(self, request.node)
1055+
finally:
1056+
self.cached_result = None
1057+
self._cached_request = None
1058+
# Avoid accumulating garbage finalizers in nodes and fixturedefs (#4871).
1059+
for handle in self._self_finalizer_handles:
1060+
handle()
1061+
self._self_finalizer_handles.clear()
10861062

10871063
def execute(self, request: SubRequest) -> FixtureValue:
10881064
"""Return the value of this fixture, executing it if not cached."""
@@ -1095,6 +1071,11 @@ def execute(self, request: SubRequest) -> FixtureValue:
10951071
for argname in self.argnames:
10961072
request._get_active_fixturedef(argname)
10971073

1074+
self._cached_request = request
1075+
setupstate = request.session._setupstate
1076+
setupstate.fixture_setup(self)
1077+
setupstate.fixture_addfinalizer(self._run_post_finalizer, self)
1078+
10981079
ihook = request.node.ihook
10991080
try:
11001081
# Setup the fixture, run the code in it, and cache the value
@@ -1103,8 +1084,6 @@ def execute(self, request: SubRequest) -> FixtureValue:
11031084
fixturedef=self, request=request
11041085
)
11051086
finally:
1106-
self._cached_request = request
1107-
request._fixturemanager._on_fixture_setup(self)
11081087
# Schedule our finalizer, even if the setup failed.
11091088
fin = functools.partial(self.finish, request)
11101089
self._self_finalizer_handles.append(request.node.addfinalizer(fin))
@@ -1113,6 +1092,12 @@ def execute(self, request: SubRequest) -> FixtureValue:
11131092

11141093
return result
11151094

1095+
def _run_post_finalizer(self) -> None:
1096+
request = self._cached_request
1097+
assert request is not None
1098+
ihook = request.node.ihook
1099+
ihook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
1100+
11161101
def _is_cache_hit(self, old_cache_key: object, new_cache_key: object) -> bool:
11171102
try:
11181103
# Attempt to make a normal == check: this might fail for objects
@@ -1649,8 +1634,6 @@ def __init__(self, session: Session) -> None:
16491634
self._nodeid_autousenames: Final[dict[str, list[str]]] = {
16501635
"": self.config.getini("usefixtures"),
16511636
}
1652-
# Using dict for keeping insertion order.
1653-
self._active_fixturedefs: Final[dict[FixtureDef[Any], None]] = {}
16541637
session.config.pluginmanager.register(self, "funcmanage")
16551638

16561639
def getfixtureinfo(
@@ -1998,25 +1981,6 @@ def _matchfactories(
19981981
if fixturedef.baseid in parentnodeids:
19991982
yield fixturedef
20001983

2001-
def _teardown_stale_fixtures(self, nextitem: nodes.Item) -> None:
2002-
exceptions: list[BaseException] = []
2003-
for fixturedef in reversed(list(self._active_fixturedefs.keys())):
2004-
try:
2005-
fixturedef._finish_if_param_changed(nextitem)
2006-
except TEST_OUTCOME as e:
2007-
exceptions.append(e)
2008-
if len(exceptions) == 1:
2009-
raise exceptions[0]
2010-
elif len(exceptions) > 1:
2011-
msg = f'errors while tearing down fixtures for "{nextitem.nodeid}"'
2012-
raise BaseExceptionGroup(msg, exceptions[::-1])
2013-
2014-
def _on_fixture_setup(self, fixturedef: FixtureDef[Any]) -> None:
2015-
self._active_fixturedefs[fixturedef] = None
2016-
2017-
def _on_fixture_finished(self, fixturedef: FixtureDef[Any]) -> None:
2018-
del self._active_fixturedefs[fixturedef]
2019-
20201984

20211985
def show_fixtures_per_test(config: Config) -> int | ExitCode:
20221986
from _pytest.main import wrap_session

src/_pytest/nodes.py

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
from typing import NoReturn
1717
from typing import overload
1818
from typing import TYPE_CHECKING
19-
from typing import TypeAlias
2019
from typing import TypeVar
2120
import warnings
2221

@@ -93,26 +92,6 @@ def _imply_path(
9392
return Path(fspath)
9493

9594

96-
class _FinalizerId:
97-
__slots__ = ()
98-
99-
100-
Finalizer: TypeAlias = Callable[[], object]
101-
FinalizerStorage: TypeAlias = dict[_FinalizerId, Finalizer]
102-
103-
104-
def append_finalizer(
105-
finalizer_storage: FinalizerStorage, finalizer: Finalizer
106-
) -> Callable[[], None]:
107-
finalizer_id = _FinalizerId()
108-
finalizer_storage[finalizer_id] = finalizer
109-
110-
def remove_finalizer() -> None:
111-
finalizer_storage.pop(finalizer_id, None)
112-
113-
return remove_finalizer
114-
115-
11695
_NodeType = TypeVar("_NodeType", bound="Node")
11796

11897

src/_pytest/runner.py

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99
import os
1010
import sys
1111
import types
12+
from typing import Any
1213
from typing import cast
1314
from typing import final
1415
from typing import Generic
1516
from typing import Literal
1617
from typing import TYPE_CHECKING
18+
from typing import TypeAlias
1719
from typing import TypeVar
1820

1921
from .config import Config
@@ -27,10 +29,8 @@
2729
from _pytest._code.code import TerminalRepr
2830
from _pytest.config.argparsing import Parser
2931
from _pytest.deprecated import check_ispytest
30-
from _pytest.nodes import append_finalizer
3132
from _pytest.nodes import Collector
3233
from _pytest.nodes import Directory
33-
from _pytest.nodes import FinalizerStorage
3434
from _pytest.nodes import Item
3535
from _pytest.nodes import Node
3636
from _pytest.outcomes import Exit
@@ -43,6 +43,7 @@
4343
from exceptiongroup import BaseExceptionGroup
4444

4545
if TYPE_CHECKING:
46+
from _pytest.fixtures import FixtureDef
4647
from _pytest.main import Session
4748
from _pytest.terminal import TerminalReporter
4849

@@ -433,6 +434,26 @@ def collect() -> list[Item | Collector]:
433434
return rep
434435

435436

437+
class _FinalizerId:
438+
__slots__ = ()
439+
440+
441+
_Finalizer: TypeAlias = Callable[[], object]
442+
_FinalizerStorage: TypeAlias = dict[_FinalizerId, _Finalizer]
443+
444+
445+
def _append_finalizer(
446+
finalizer_storage: _FinalizerStorage, finalizer: _Finalizer
447+
) -> Callable[[], None]:
448+
finalizer_id = _FinalizerId()
449+
finalizer_storage[finalizer_id] = finalizer
450+
451+
def remove_finalizer() -> None:
452+
finalizer_storage.pop(finalizer_id, None)
453+
454+
return remove_finalizer
455+
456+
436457
class SetupState:
437458
"""Shared state for setting up/tearing down test items or collectors
438459
in a session.
@@ -503,11 +524,12 @@ def __init__(self) -> None:
503524
Node,
504525
tuple[
505526
# Node's finalizers.
506-
FinalizerStorage,
527+
_FinalizerStorage,
507528
# Node's exception and original traceback, if its setup raised.
508529
tuple[OutcomeException | Exception, types.TracebackType | None] | None,
509530
],
510531
] = {}
532+
self._fixture_finalizers: dict[FixtureDef[Any], _FinalizerStorage] = {}
511533

512534
def setup(self, item: Item) -> None:
513535
"""Setup objects along the collector chain to the item."""
@@ -523,8 +545,8 @@ def setup(self, item: Item) -> None:
523545
for col in needed_collectors[len(self.stack) :]:
524546
assert col not in self.stack
525547
# Push onto the stack.
526-
finalizers = FinalizerStorage()
527-
append_finalizer(finalizers, col.teardown)
548+
finalizers = _FinalizerStorage()
549+
_append_finalizer(finalizers, col.teardown)
528550
self.stack[col] = (finalizers, None)
529551
try:
530552
col.setup()
@@ -544,7 +566,7 @@ def addfinalizer(
544566
assert node and not isinstance(node, tuple)
545567
assert callable(finalizer)
546568
assert node in self.stack, (node, self.stack)
547-
return append_finalizer(self.stack[node][0], finalizer)
569+
return _append_finalizer(self.stack[node][0], finalizer)
548570

549571
def teardown_exact(self, nextitem: Item | None) -> None:
550572
"""Teardown the current stack up until reaching nodes that nextitem
@@ -569,7 +591,7 @@ def teardown_exact(self, nextitem: Item | None) -> None:
569591

570592
if isinstance(node, Item) and nextitem is not None:
571593
try:
572-
self._teardown_item_towards_nextitem(nextitem)
594+
self._finish_stale_fixtures(nextitem)
573595
except TEST_OUTCOME as e:
574596
exceptions.append(e)
575597

@@ -586,8 +608,48 @@ def teardown_exact(self, nextitem: Item | None) -> None:
586608
if nextitem is None:
587609
assert not self.stack
588610

589-
def _teardown_item_towards_nextitem(self, nextitem: Item) -> None:
590-
nextitem.session._fixturemanager._teardown_stale_fixtures(nextitem)
611+
def fixture_setup(self, fixturedef: FixtureDef[Any]) -> None:
612+
assert fixturedef not in self._fixture_finalizers
613+
self._fixture_finalizers[fixturedef] = _FinalizerStorage()
614+
615+
def fixture_addfinalizer(
616+
self, finalizer: Callable[[], object], fixturedef: FixtureDef[Any]
617+
) -> Callable[[], None]:
618+
assert fixturedef in self._fixture_finalizers
619+
return _append_finalizer(self._fixture_finalizers[fixturedef], finalizer)
620+
621+
def fixture_teardown(self, fixturedef: FixtureDef[Any], node: Node) -> None:
622+
assert fixturedef in self._fixture_finalizers
623+
# Do not remove for now to allow adding finalizers last second.
624+
finalizers = self._fixture_finalizers[fixturedef]
625+
626+
exceptions: list[BaseException] = []
627+
while finalizers:
628+
_, fin = finalizers.popitem()
629+
try:
630+
fin()
631+
except TEST_OUTCOME as e:
632+
exceptions.append(e)
633+
634+
self._fixture_finalizers.pop(fixturedef)
635+
if len(exceptions) == 1:
636+
raise exceptions[0]
637+
elif len(exceptions) > 1:
638+
msg = f'errors while tearing down fixture "{fixturedef.argname}" of {node}'
639+
raise BaseExceptionGroup(msg, exceptions[::-1])
640+
641+
def _finish_stale_fixtures(self, nextitem: Item) -> None:
642+
exceptions: list[BaseException] = []
643+
for fixturedef in reversed(list(self._fixture_finalizers.keys())):
644+
try:
645+
fixturedef._finish_if_param_changed(nextitem)
646+
except TEST_OUTCOME as e:
647+
exceptions.append(e)
648+
if len(exceptions) == 1:
649+
raise exceptions[0]
650+
elif len(exceptions) > 1:
651+
msg = f'errors while tearing down fixtures for "{nextitem.nodeid}"'
652+
raise BaseExceptionGroup(msg, exceptions[::-1])
591653

592654

593655
def collect_one_node(collector: Collector) -> CollectReport:

0 commit comments

Comments
 (0)