Repository navigation
Prefer valid distributions when collecting entry points - #553
Conversation
64141d1 to
6a39aee
Compare
f8fa1c2 to
86e2d3a
Compare
| return distribution(distribution_name).version | ||
|
|
||
|
|
||
| def _metadata_text(dist: IDistribution) -> str | None: |
There was a problem hiding this comment.
Can this be an instance method?
There was a problem hiding this comment.
Yes — _metadata_text is now Distribution._metadata_text(self), and metadata calls self._metadata_text().
| """ | ||
| with suppress(AttributeError): | ||
| return dist._has_metadata # type: ignore[attr-defined] | ||
| return bool(_metadata_text(dist)) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
86e2d3a to
eba46e3
Compare
|
I think this is ready to go. I'm going to sleep on it before merging. |
|
On second thought, let's get it out; we can iterate later if needed. |
`_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]>
eba46e3 to
0aa24f3
Compare
Closes #534.
Built on the #550 revert (#552), now on main.
The fix
entry_points()feddistributions()straight to_unique(), which keeps the first distribution seen for each normalized name. Where an unclean upgrade leaves an emptyfoo-1.0.dist-infobeside the validfoo-1.1.dist-info, the empty one could be listed first, and with it the valid distribution's entry points disappeared entirely._prefer_validalready existed for exactly this preference, used byfrom_name. Applying it ahead of_uniqueinentry_points()is the whole of the fix.Detecting metadata rather than loading it
_prefer_validdecided validity by consultingmetadata, 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
PathDistributiongrows a_has_metadatathat detects the metadata file without opening it, and that is the preference's predicate. The remaining 29% onentry_points()buys a large improvement in retrieval by name, which no longer parses the metadata of the distribution it returns twice:entry_points()discoverycached distributionuncached distributionDistributions that aren't ours
localize.distpasses a provider's ownDistributionsubclass through unadapted (#486), so it has no_has_metadataandtest_conforming_provider_is_not_warned_aboutcaught that directly.Nothing on
IDistributionis as cheap for those:locate_filereturns aSimplePath, which does haveexists(), but it resolves against the package root rather than the metadata, solocate_file('METADATA')namessite-packages/METADATA. Every other member loads the metadata or parses it. SoDistribution._has_metadata()reads, through a_metadata_textthatmetadatashares so neither names the files twice, andPathDistributionoverrides 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
metadataraisesimportlib.metadata.MetadataNotFound, a different class from this package's, so it escapedExceptionTrap(MetadataNotFound)on main and propagated out offrom_nameinstead of marking the distribution invalid.test_dist_without_metadata_is_deferredcovers it.Tests
test_entry_points_from_valid_dist— fails before the fix withassert set() == {'ep'}. It brackets the valid distribution between two invalid ones, astest_valid_dists_preferreddoes, 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-MetadataNotFoundcase above.toxpasses on each commit separately: 260 passed, 1 skipped, 41 subtests; mypy and ruff clean.🤖 Generated with Claude Code