Repository navigation
8.1.1 removed ability to introspect teardown exceptions in pytest_fixture_post_finalizer #12306
Description
Activity
- addedtype: regressionindicates a problem that was introduced in a release which was working previouslyindicates a problem that was introduced in a release which was working previously
on Jul 6, 2024 I'd be happy to accept a PR based on your branch to restore this behavior 🙂
- addedtopic: fixturesanything involving fixtures directly or indirectlyanything involving fixtures directly or indirectly
on Jul 6, 2024 I've been looking into issue #12306 about the regression in
pytest_fixture_post_finalizerwheresys.exc_info()no longer returns the teardown exception. After digging through the commit history and current code, I'd like to propose a fix and get your feedback before submitting a PR.Timeline & root cause
-
Commit 434282e (pytest 7.3) introduced
ExceptionGroupcollection for teardown exceptions and moved the hook call from inside thetry/finallyblock to before the exception re‑raise. This brokesys.exc_info()because the exception is already caught when the hook runs. -
Commit 719f1ec changed the architecture: the hook is no longer called directly in
finish()but registered as a finalizer (self.addfinalizer(lambda: hook.pytest_fixture_post_finalizer(...))). This was done so that a thrown exception from the hook does not interrupt the cleanup logic. -
Commit 96728d5 added a guard
if cached_result is None: returnto prevent the hook from being called multiple times.
Current behavior
In
fixtures.py,finish()pops finalizers fromself._finalizersin a loop, catches any exception, and stores it in anexceptionslist. The hook is the first finalizer registered, so it is executed last—after the teardown exception has already been caught. At that pointsys.exc_info()is(None, None, None).Why the existing branch (mchase‑nasuni) can't be merged directly
That branch is based on the pre‑719f1ec9 architecture where the hook was called directly in
finish(). Since 719f1ec the hook lives in_finalizers. Applying the old diff would cause the hook to run twice (once as a finalizer, once infinally) and would reintroduce the problem that 719f1ec fixed (hook exceptions interfering with cleanup).Proposed solution
The core requirement is that the hook must see an active exception (so
sys.exc_info()works) while still isolating hook‑thrown exceptions from the main cleanup.I suggest:
- Remove the hook from
_finalizers– Inexecute(), do not registerpytest_fixture_post_finalizeras a finalizer. - Call the hook inside the re‑raise block – In
finish(), after thewhileloop that runs the other finalizers, wrap the re‑raise logic in atry/finallyand invoke the hook in thefinallyclause. - Catch hook exceptions – Inside that
finally, wrap the hook call in atry/exceptand ignore any exception from the hook (so it doesn't disturb the re‑raise of the original teardown exception).
Pseudocode:
def finish(self, request): # ... run all finalizers except the hook, collect exceptions ... try: if len(exceptions) == 1: raise exceptions[0] elif len(exceptions) > 1: raise BaseExceptionGroup(...) finally: try: node.ihook.pytest_fixture_post_finalizer(fixturedef=self, request=request) except BaseException: pass # hook exception does not affect the main flow
This preserves the isolation that 719f1ec intended (hook exceptions don't break cleanup) while making
sys.exc_info()valid again.Benefits
sys.exc_info()works in the hook (exception is being re‑raised).- Hook exceptions are caught and ignored, not interfering with the original teardown exception.
- The guard from 96728d5 is no longer needed because the hook is never registered as a finalizer.
- No double‑invocation of the hook.
Questions for maintainers
- Does this approach align with the intent behind 719f1ec? (I believe it does – we still isolate hook failures.)
- Are there any other reasons the hook was moved to a finalizer that I might have missed?
- Is there a preference for where the hook call should sit (e.g., inside the
tryblock before the re‑raise, or in thefinallyas above)?
If you agree on the direction, I'll prepare a PR with the changes and appropriate tests (verifying
sys.exc_info()works, hook exceptions are swallowed, and the hook isn't called multiple times).Thanks for your time!
-
From several years ago until recently (works in 8.0.2, doesn't work in 8.1.1), it was possible to get teardown exception information in
pytest_fixture_post_finalizerby callingsys.exc_info(). This appears to have changed with the following commit: 434282e#diff-49027cfd80e14edac9b0fae71f7228a408a09599f66a7815839ce8c3ae2ab84fL1020-R1043, which moved the hook call from inside the finally block to before the exception is (re)raised.Perhaps there's another way to get at this information, but I wasn't able to find it by reading documentation, searching the web, or trying to read the source code. I was able to make things work again by somewhat rearranging the new code.
This was first encountered in CI images that I believe were based off of Ubuntu 22.04, but I was able to reproduce on macOS Monterey.
Here is the code for my attempt at a minimal example:
conftest.pytest_repro.pyAgainst 8.0.2, this prints
Saw exception!, andpip listhas the following output:Against 8.1.1, this prints
Didn't see exception, andpip listhas the following output:Against 8.2.0, this still prints
Didn't see exception, andpip listhas the following output:Against a branch I created in a fork of pytest to check that I'm right about this issue (main...mchase-nasuni:pytest:mchase-nasuni-internal-testing), this prints
Saw exception!, andpip listhas the following output: