From 90172a20fc74b3d74c525c49638f135036b86fa4 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 ef3016bd5a6..4584f843a91 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1014,31 +1014,8 @@ def addfinalizer(self, finalizer: Callable[[], object]) -> None: 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: finalizer = functools.partial(self.finish, request=request) @@ -1132,6 +1109,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 221c0c8a835..6df64b9b039 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 7b7cfa0971d..d756c3bfdf3 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -4014,7 +4014,7 @@ def test_func(my_fixture): ) -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(