Skip to content

Prefer valid distributions when collecting entry points - #553

Merged
jaraco merged 2 commits into
mainfrom
bugfix/534-entry-points-prefer-valid
Oct 23, 2026
Merged

jaraco merged 2 commits into
mainfrom
bugfix/534-entry-points-prefer-valid

Conversation

@jaraco

@jaraco jaraco commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Closes #534.

Built on the #550 revert (#552), now on main.

The fix

entry_points() fed distributions() straight to _unique(), which keeps the first distribution seen for each normalized name. Where an unclean upgrade leaves an empty foo-1.0.dist-info beside the valid foo-1.1.dist-info, the empty one could be listed first, and with it the valid distribution's entry points disappeared entirely.

_prefer_valid already existed for exactly this preference, used by from_name. Applying it ahead of _unique in entry_points() is the whole of the fix.

Detecting metadata rather than loading it

_prefer_valid decided validity by consulting metadata, which runs the whole METADATA file through the email parser and then discards the result. Applied to every distribution rather than to one, that is the cost @jaraco anticipated on the issue — measured, entry_points() went from 1.53 msec to 13.4 msec, 8.7x.

So PathDistribution grows a _has_metadata that detects the metadata file without opening it, and that is the preference's predicate. The remaining 29% on entry_points() buys a large improvement in retrieval by name, which no longer parses the metadata of the distribution it returns twice:

benchmark before after
entry_points() 1.53 msec 2.03 msec (+29%)
discovery 138 µsec 15.5 µsec (−89%)
cached distribution 138 µsec 15.6 µsec (−89%)
uncached distribution 236 µsec 107 µsec (−55%)

Distributions that aren't ours

localize.dist passes a provider's own Distribution subclass through unadapted (#486), so it has no _has_metadata and test_conforming_provider_is_not_warned_about caught that directly.

Nothing on IDistribution is as cheap for those: locate_file returns a SimplePath, which does have exists(), but it resolves against the package root rather than the metadata, so locate_file('METADATA') names site-packages/METADATA. Every other member loads the metadata or parses it. So Distribution._has_metadata() reads, through a _metadata_text that metadata shares so neither names the files twice, and PathDistribution overrides it with the detection above. The predicate is _safe_has_metadata, which falls back for a distribution supplying neither helper.

That turns out to fix something pre-existing. Asking such a distribution for metadata raises importlib.metadata.MetadataNotFound, a different class from this package's, so it escaped ExceptionTrap(MetadataNotFound) on main and propagated out of from_name instead of marking the distribution invalid. test_dist_without_metadata_is_deferred covers it.

Tests

  • test_entry_points_from_valid_dist — fails before the fix with assert set() == {'ep'}. It brackets the valid distribution between two invalid ones, as test_valid_dists_preferred does, so that no file system lists it first.
  • test_invalid_dists_deferred — the invalid distributions are moved to the end, not dropped.
  • test_dist_without_metadata_is_deferred — the foreign-MetadataNotFound case above.

tox passes on each commit separately: 260 passed, 1 skipped, 41 subtests; mypy and ruff clean.

🤖 Generated with Claude Code

Comment thread importlib_metadata/__init__.py
Comment thread importlib_metadata/__init__.py Outdated
@jaraco
jaraco force-pushed the bugfix/534-entry-points-prefer-valid branch from 64141d1 to 6a39aee Compare October 23, 2026 22:25
Comment thread importlib_metadata/__init__.py Outdated
Comment thread importlib_metadata/__init__.py Outdated
Comment thread importlib_metadata/__init__.py Outdated
@jaraco
jaraco force-pushed the bugfix/534-entry-points-prefer-valid branch 3 times, most recently from f8fa1c2 to 86e2d3a Compare October 23, 2026 23:02
Comment thread importlib_metadata/__init__.py Outdated
return distribution(distribution_name).version


def _metadata_text(dist: IDistribution) -> str | None:

@jaraco jaraco Oct 23, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Can this be an instance method?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes — _metadata_text is now Distribution._metadata_text(self), and metadata calls self._metadata_text().

Comment thread importlib_metadata/__init__.py Outdated
"""
with suppress(AttributeError):
return dist._has_metadata # type: ignore[attr-defined]
return bool(_metadata_text(dist))

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

What about instead having Distribution._has_metadata(), supplying the default behavior, have it overridden in PathDistribution, then this function is _safe_has_metadata, which suppresses the AttributeError for IDistributions not derived from Distribution?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done, that reads better. Distribution._has_metadata() supplies the default, PathDistribution overrides it with the detection, and the predicate is _safe_has_metadata.

One wrinkle worth knowing: the fallback cannot be Distribution._has_metadata(dist), because its body is self._metadata_text(), which dispatches on the instance again and a foreign distribution has no _metadata_text either — test_conforming_provider_is_not_warned_about failed on exactly that. So the fallback is bool(Distribution._metadata_text(dist)), unbound, which needs nothing of the distribution beyond read_text.

@jaraco
jaraco force-pushed the bugfix/534-entry-points-prefer-valid branch from 86e2d3a to eba46e3 Compare October 23, 2026 23:21
@jaraco

jaraco commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

I think this is ready to go. I'm going to sleep on it before merging.

@jaraco

jaraco commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

On second thought, let's get it out; we can iterate later if needed.

jaraco and others added 2 commits October 23, 2026 16:31
`_prefer_valid` decided validity by consulting `metadata`, which runs the
whole METADATA file through the email parser only for the result to be
discarded. Give `Distribution` a `_has_metadata` that reads no further than
the text, through a `_metadata_text` that `metadata` shares so neither names
the files twice, and override it in `PathDistribution` to detect the
metadata file without opening it at all. An old-style egg-info file, which
is itself the metadata, is detected with `is_file`; a metadata directory
exists whether or not it holds anything, so its own presence says nothing.

The predicate is `_safe_has_metadata`, since a provider's own `Distribution`
subclass derives from `importlib.metadata` and is passed through unadapted
(#486), so it supplies neither helper. Reading its metadata rather than
asking it for `metadata` also closes a gap: the `MetadataNotFound` such a
distribution raises is a different class from this package's, so it escaped
the preference rather than marking the distribution invalid.

Co-Authored-By: Claude Opus 5 <[email protected]>
Where an unclean upgrade leaves an empty metadata directory beside the valid
one, `_unique` kept whichever the file system happened to list first, so the
entry points of the valid distribution could go missing entirely.

Closes #534.

Co-Authored-By: Claude Opus 5 <[email protected]>
@jaraco
jaraco force-pushed the bugfix/534-entry-points-prefer-valid branch from eba46e3 to 0aa24f3 Compare October 23, 2026 23:32
@jaraco
jaraco merged commit 5d253ae into main Oct 23, 2026
30 of 32 checks passed
@jaraco
jaraco deleted the bugfix/534-entry-points-prefer-valid branch October 23, 2026 23:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

entry_points doesn't handle empty .dist-info files well

1 participant