From 86a3ed6ae4c969cdb423b7c31fd79194061a8996 Mon Sep 17 00:00:00 2001 From: Aleksandr Brodin Date: Wed, 27 Mar 2024 10:13:11 +0700 Subject: [PATCH] move fixture finalizing to standalone hook --- src/_pytest/fixtures.py | 49 +++++++++++++++++++------------------- src/_pytest/hookspec.py | 22 ++++++++++++++--- testing/python/fixtures.py | 8 ++++--- 3 files changed, 49 insertions(+), 30 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 03b042127..f94bbcd26 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1015,31 +1015,8 @@ class FixtureDef(Generic[FixtureValue]): self._finalizers.append(finalizer) def finish(self, request: SubRequest) -> None: - exceptions: List[BaseException] = [] - while self._finalizers: - fin = self._finalizers.pop() - try: - fin() - except BaseException as e: - exceptions.append(e) node = request.node - if len(exceptions) == 1: - final_exception = exceptions[0] - elif len(exceptions) > 1: - msg = f'errors while tearing down fixture "{self.argname}" of {node}' - final_exception = BaseExceptionGroup(msg, exceptions[::-1]) - else: - final_exception = None - node.ihook.pytest_fixture_post_finalizer( - fixturedef=self, request=request, exception=final_exception - ) - # Even if finalization fails, we invalidate the cached fixture - # value and remove all finalizers because they may be bound methods - # which will keep instances alive. - self.cached_result = None - self._finalizers.clear() - if final_exception: - raise final_exception + node.ihook.pytest_fixture_teardown(fixturedef=self, request=request) def execute(self, request: SubRequest) -> FixtureValue: """Return the value of this fixture, executing it if not cached.""" @@ -1151,6 +1128,30 @@ def pytest_fixture_setup( return result +def pytest_fixture_teardown( + fixturedef: FixtureDef[FixtureValue], request: SubRequest +) -> None: + exceptions: List[BaseException] = [] + while fixturedef._finalizers: + fin = fixturedef._finalizers.pop() + try: + fin() + except BaseException as e: + exceptions.append(e) + node = request.node + node.ihook.pytest_fixture_post_finalizer(fixturedef=fixturedef, request=request) + # Even if finalization fails, we invalidate the cached fixture + # value and remove all finalizers because they may be bound methods + # which will keep instances alive. + fixturedef.cached_result = None + fixturedef._finalizers.clear() + if len(exceptions) == 1: + raise exceptions[0] + elif len(exceptions) > 1: + msg = f'errors while tearing down fixture "{fixturedef.argname}" of {node}' + raise BaseExceptionGroup(msg, exceptions[::-1]) + + def wrap_function_to_error_out_if_called_directly( function: FixtureFunction, fixture_marker: "FixtureFunctionMarker", diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 221c0c8a8..6df64b9b0 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -860,10 +860,28 @@ def pytest_fixture_setup( """ +def pytest_fixture_teardown( + fixturedef: "FixtureDef[Any]", request: "SubRequest" +) -> None: + """Perform fixture teardown execution. + + :param fixturdef: + The fixture definition object. + :param request: + The fixture request object. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given fixture, only + conftest files in the fixture scope's directory and its parent directories + are consulted. + """ + + def pytest_fixture_post_finalizer( fixturedef: "FixtureDef[Any]", request: "SubRequest", - exception: "BaseException | None", ) -> None: """Called after fixture teardown, but before the cache is cleared, so the fixture result ``fixturedef.cached_result`` is still available (not @@ -873,8 +891,6 @@ def pytest_fixture_post_finalizer( The fixture definition object. :param request: The fixture request object. - :param exception: - An exception raised in the finalisation of the fixtures. Use in conftest plugins ======================= diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 2fe35800f..06c9c2c1f 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -4014,7 +4014,7 @@ def test_pytest_fixture_setup_and_post_finalizer_hook(pytester: Pytester) -> Non ) -def test_exceptions_in_pytest_fixture_setup_and_post_finalizer_hook( +def test_exceptions_in_pytest_fixture_setup_and_pytest_fixture_teardown( pytester: Pytester, ) -> None: pytester.makeconftest( @@ -4024,8 +4024,10 @@ def test_exceptions_in_pytest_fixture_setup_and_post_finalizer_hook( def pytest_fixture_setup(fixturedef): result = yield print('SETUP EXCEPTION in {0}: {1}'.format(fixturedef.argname, result.exception)) - def pytest_fixture_post_finalizer(fixturedef, exception): - print('TEARDOWN EXCEPTION in {0}: {1}'.format(fixturedef.argname, exception)) + @pytest.hookimpl(hookwrapper=True) + def pytest_fixture_teardown(fixturedef): + result = yield + print('TEARDOWN EXCEPTION in {0}: {1}'.format(fixturedef.argname, result.exception)) """ ) pytester.makepyfile(