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.

@RonnyPfannschmidt RonnyPfannschmidt left a comment

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.

thanks

This branch has not been deployed

No deployments
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