Skip to content

Mark editable-installed plugins for assertion rewriting - #15040

Open
GangEunzzang wants to merge 4 commits into
pytest-dev:mainfrom
GangEunzzang:fix/mark-editable-plugins-for-rewrite
Open

GangEunzzang wants to merge 4 commits into
pytest-dev:mainfrom
GangEunzzang:fix/mark-editable-plugins-for-rewrite

Conversation

@GangEunzzang

Copy link
Copy Markdown

Problem

_mark_plugins_for_rewrite() decides which modules to mark for assertion rewriting by walking the Python files recorded in each plugin distribution's metadata. A PEP 660 editable install doesn't record them — dist.files only holds the __editable__*.pth shim and the dist-info entries — so nothing matches and the plugin never gets marked.

The effect is that assertions inside a plugin stop being rewritten as soon as it is installed with pip install -e ., which is the usual way to work on one. With a plugin that asserts in its own helpers:

# myplug/helper.py
def compare(got, want):
    assert got == want
# pip install .
E       AssertionError: assert {'a': 1, 'b': 2} == {'a': 1, 'b': 3}

# pip install -e .
E       AssertionError

Fix

When a distribution is installed in editable mode and its recorded files yield no rewritable module, fall back to the top-level package of its pytest11 entry points. Editable mode is read from direct_url.json (PEP 610), so regular installs keep taking the existing file-based path untouched.

The direct_url.json gate matters. Without it the fallback also fires for distributions that simply record no files, which a number of tests in the suite construct, and 13 of them change behaviour.

Tests

The new test covers three distributions: an editable one, a regular install from a local directory, and one installed from an index with no direct_url.json. Only the editable case fails on main.

Fixes #11783

A PEP 660 editable install does not record the package's Python files in
the distribution metadata, so _mark_plugins_for_rewrite() found no module
to mark and assertions inside the plugin were no longer rewritten.

Fall back to the top-level package of the pytest11 entry points when a
distribution is installed in editable mode and its recorded files yield
nothing. Editable mode is read from direct_url.json.

Fixes pytest-dev#11783
@psf-chronographer psf-chronographer Bot added the bot:chronographer:provided (automation) changelog entry is part of PR label Sep 16, 2026
Use typing.cast for the recording hook passed to
_mark_plugins_for_rewrite(), matching the pattern used elsewhere in the
test suite.
Comment thread src/_pytest/config/__init__.py Outdated
names = {
top_level
for ep in entry_points
if (top_level := ep.value.partition(":")[0].strip().split(".")[0])

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

potential edge-case - this might mark all of a set of namespace packages intead of just the one with the plugin

i'like to see this validated, but i recon its pretty tricky to create the conditions

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RonnyPfannschmidt Thanks for the review — you were right.

I set the conditions up with two distributions sharing a PEP 420 namespace: nsplug installed editable with a myns.plug.plugin entry point, and an unrelated nsother installed normally. myns did get marked, so an assertion inside myns/other/helper.py came out rewritten:

# main
E       AssertionError

# this branch, before the fix
E       AssertionError: assert {'a': 1} == {'a': 2}

The fallback now walks the entry point module and takes the outermost package that isn't a namespace package, so it stops at myns.plug and leaves myns.other alone. It resolves through PathFinder rather than importlib.util.find_spec, since that one imports the parent package, which would defeat the point of marking it.

Added a unit test covering both shapes.

Marking the top-level name of an entry point rewrites every distribution
sharing that name when it is a namespace package. Walk the entry point
module instead and take the outermost package that is not a namespace
package.

PathFinder is used rather than importlib.util.find_spec because the
latter imports the parent package, which has to stay unimported until it
is marked.
PathFinder.find_spec returns None for names it cannot resolve rather
than raising, and Distribution.read_text already suppresses the errors
raised by a missing metadata file, so neither guard could be reached.
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.

Assertion rewriting issue with editable-installed plugins

2 participants