Skip to content

Warn when a collected test module was imported too early, and fix three cases where it is - #15022

Draft
RonnyPfannschmidt wants to merge 5 commits into
pytest-dev:mainfrom
RonnyPfannschmidt:fix-1930-early-import-rewrite
Draft

RonnyPfannschmidt wants to merge 5 commits into
pytest-dev:mainfrom
RonnyPfannschmidt:fix-1930-early-import-rewrite

Conversation

@RonnyPfannschmidt

Copy link
Copy Markdown
Member

AI-authored. I asked Claude Opus 5 (via Claude Code) to dig into #1930. The
investigation, the repros, the fixes, the tests and this text are the agent's
work; I read it and I am posting it. Commits carry Co-authored-by trailers.
I will answer review questions on it myself.

Closes #1930.

What the agent found

#1930 asked for a warning when assertion rewriting silently doesn't happen. Chasing
it turned up that the warning, once written, fires in three places on main where
rewriting is genuinely missing today — so the fixes come first and the warning last.

The original #1929 repro still reproduces, in the shape the gist actually had it:

# conftest.py
import foo

# foo.py  — only a test module because it is named on the command line
def test_compare():
    assert Foo(1) == Foo(2)
$ pytest foo.py
E       AssertionError          # no introspection, no warning

It works if the module is named test_*.py (_should_rewrite matches python_files
regardless of who imports first), which is why #1930 looked fixed in 2018.

The commits

  1. pytest pkg/__init__.py is never rewritten. _early_rewrite_bailout seeds
    _basenames_to_check_rewrite from each initial path's file stem, so an
    __init__.py contributes __init__ while the module is imported as pkg. The
    bailout fires and _should_rewrite — which would have said yes via isinitpath
    is never reached.

  2. --import-mode=importlib never rewrites a package's __init__.py.
    _import_module_using_spec asks sys.meta_path using module_path.parent; for
    pkg/sub/__init__.py that is pkg/sub, so PathFinder hunts for sub inside
    sub/ and finds nothing. The loop falls through to spec_from_file_location,
    which picks SourceFileLoader and bypasses the hook. Same tree under prepend
    gives assert 1 == 2; under importlib, bare AssertionError. Distinct from
    --import-mode=importlib breaks assertion rewriting #12044.

  3. --pyargs t.py imports the test module while resolving it. search_pypath
    hands the argument to importlib.util.find_spec, which imports everything up to
    the last dot as a package — for t.py that imports t. The lookup then fails and
    pytest falls back to the path, but the module is in sys.modules and can no
    longer be rewritten.

  4. The warning warn when tests raise plain assertion errors without annotation #1930 asked for. After importing a test module, check whether it
    came from the rewrite hook's loader; if not, but _should_rewrite says it should
    have, emit PytestAssertRewriteWarning naming both remedies.

    Re-asking _should_rewrite is what makes this usable. Without that gate the check
    fired ~150 times across pytest's own suite — every package __init__.py, every
    --doctest-modules target — for 87 failures. With it: 8 warnings, 5 failures,
    and all five were the three bugs above. With those fixed the suite is clean and
    the warning is silent.

  5. Docs. assert.rst said pytest rewrites the modules it discovers, without
    saying that an earlier import takes that away. The pytest_assertrepr_compare
    example there does the same top-level conftest import that bit the no output from pytest_assertrepr_compare in conftest #1929 reporter.

Notes for review

  • All four changelog fragments are named 1930.*; the three bugfixes have no issue
    numbers of their own. Happy to open separate issues and rename if you'd rather.
  • warn_if_not_rewritten reaches for hook._should_rewrite. It lives in
    _pytest/assertion/__init__.py, next to the hook, so it stays inside the package —
    but say the word if you'd rather that became a public method on the hook.
  • The --pyargs fix keys off a .py suffix. That matches the existing
    "looks like package module, but actually filename" comment and the documented
    contract (--pyargs takes module names), at the cost of no longer resolving a
    hypothetical real module named t.py.

🤖 Generated with Claude Code

@psf-chronographer psf-chronographer Bot added the bot:chronographer:provided (automation) changelog entry is part of PR label Sep 14, 2026
@RonnyPfannschmidt
RonnyPfannschmidt marked this pull request as draft September 14, 2026 09:33
RonnyPfannschmidt and others added 5 commits September 15, 2026 13:34
_early_rewrite_bailout seeds _basenames_to_check_rewrite from the initial
paths using the file stem, so `pytest pkg/__init__.py` contributed
`__init__`. The module is imported under the name `pkg`, whose last part
never matched, so the hook bailed out before _should_rewrite could
recognise the file as an initial path and rewrite it.

Seed the containing directory name as well when the initial path is an
__init__.py.

Co-Authored-By: Claude Opus 5 (1M context) via Claude Code <noreply@anthropic.com>
…tlib

_import_module_using_spec asks sys.meta_path for the module using the
module file's parent directory as the search path. For a package that is
the package directory itself, so PathFinder looked for `sub` inside
`sub/` and found nothing; the loop fell through to spec_from_file_location,
which picks SourceFileLoader and bypasses the assertion-rewrite hook.

Search the directory containing the package instead when the module path
is an __init__.py.

Co-Authored-By: Claude Opus 5 (1M context) via Claude Code <noreply@anthropic.com>
search_pypath passes the argument straight to importlib.util.find_spec.
For `--pyargs t.py` importlib imports everything up to the last dot as a
package, so `t` -- the test module itself -- lands in sys.modules before
collection starts. The lookup then fails and pytest falls back to treating
the argument as a path, but the module is now imported, and an imported
module can no longer be assertion-rewritten.

A name ending in `.py` is a filename, not a module name, so skip the
sys.path search for it entirely.

Co-Authored-By: Claude Opus 5 (1M context) via Claude Code <noreply@anthropic.com>
…arly

Rewriting only happens on import, so anything that imports a test module
before collection reaches it -- typically a top-level `import` in a
conftest -- silently costs that module its assertion introspection. The
existing "Module already imported so cannot be rewritten" warning only
covers modules passed to register_assert_rewrite, not this case.

After importing a test module, check whether it came from the rewrite
hook's loader. If it did not, but the hook would have rewritten it, warn
with PytestAssertRewriteWarning and name both remedies.

Re-asking _should_rewrite is what keeps this quiet: without it the check
fires for every package __init__.py and every --doctest-modules target,
neither of which is meant to be rewritten.

Closes pytest-dev#1930

Co-Authored-By: Claude Opus 5 (1M context) via Claude Code <noreply@anthropic.com>
The "Assertion introspection details" section said only that pytest
rewrites the modules it discovers, without saying that an import which
beats collection to the module takes that away. The conftest example
further up does exactly the top-level import that bit the reporter of
issue pytest-dev#1929, so note why it is safe there and what makes it unsafe
elsewhere.

Co-Authored-By: Claude Opus 5 (1M context) via Claude Code <noreply@anthropic.com>
@RonnyPfannschmidt
RonnyPfannschmidt force-pushed the fix-1930-early-import-rewrite branch from 532e7b8 to bb6fdf4 Compare September 15, 2026 11:34
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.

warn when tests raise plain assertion errors without annotation

1 participant