Repository navigation
Pass teardown exception to pytest_fixture_post_finalizer explicitly - #15155
krishansubudhi wants to merge 2 commits into
Conversation
Since pytest 8.1, a teardown error is caught and stored before pytest_fixture_post_finalizer runs, so the hook could no longer see it via sys.exc_info() the way it could in 8.0. Call the hook from FixtureDef.finish() again, in a ``finally`` around re-raising the teardown error(s), instead of registering it as the first finalizer. The cache reset moves into a nested ``finally`` so a raising hook still cannot leave the fixture half torn down (pytest-dev#14114), and the existing early return still keeps the hook to one call per teardown. Closes pytest-dev#12306 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
RonnyPfannschmidt
left a comment
There was a problem hiding this comment.
🤖 Written by Claude Opus 5.5 via Claude Code for the pytest maintainers; I prompted it, it did the work, I read it.
Thanks for digging through the history here. The timeline in the description is accurate. I'd still like a different design, because this PR repeats the mistake that the earlier changes in this area made.
The hook should not depend on sys.exc_info(), and it should not need a particular call site to see the error. 434282e broke the implicit exc_info contract by moving the call, and #14275 made the hook a finalizer. Both changes were about where the hook runs, and both broke something: #12306, then #14800. This PR moves the call site again so that the implicit state lines up. The next refactor of finish() will break it the same way.
What I'd like instead:
- Add an explicit parameter to
pytest_fixture_post_finalizerthat carries the teardown error: the single exception, theBaseExceptionGroup, orNone. pluggy allows hookimpls to accept a subset of the spec's arguments, so existing plugins keep working. - Call the hook directly from
FixtureDef.finish()after the finalizers have run. Drop the lambda finalizer and theassert not self._finalizersinexecute(). This part of your change is good, and it also fixes #14800 (overlaps with #14801). - An exception raised by the hook itself is a plugin bug and should fail hard, not be reported like a fixture teardown error (see my comment on #14114).
- Docs and the changelog should point users to the new argument. If
sys.exc_info()still happens to work, we shouldn't present it as the supported way.
One concrete bug in the current diff: when a parametrized fixture is torn down because the next test needs a different param, execute() calls self.finish(request) with the new request. With this PR the hook receives that request, so the hook for the param=1 instance sees request.param == 2. On main the hook gets the request that set the fixture up. Whatever the final design is, finish() needs to use the setup request.
Please also state in the PR whether the hook should run for a setup that failed before a result was cached. With this PR it doesn't, and #14801 tests that it does.
Generated by Claude Code
|
Thanks for the thorough review. Agreed on all points. I'll rework it: |
Per review, stop relying on sys.exc_info() inside the hook: - Add a ``teardown_exception`` argument to pytest_fixture_post_finalizer: the single teardown exception, a BaseExceptionGroup when several finalizers failed, or None. Existing hookimpls that do not accept it keep working. - Call the hook directly from FixtureDef.finish() after the finalizers have run; exceptions raised by the hook itself propagate. - finish() now passes the request that set the fixture up, also when a parametrized fixture is torn down to switch to another param. - Run the hook (once) for a setup that failed before a result was cached, and tear such a setup down before the next execute() (pytest-dev#14800).
|
Thanks for the thorough review, reworked in f3b9ce1:
|
Reworked per review: the hook now gets a teardown_exception argument instead of relying on sys.exc_info(). Also fixes #14800.
What changed and why
Up to pytest 8.0,
pytest_fixture_post_finalizerwas called from afinally:inFixtureDef.finish(), so a hook could inspect a failing teardown withsys.exc_info(). In 8.1, 434282e collected teardown errors into a list first. The hook then ran after the errors had already been caught, andsys.exc_info()became(None, None, None). Later, #14114 (719f1ec) made the hook the first registered finalizer so that a raising hook could not leave the fixture half torn down. That kept the hook out of reach of the active exception.This PR calls the hook from
finish()again. It runs in afinallyaround re-raising the collected teardown error(s), so the error (or theBaseExceptionGroup) is active while the hook runs. The cache reset moves into a nestedfinally, which keeps the #14114 guarantee: a raising hook still cannot stopcached_resultfrom being cleared or the finalizers from being dropped. The existingcached_result is Noneearly return still limits the hook to one call per teardown (#5848). The special-case lambda finalizer and itsassert not self._finalizersinexecute()are gone, so the source diff is net zero lines.One behaviour difference is worth a reviewer's eye. If a fixture's teardown fails and the hook also raises, both errors are still reported, but they are chained ("During handling of the above exception, another exception occurred") instead of grouped in one
BaseExceptionGroup. That is how 8.0 behaved. If you would rather keep the group, I can rework it.test_fixture_post_finalizer_hook_exceptionfrom #14114 still passes unchanged.How it was tested
test_fixture_post_finalizer_sees_teardown_exceptionintesting/python/fixtures.py. It fails onmainand passes with this change. It also checks thatsys.exc_info()is empty when the teardown succeeds.pytest -n 8): 4681 passed, 49 skipped, 13 xfailed, 7 xpassed. The one error came from my own-p no:cacheproviderflag, andtesting/test_legacypath.pypasses when run normally.pre-commit run --all-filesis clean (ruff, ruff-format, mypy, codespell and the rest).changelog/12306.bugfix.rst; added myself toAUTHORS.This PR was written with an AI coding agent and reviewed by me (Krishan); the commit carries a
Co-authored-bytrailer as the PR template asks.