Repository navigation
Add an error_later warning filter action - #15135
Draft
RonnyPfannschmidt wants to merge 9 commits into
Draft
RonnyPfannschmidt wants to merge 9 commits into
RonnyPfannschmidt wants to merge 9 commits into
Conversation
The 'error' action raises at the warnings.warn() call site. That aborts the code under test halfway through and anchors the failure on the frame that emitted the warning rather than on what the test was doing. It stays the right choice for people who want to fix warnings immediately. 'error_later' is accepted wherever a filter is (-W, the filterwarnings ini option, @pytest.mark.filterwarnings). It is installed as 'always', so the warning is recorded and the code runs to completion; pytest then decides against the filters it applied, walked last-first like warnings.filters and matched like warnings.filterwarnings (message with re.I, module regex against the emitting module, lineno), whether the recorded warning errors later. The action itself rejects the module and line fields, because a recorded warning carries a filename, not the emitting module's __name__. Each runtest phase is wrapped, not followed by a trylast hook, so a phase that raised is drained too: its warnings are added to that phase's failure report instead of leaking into teardown as a second, separate error. Warnings with no test phase to fail -- collection, plugin and conftest import, pytest_configure, issue_config_time_warning, pytest_sessionfinish, a skipped or xfailed test -- are listed in a "late warning errors" section at the end, counted in the summary line and fail the run with the new ExitCode.LATE_WARNING_ERROR (7). The record=False context that _do_configure keeps open diverts them through showwarning, until the session can no longer report; after that they are shown as before rather than dropped. Co-Authored-By: Claude Opus 5.5 via Claude Code <noreply@anthropic.com>
Failing every test that emits a matching warning is the right default for a suite that is already clean, but it turns a newly deprecated API into a wall of red. 'session' lets the tests pass and only fails the run: every late warning goes into the end-of-run "late warning errors" section, is counted in the summary line, and the exit code becomes LATE_WARNING_ERROR unless the run already failed for another reason. The value is validated in pytest_configure, so a typo is a usage error up front rather than once a warning first matches. Co-Authored-By: Claude Opus 5.5 via Claude Code <noreply@anthropic.com>
Under pytest-xdist every worker has its own late-warning list and exit status, and the controller uses neither: 'session' mode, and the end-of-run section in either mode, passed silently. Workers now put their late warnings into config.workeroutput, which xdist already serialises and hands to the controller with pytest_testnodedown. The controller merges them into its own list, so the section, the summary line and the exit code come out as without xdist. Test reports cannot carry this: xdist only forwards collect reports that did not pass, so warnings emitted while collecting would still be lost. Every worker collects and configures, so warnings with no test node are merged once. The collector is only registered when the pytest_testnodedown hook spec exists, since a builtin plugin implementing an unknown hook fails validation. Co-Authored-By: Claude Opus 5.5 via Claude Code <noreply@anthropic.com>
Covers the action, both error_later_report modes, which warnings land in the end-of-run section, xdist, and the limits: no module or line field, not accepted by PYTHONWARNINGS or python -W, and filters the code under test installs itself can only ignore a warning, not stop it from erroring later. Exit code 7 is added to the exit code reference. The changelog fragment is an orphan (+error-later) until the pull request has a number; the changelog filename hook rejects it until then. Co-Authored-By: Claude Opus 5.5 via Claude Code <noreply@anthropic.com>
error_later decided afterwards whether a recorded warning should error, by re-walking the filters pytest had applied. That walk could not see filters installed by the code under test, conftest or fixtures, and had to guess the emitting module from the file name, which is wrong for exec'd code, __main__ and modules that rebind __name__. So module and line fields were rejected outright. Install each error_later filter as a real "always" filter instead, with a probe object in the module slot: the stdlib calls .match() on it, so the probe sees the exact module name, and records a verdict when the filter matches. The stdlib also calls .match() on filters that go on to lose on category or line, so a never-matching loss probe follows each one and clears the verdict. A verdict is only recorded when the warning will reach pytest's recording, so a warning taken by pytest.warns or by the test's own catch_warnings leaves nothing behind. Module and line fields now work, and precedence is exactly the stdlib's: filters the test installs itself outrank error_later as they outrank error. Without an error_later filter, recording stays a plain list.append. Co-Authored-By: Claude Opus 5.5 via Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 via Claude Code <noreply@anthropic.com>
The xdist collector was registered when config.hook had pytest_testnodedown, taken to mean xdist's hookspec is present. But pytest-cov implements that hook as optional too, which creates the hook caller without a spec when xdist is not installed, so the collector's non-optional implementation failed validation: INTERNALERROR "unknown hook 'pytest_testnodedown'" in check_pending (the plugins CI job). Mark the implementation optional and register it unconditionally, as pytest-cov does; without xdist it is simply never called. Co-Authored-By: Claude Opus 5.5 via Claude Code <noreply@anthropic.com>
`:option:`-W <pytest -W>`` does not resolve (pytest's option is referenced as plain `:option:`-W``), and warnings.catch_warnings is a class. Both failed the Sphinx -W build on Read the Docs. Co-Authored-By: Claude Opus 5.5 via Claude Code <noreply@anthropic.com>
A test that replaces warnings.showwarning with a wrapper forwarding to the original, as numpy.testing.suppress_warnings does, turned an error_later warning into a plain one (pytest-dev#15139). The win probe only recorded a verdict when pytest's recording was the direct destination, so that a warning taken elsewhere could not leave one behind. The probe now always leaves the verdict pending, and a wrapper around warnings._showwarnmsg, which the stdlib calls for every shown warning, makes it current for the length of that call only. Whatever reaches pytest's recording during the call consumes it, and it is gone when the call returns, so a hook that discards the warning cannot hand it to the next one. Keying the verdict on the message text instead still mis-tags a later warning with the same text that a filter in front of error_later takes. The wrapper is installed with the first error_later filter and put back when the pytest warnings context that installed it exits, since catch_warnings() does not restore _showwarnmsg. Co-Authored-By: Claude Opus 5.5 via Claude Code <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Written by Claude Opus 5.5 via Claude Code for the pytest maintainers; I prompted it, it did the work, I read it.
Draft: the name, exit code 7 and the xdist hook in core are open questions, listed below.
A test-runner version of
-W error, as suggested in #14931 (review): track where warnings that are errors come from, let the current test continue, and report the full picture as an error.errorraises at thewarnings.warncall site. That aborts the code under test halfway through, so a test that emits several warnings reports only the first.error_laterrecords the warning instead, lets the code run to completion, and fails afterwards.Behaviour
error_lateris accepted wherever a warning filter is:-W, thefilterwarningsini option and@pytest.mark.filterwarnings.error_laterapplies, with its own filter matching: message, category, module and line fields all work, and precedence is the stdlib's, including filters installed by the code under test, fixtures and conftest. See "How matching works" below.error_later_report = test(the default) fails the phase that emitted the warning. If that phase already failed, its failure gets a "late warning errors" section instead of a second report.error_later_report = sessionlets tests pass and fails the run at the end with a newExitCode.LATE_WARNING_ERROR(7).pytest_configure,pytest_sessionfinish, and skipped or xfailed tests.workeroutput, merged inpytest_testnodedown, so both modes behave the same with-n.error_laterfilter pay nothing: the check returns immediately.Open questions
Name.
error_laterhas an underscore, while the stdlib actions are single words. It is also rejected byPYTHONWARNINGSandpython -Wat interpreter start.Exit code.
ExitCode.LATE_WARNING_ERROR = 7is new public API.xdist hook. Core now implements an xdist hook,
pytest_testnodedown, the same way pytest-cov does. It is only registered when the hookspec exists.Filters installed by the test outrank
error_later. A test's ownsimplefilter("always")orcatch_warningsnow takes precedence over anerror_laterfilter, just as it already does overerror. Warnings re-emitted bypytest.warnscarry a module name derived from the file path, so module-restricted filters miss them, again as forerror.How matching works
Each
error_laterfilter is installed as a realalwaysfilter whose module slot holds a probe object. The stdlib calls.match()on it, so the probe sees the exact module name and records a verdict when the filter matches. The stdlib also calls.match()on filters that go on to lose on category or line, so a never-matching loss probe follows each one and clears the verdict. The probe only leaves the verdict pending. A wrapper aroundwarnings._showwarnmsg, which the stdlib calls for every warning it shows, makes it current for that one call, so pytest's recording picks it up whether the warning arrives directly or through ashowwarninghook that forwards it (numpy'ssuppress_warnings, #15139). When the call returns the verdict is gone, so a warning taken bypytest.warns, by the test's owncatch_warningsor by a hook that discards it leaves nothing behind for the next one. Without anerror_laterfilter, recording stays a plainlist.append.Relies on
Behaviour that is not documented, checked against the CPython 3.10–3.14 sources:
_warnings.c:check_matched(and the pure-Python loop) calls.match()on whatever object sits in a filter tuple's message and module slots; only the 5-tuple shape and astraction are validated.always, the warning is shown synchronously in the emitting thread, with no other filter search in between._warnings.c:call_show_warning(and the pure-Pythonwarn_explicit) looks upwarnings._showwarnmsgon every shown warning, so replacing it takes effect at once.catch_warningsdoes not restore it, so pytest puts it back when its own warnings context exits.warnings.filters(direct insertion),warnings._filters_mutated(),warnings._showwarnmsg,warnings._showwarnmsg_impl, and on 3.14warnings._get_filters()andwarnings._get_context().log.warnings.filters; they expose.patternbut not the rest ofre.Pattern.Verified on 3.10, 3.14, 3.14 with context-aware warnings and free-threaded 3.14t; 3.11–3.13 only through a standalone check of the mechanism. The
_showwarnmsgwrapper is verified on 3.11–3.14 and on 3.14 with context-aware warnings, not on 3.10 or 3.14t.Related
error_lateris silently dropped when a test wrapswarnings.showwarning#15139:error_laterwas dropped when a test wrapswarnings.showwarning.PytestWarninginstances indeprecated.pyaccumulate tracebacks under-W error#14912 / fix: stop reusing shared PytestWarning instances whose tracebacks grow under -W error #14931: independent of this PR. The shared-instance traceback growth is fixed there; this PR does not include a fix for it.error_laterfollows the order pytest applies filters in, so it inherits whatever Fix warning filter precedence across test scopes #15087 settles.Known gaps
pytest_sessionfinishon xdist workers are still lost.Not verified
The Sphinx build, PyPy, 3.13t, and interaction with
--pdband-x.