| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
There was a problem hiding this comment.
pytest_unconfigure fires before _cleanup_stack.close(), so warning
filters managed via the cleanup stack (the warnings plugin's
catch_warnings context, in particular) are guaranteed active when GC
Sorry, something went wrong.
filterwarnings = error::ResourceWarning does not fail tests that leak resources through a reference cycle. collect_unraisable wraps the captured ResourceWarning in PytestUnraisableExceptionWarning, a class the user has no filter for, so the run exits 0. This test pins that contract: on a refcycle-leaking test with error::ResourceWarning configured, pytest should exit non-zero and the output should show the inner ResourceWarning rather than the wrapping PytestUnraisableExceptionWarning. Fails at this commit; the next commit ships the fix that turns it green. Refs pytest-dev#14263.
When sys.unraisablehook captures a Warning subclass instance and the user has an active ``error::<that class>`` filter, raise the original warning rather than wrapping in PytestUnraisableExceptionWarning. The wrap path remains for any case where no matching error filter is set, so suites that don't use ``error::<warning>`` filters see no change. Filter matching is approximate: category only, not message/module/lineno. The check errs toward false negatives, never false positives. The regression test added in the previous commit now passes. Additional coverage: - test_refcycle_userwarning_filter: locks the contract for a non-builtin Warning subclass. - test_unraisable_warning_without_filter_still_wraps: scope guard. A Warning raised from __del__ without a matching error filter must still be wrapped, not raised directly. - test_unraisable_warning_filter_add_note_dedups: covers the duplicate- note guard in the unwrap path for singleton/cached Warning instances. Tightens the ``errors`` list type from list[Exception] to list[Warning | RuntimeError]. Adds Paul Zuradzki to AUTHORS. Notes in test_create_task_raises_unraisable_warning_filter that the propagated class is now bare RuntimeWarning rather than the wrapping PytestUnraisableExceptionWarning (because -Werror activates the new unwrap path). Closes pytest-dev#14263.
Forces the bad LIFO order with a conftest that registers a warnings.resetwarnings cleanup via @hookimpl(trylast=True): it pops before unraisableexception's cleanup and clears the user's error::ResourceWarning filter before GC runs, so the leak exits 0. The next commit moves GC into pytest_unconfigure (runs before the cleanup stack closes), making the test pass regardless of plugin order. Refs pytest-dev#14263.
Register only the hook-restore + stash-cleanup as the config.add_cleanup callback. Move the GC pump and collect_unraisable call into a new pytest_unconfigure(config) hook. pytest_unconfigure fires before _cleanup_stack.close(), so warning filters managed via the cleanup stack (the warnings plugin's catch_warnings context, in particular) are guaranteed active when GC runs. This decouples the unraisable step from plugin registration order in default_plugins. The previous arrangement worked only because of LIFO ordering on _cleanup_stack; pytest-dev#13057 (Dec 2024) reordered default_plugins to make that ordering correct, but the structural fragility remained. No observable behavior change. The 141 existing tests in test_unraisableexception + test_warnings + test_recwarn + test_threadexception still pass. The previous commit's test_refcycle_resource_warning_filter continues to fail on main and pass here. This is the structural side of the issue's two proposed fixes; the user-visible side shipped in the previous commit.
After GC moved into pytest_unconfigure, a plugin whose pytest_configure raises UsageError leaves the stash key unset while pytest_unconfigure still runs. Without a presence check it hits KeyError and reports INTERNALERROR instead of USAGE_ERROR. The next commit adds the guard. Refs pytest-dev#14263.
When another plugin's pytest_configure raises (e.g. pytest.UsageError in testing/acceptance_test.py::test_config_error), pluggy skips remaining configure hooks. unraisableexception.pytest_configure never runs, config.stash[unraisable_exceptions] is never set. The previous config.add_cleanup callback wasn't registered in that case either, so cleanup was a no-op. The pytest_unconfigure hook introduced in the previous commit ran unconditionally and hit KeyError on the unset stash, surfacing as INTERNAL_ERROR where pytest should exit with USAGE_ERROR. Guard with a stash-presence check at the top of pytest_unconfigure. test_config_error catches the regression direction.
…gure pytest-dev#14441 reduced the default gc_collect_harder passes to 1 on CPython (5 on PyPy, where __del__ can resurrect objects). That change lived in cleanup(), which this branch emptied when it moved GC into pytest_unconfigure. Carry the same default into the new location so the relocation does not silently revert pytest-dev#14441. CPython still collects the refcycle regression tests in a single pass.
There was a problem hiding this comment.
self-review
Sorry, something went wrong.
|
|
||
| msg = meta.msg | ||
| try: | ||
| warnings.warn(pytest.PytestUnraisableExceptionWarning(msg)) |
There was a problem hiding this comment.
Explainer:
Sorry, something went wrong.
There was a problem hiding this comment.
Commit 4cf46 says, "if user said error::ResourceWarning, an unraisable ResourceWarning becomes a real raised ResourceWarning"
Sorry, something went wrong.
| errors.append(hook_error) | ||
| continue | ||
|
|
||
| if isinstance(meta.exc_value, Warning) and _warning_class_has_error_filter( |
There was a problem hiding this comment.
When collect_unraisable sees an unraisable whose exc_value is a Warning subclass instance, re-raise it directly instead of wrapping w/ pytest.PytestUnraisableExceptionWarning
Sorry, something went wrong.
| if sys.version_info >= (3, 11): | ||
| if meta.cause_msg not in getattr(meta.exc_value, "__notes__", []): | ||
| meta.exc_value.add_note(meta.cause_msg) | ||
| errors.append(meta.exc_value) |
There was a problem hiding this comment.
Append meta.exc_value (the actual ResourceWarning instance) to errors (what eventually gets raise)
Sorry, something went wrong.
| ): | ||
| # Honor the user's error filter on the inner warning class | ||
| # rather than wrapping in PytestUnraisableExceptionWarning. See #14263. | ||
| if sys.version_info >= (3, 11): |
There was a problem hiding this comment.
For Python 3.11+, attach meta.cause_msg as a PEP 678 note
Sorry, something went wrong.
| # still active. This decouples the GC step from plugin registration order. | ||
| # A single collection doesn't necessarily collect everything; the | ||
| # iteration count was determined experimentally by the Trio project. | ||
| if unraisable_exceptions not in config.stash: |
There was a problem hiding this comment.
Bug made possible by -- this PR -- moving GC and collect_unraisable from cleanup callback into pytest_unconfigure hook.
Guard checks: if our configure never ran, stash key is not there, and there's nothing to drain deque, so early return.
Else, this bug was possile:
Sorry, something went wrong.
|
|
||
| # TODO: should be a test failure or error. Currently the exception | ||
| # propagates all the way to the top resulting in exit code 1. | ||
| assert result.ret == 1 |
There was a problem hiding this comment.
I noticed similar TODOs elsewhere.
AI explainer:
The exit code 1 comes from pytest's session-end unraisable handling, not from runpytest_subprocess() (that helper just reports the child's exit code). The leaked warning fires during session-end GC in pytest_unconfigure, after test_it has already been reported as passed, so it propagates to the top of the run instead of failing a specific test. Ideally we'd get failed=1 attributed to test_it; that needs allocation tracking and is out of scope here.
Sorry, something went wrong.
|
cc: @Zac-HD - this is the PR/GH Issue we chatted about at PyCon US sprints last Monday |
Sorry, something went wrong.
There was a problem hiding this comment.
Adding the gc logic to pytest-unconfigure looks good to me, but I'd make two tweaks:
Sorry, something went wrong.
…rnings Reviewer feedback on pytest-dev#14499: - Keep the GC + queue drain in cleanup() as well as pytest_unconfigure, so objects freed while the cleanup stack unwinds still surface. - Drop the direct-raise path (_warning_class_has_error_filter plus the unwrap branch in collect_unraisable). Re-raising the inner warning lost the traceback the hook captures as a string, and the filter match was only approximate. collect_unraisable now always wraps in PytestUnraisableExceptionWarning and lets the warnings machinery decide. Tests dropped or reworked: - Removed test_refcycle_resource_warning_filter: with only error::ResourceWarning the leak no longer fails the suite, which was the dropped direct-raise. - Removed test_refcycle_userwarning_filter: redundant with test_refcycle_unraisable_warning_filter once the wrapper is always raised. - Removed test_unraisable_warning_filter_add_note_dedups: covered the deleted add_note code. - Reworked test_unraisable_decouples_from_cleanup_stack_order to raise ValueError from __del__ and filter on error::pytest.PytestUnraisableExceptionWarning. A ResourceWarning leak only enters the unraisable pipeline through an error::ResourceWarning filter, which no longer promotes the wrapper; a raised exception is wrapped unconditionally, so the timing fix stays observable. Changelog reworded to describe the pytest_unconfigure timing fix instead of the dropped direct-raise behavior.
| Back | FazBrowse Home | New Git URL |
Summary
Refs #14263.
At session end, unraisableexception forces a gc.collect() so leaked objects finalize and their warnings reach sys.unraisablehook. That collection used to run from a config.add_cleanup callback. Those callbacks pop in reverse order, so another plugin's cleanup could tear down a warning filter before the collection ran. The leaked warning then fired with no filter active, so pytest dropped it with no output.
This moves the collection into pytest_unconfigure, which runs before pytest closes the cleanup stack. The warning filters and sys.unraisablehook are both still in place there, so the leak surfaces no matter what order plugins clean up in. That's approach 2 from the issue. I left the collection in cleanup() too, as a final sweep for anything freed while the cleanup stack unwinds.
A surfaced unraisable is still wrapped in PytestUnraisableExceptionWarning. To fail a run on these, use a filter that matches the wrapper: bare error, -W error, or error::pytest.PytestUnraisableExceptionWarning. A class-only filter like error::ResourceWarning surfaces the warning but doesn't fail the run on its own (see Scope).
Testing
Automated
New/changed tests:
Manual. Save as test_del.py:
To see the cleanup-order fix by hand, swap src/_pytest/unraisableexception.py between main and this branch and rerun test_unraisable_decouples_from_cleanup_stack_order: main exits 0, this branch exits 1.
If you run from inside the pytest source tree, the repo's pyproject.toml sets filterwarnings = ['error', ...], which promotes every warning. Comment out the bare 'error' entry or run the snippet from outside the repo.
Reproducer from the issueMy adapted version of the original gist. The original references an undefined stderr_lines in its run_scenario helper; mine applies a one-line fix.
Scopecollect_unraisable wraps every queued unraisable in PytestUnraisableExceptionWarning and emits it through warnings.warn. An earlier revision of this PR added an unwrap branch: when an active error::<class> filter matched the inner warning, it re-raised that warning directly so error::ResourceWarning failed the test. Per reviewer feedback I dropped that branch. Re-raising lost the traceback the hook captures as a string, and the filter check (action == "error" plus issubclass) only approximated Python's real filter matching. So pytest keeps wrapping and lets the warnings machinery decide.
The issue's exact error::ResourceWarning config now surfaces the leak (logged as PytestUnraisableExceptionWarning) instead of dropping it silently, but doesn't fail the run on its own. Suites using bare error, including this repo, do fail, since error matches the wrapper.
Branch structureThe branch builds the original direct-raise approach across red→green commits, then a final commit drops it and keeps the timing fix.
- Early commits added the unwrap branch and its tests, moved the GC into pytest_unconfigure, and guarded pytest_unconfigure against an unset stash when another plugin's pytest_configure raises UsageError.
- cd7e0432e moved gc.collect() and queue processing into pytest_unconfigure.
- The final feedback commit drops the unwrap branch and _warning_class_has_error_filter, keeps the GC in cleanup() as well as pytest_unconfigure, removes the tests that only covered the dropped behavior, and reworks the cleanup-order test to raise ValueError from __del__ under an error::pytest.PytestUnraisableExceptionWarning filter.
Relationship to #14273#14273 attempted approach 2 (move GC to pytest_unconfigure). Maintainers closed it unmerged: #13057 (Dec 2024) had already shipped approach 1 by reordering default_plugins, so the move alone showed no behavior difference under the built-in plugin order. A maintainer asked for "a regression test which demonstrates that the unraisable hook is now subject to warnings."
test_unraisable_decouples_from_cleanup_stack_order answers that. The @hookimpl(trylast=True) conftest registers a warnings.resetwarnings cleanup that pops before unraisableexception's own, defeating the static default_plugins order. On main the run exits 0 (filter gone before the collection runs); here it exits 1 (collection runs in pytest_unconfigure, filter still active).
AI disclosure
Changelog
changelog/14263.bugfix.rst.