Skip to content

Commit 381cc29

Browse files
jaracoclaude
andcommitted
Detect the presence of metadata without reading it.
`_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 `PathDistribution` a `_has_metadata` that detects the metadata file without opening it, and make it the preference's predicate. The interface offers nothing as cheap for a distribution that isn't one of ours: `locate_file` resolves against the package root rather than the metadata, and every other member loads the metadata or parses it. So those fall back to `read_text`, which also closes a gap, as the `MetadataNotFound` of a distribution deriving from `importlib.metadata` is a different class and escaped the preference rather than marking the distribution invalid. The files that fallback reads are named once, in `_metadata_text`, which `metadata` now uses too, so neither repeats the other. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 60bbaca commit 381cc29

4 files changed

Lines changed: 89 additions & 16 deletions

File tree

‎importlib_metadata/__init__.py‎

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,14 @@
2727
from importlib import import_module
2828
from importlib.abc import MetaPathFinder
2929
from itertools import starmap
30-
from typing import Any
30+
from typing import Any, TypeVar
3131

3232
from ._collections import FreezableDefaultDict, Pair
3333
from ._compat import (
3434
NullFinder,
3535
install,
3636
localize,
3737
)
38-
from ._context import ExceptionTrap
3938
from ._functools import apply, compose, method_cache, noop, pass_none, passthrough
4039
from ._itertools import always_iterable, bucket, unique_everseen
4140
from ._meta import (
@@ -67,6 +66,9 @@
6766
]
6867

6968

69+
_D = TypeVar('_D', bound=IDistribution)
70+
71+
7072
class PackageNotFoundError(ModuleNotFoundError):
7173
"""The package was not found."""
7274

@@ -520,18 +522,14 @@ def discover(
520522
)
521523

522524
@staticmethod
523-
def _prefer_valid(dists: Iterable[Distribution]) -> Iterable[Distribution]:
525+
def _prefer_valid(dists: Iterable[_D]) -> Iterable[_D]:
524526
"""
525527
Prefer (move to the front) distributions that have metadata.
526528
527529
Ref python/importlib_resources#489.
528530
"""
529531

530-
has_metadata = ExceptionTrap(MetadataNotFound).passes(
531-
operator.attrgetter('metadata')
532-
)
533-
534-
buckets = bucket(dists, has_metadata)
532+
buckets = bucket(dists, _has_metadata)
535533
return itertools.chain(buckets[True], buckets[False])
536534

537535
@staticmethod
@@ -566,14 +564,7 @@ def metadata(self) -> PackageMetadata:
566564
:raises MetadataNotFound: If no metadata file is present.
567565
"""
568566

569-
text = (
570-
self.read_text('METADATA')
571-
or self.read_text('PKG-INFO')
572-
# This last clause is here to support old egg-info files. Its
573-
# effect is to just end up using the PathDistribution's self._path
574-
# (which points to the egg-info file) attribute unchanged.
575-
or self.read_text('')
576-
)
567+
text = _metadata_text(self)
577568
return self._assemble_message(self._ensure_metadata_present(text))
578569

579570
@staticmethod
@@ -1078,6 +1069,22 @@ def read_text(self, filename: str | os.PathLike[str]) -> str | None:
10781069

10791070
read_text.__doc__ = Distribution.read_text.__doc__
10801071

1072+
@property
1073+
def _has_metadata(self) -> bool:
1074+
"""
1075+
Detect the metadata file without opening it
1076+
(python/importlib_metadata#534).
1077+
1078+
An old-style egg-info file, whose metadata is the
1079+
distribution's own path, is read; there is nothing to detect
1080+
without reading it.
1081+
"""
1082+
return (
1083+
self._path.joinpath('METADATA').exists()
1084+
or self._path.joinpath('PKG-INFO').exists()
1085+
or bool(self.read_text(''))
1086+
)
1087+
10811088
def locate_file(self, path: str | os.PathLike[str]) -> SimplePath:
10821089
return self._path.parent / path
10831090

@@ -1148,6 +1155,45 @@ def version(distribution_name: str) -> str:
11481155
return distribution(distribution_name).version
11491156

11501157

1158+
def _metadata_text(dist: IDistribution) -> str | None:
1159+
"""
1160+
The metadata text for ``dist``, if any.
1161+
1162+
Shared by ``Distribution.metadata`` and the fallback in
1163+
``_has_metadata``, so that the one names the files to read and the
1164+
other needn't repeat them.
1165+
"""
1166+
return (
1167+
dist.read_text('METADATA')
1168+
or dist.read_text('PKG-INFO')
1169+
# This last clause is here to support old egg-info files. Its
1170+
# effect is to just end up using the PathDistribution's self._path
1171+
# (which points to the egg-info file) attribute unchanged.
1172+
or dist.read_text('')
1173+
)
1174+
1175+
1176+
def _has_metadata(dist: IDistribution) -> bool:
1177+
"""
1178+
Whether ``dist`` supplies metadata.
1179+
1180+
A ``PathDistribution`` detects the metadata without reading it. For
1181+
any other distribution, including a provider's own subclass passed
1182+
through unadapted (#486), the interface offers nothing cheaper than
1183+
``read_text``; ``locate_file`` resolves against the package root and
1184+
not the metadata, and every other member loads the metadata or
1185+
parses it.
1186+
1187+
Reading is also what makes the answer reliable. Asking such a
1188+
distribution for ``metadata`` raises the ``MetadataNotFound`` of
1189+
``importlib.metadata``, a different class from this package's,
1190+
which no trap of ours would catch.
1191+
"""
1192+
with suppress(AttributeError):
1193+
return dist._has_metadata # type: ignore[attr-defined]
1194+
return bool(_metadata_text(dist))
1195+
1196+
11511197
_unique = functools.partial(
11521198
unique_everseen,
11531199
key=operator.attrgetter('_normalized_name'),
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
The preference for valid distributions no longer propagates the
2+
``MetadataNotFound`` of a distribution that a provider derived from
3+
``importlib.metadata``, whose exception is a different class from this
4+
package's and so escaped the preference rather than marking the distribution
5+
invalid.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Detecting whether a distribution supplies metadata no longer reads or parses
2+
that metadata where the metadata directory can be inspected instead, so
3+
retrieving a distribution by name, which prefers the valid candidates, is
4+
substantially faster.

‎tests/compat/test_localize.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,24 @@ def test_conforming_provider_is_not_warned_about(self):
119119
assert distribution('distinfo-pkg').version == '1.0.0'
120120
assert not [str(warning.message) for warning in caught]
121121

122+
def test_dist_without_metadata_is_deferred(self):
123+
"""
124+
A provider's own distribution supplies no detection of its own,
125+
so the metadata is sought through ``read_text``. Asking for
126+
``metadata`` instead would raise, as the ``MetadataNotFound``
127+
of a distribution deriving from ``importlib.metadata`` is a
128+
different class.
129+
"""
130+
131+
class Empty(stdlib.Distribution):
132+
def read_text(self, filename):
133+
return None
134+
135+
def locate_file(self, path):
136+
return None
137+
138+
assert not importlib_metadata._has_metadata(Empty())
139+
122140

123141
class ConformanceCheckTests(unittest.TestCase):
124142
@unittest.skipIf(

0 commit comments

Comments
 (0)