Skip to content

recwarn: pass originating module name when re-emitting unmatched warnings (fixes #11933)#14585

Open
mokashang wants to merge 2 commits into
pytest-dev:mainfrom
mokashang:fix/recwarn-reemit-module
Open

recwarn: pass originating module name when re-emitting unmatched warnings (fixes #11933)#14585
mokashang wants to merge 2 commits into
pytest-dev:mainfrom
mokashang:fix/recwarn-reemit-module

Conversation

@mokashang

Copy link
Copy Markdown

Problem

pytest.warns re-emits unmatched warnings on __exit__ via
warnings.warn_explicit(..., module=w.__module__, ...). But w is a
warnings.WarningMessage instance, so w.__module__ evaluates to
"warnings" (the module that the WarningMessage class is defined in) for
every re-emitted warning, regardless of where the warning actually came from.

The visible consequence: a user-installed filter such as

warnings.filterwarnings("ignore", category=UserWarning, module=r"my_module")

matches the original warning fine, but silently fails to match its re-emit.
Code that should be quiet around pytest.warns(...) blocks gets noisy
(or, with "error" filters, raises unexpectedly).

This is #11933, reported by @Zac-HD with a precise diagnosis: module=w.__module__
should be the importable module name of the call site, not the home module of
WarningMessage.

Approach

WarningMessage does not preserve the module argument originally passed to
warn_explicit, so we can't read it off w. The next best thing is to look up
sys.modules by __file__ to find the module whose source file matches
w.filename, and fall back to None when nothing matches. warn_explicit's
documented behavior with module=None is to derive a name from filename
itself, which matches what plain warnings.warn(...) would have produced if
called from outside any pytest.warns context — so the fallback is a clean
degenerate, not a regression.

The fix is contained to two spots in src/_pytest/recwarn.py: a helper
_module_name_for_filename, and replacing the module=w.__module__ argument
in the re-emit call.

Why this PR re-opens ground covered by #12898

The earlier attempt (#12898, by @reaganjlee, closed for inactivity) landed
on essentially the same shape of fix but was closed because the regression test
@nicoddemus asked for was inadequate — specifically, the test passed even when
module= was set to None instead of the recovered name, so it pinned the
wrong invariant.

This PR addresses that directly. The new test
test_re_emit_preserves_module_name uses a packaged module
(regr_pkg/inner.py) so the importable name (regr_pkg.inner) differs from
warn_explicit's filename-derived default (<...>/regr_pkg/inner). The
filter regex ^regr_pkg\.inner$ matches only the importable form and
nothing else. Concretely, the test:

  • passes with the fix (module recovered to "regr_pkg.inner"),
  • fails with the original bug (module "warnings" doesn't match),
  • fails with module=None (filename-derived "<...>/regr_pkg/inner" doesn't match).

So the test pins down "recover the correct module name", not the looser
"don't pass \"warnings\"".

Verification

  • testing/test_recwarn.py — 67 passed (66 existing + 1 new)
  • testing/test_recwarn.py testing/test_warnings.py — 119 passed, 4 skipped
  • Full testing/ suite (excluding test_recwarn.py, kept separate above) —
    3974 passed, 124 skipped, 12 xfailed, 1 xpassed; no failures introduced.
  • ruff check, ruff format --check, mypy on the touched files — all clean.

I also confirmed end-to-end by reverting the fix locally and re-running the
new test: it fails as designed. With the module=None "looser" fix it also
fails, which is what nicoddemus wanted from a regression test.

Fixes #11933.

…ings

The re-emit loop in WarningsChecker.__exit__ passed
module=w.__module__ to warnings.warn_explicit, but w is a
warnings.WarningMessage instance, so w.__module__ is always
the string "warnings" (the module that WarningMessage is defined
in). As a result, a user-installed filter such as
warnings.filterwarnings("ignore", module=r"my_module") never matches
the re-emitted warning, even though it matches the original.

Recover the originating module's importable name by looking it up in
sys.modules by filename. Fall back to None when no module
matches, which lets warn_explicit derive the name from the filename
itself (matching its own default behavior).

Add a regression test using a packaged module so the importable name
(regr_pkg.inner) differs from the filename-derived default. The
test's module-anchored regex matches only the correct importable name
and fails for both the original buggy value ("warnings") and the
filename-derived fallback, ensuring the test pins the fix down rather
than the looser invariant.

Fixes pytest-dev#11933
@psf-chronographer psf-chronographer Bot added the bot:chronographer:provided (automation) changelog entry is part of PR label Jun 12, 2026
The regression test for pytest-dev#11933 writes a small helper module via
Path.write_text without specifying an encoding.  pytest's own CI
matrix runs with PYTHONWARNDEFAULTENCODING=1 (set in tox.ini for
all environments except pylib), and the project's filterwarnings
config treats every warning as an error.  The unencoded write_text
triggers EncodingWarning, failing the test on every Python 3.10+
job that respects that env var (all ubuntu-py311+, macos, windows).

Matches the convention already used elsewhere in testing/ (see
test_assertion.py, test_assertrewrite.py, etc.).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:chronographer:provided (automation) changelog entry is part of PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

recwarn: warnings are re-emitted with wrong module

1 participant