2727from importlib import import_module
2828from importlib .abc import MetaPathFinder
2929from itertools import starmap
30- from typing import Any
30+ from typing import Any , TypeVar
3131
3232from ._collections import FreezableDefaultDict , Pair
3333from ._compat import (
3434 NullFinder ,
3535 install ,
3636 localize ,
3737)
38- from ._context import ExceptionTrap
3938from ._functools import apply , compose , method_cache , noop , pass_none , passthrough
4039from ._itertools import always_iterable , bucket , unique_everseen
4140from ._meta import (
6766]
6867
6968
69+ _D = TypeVar ('_D' , bound = IDistribution )
70+
71+
7072class 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 , _safe_has_metadata )
535533 return itertools .chain (buckets [True ], buckets [False ])
536534
537535 @staticmethod
@@ -566,15 +564,37 @@ def metadata(self) -> PackageMetadata:
566564 :raises MetadataNotFound: If no metadata file is present.
567565 """
568566
569- text = (
567+ text = self ._metadata_text ()
568+ return self ._assemble_message (self ._ensure_metadata_present (text ))
569+
570+ def _metadata_text (self ) -> str | None :
571+ """
572+ The metadata text, if any.
573+
574+ Separate from ``metadata`` so that ``_has_metadata`` can name
575+ the same files without repeating them and without paying to
576+ parse what it finds.
577+ """
578+ return (
570579 self .read_text ('METADATA' )
571580 or self .read_text ('PKG-INFO' )
572581 # This last clause is here to support old egg-info files. Its
573582 # effect is to just end up using the PathDistribution's self._path
574583 # (which points to the egg-info file) attribute unchanged.
575584 or self .read_text ('' )
576585 )
577- return self ._assemble_message (self ._ensure_metadata_present (text ))
586+
587+ def _has_metadata (self ) -> bool :
588+ """
589+ Whether this distribution supplies metadata.
590+
591+ Reads the metadata, as the interface offers nothing cheaper:
592+ ``locate_file`` resolves against the package root and not the
593+ metadata, and every other member loads the metadata or parses
594+ it. A subclass able to detect the metadata without reading it
595+ should override this (python/importlib_metadata#534).
596+ """
597+ return bool (self ._metadata_text ())
578598
579599 @staticmethod
580600 def _assemble_message (text : str ) -> PackageMetadata :
@@ -1078,6 +1098,24 @@ def read_text(self, filename: str | os.PathLike[str]) -> str | None:
10781098
10791099 read_text .__doc__ = Distribution .read_text .__doc__
10801100
1101+ def _has_metadata (self ) -> bool :
1102+ """
1103+ Detect the metadata file without opening it
1104+ (python/importlib_metadata#534).
1105+
1106+ The last clause is for an old-style egg-info file, whose
1107+ metadata is the distribution's own path; a metadata directory
1108+ is not itself metadata. ``SimplePath`` doesn't name
1109+ ``is_file``, but only these legacy paths reach that clause and
1110+ they arrive as a ``pathlib.Path`` or a ``zipfile.Path``, both
1111+ of which supply it.
1112+ """
1113+ return (
1114+ self ._path .joinpath ('METADATA' ).exists ()
1115+ or self ._path .joinpath ('PKG-INFO' ).exists ()
1116+ or self ._path .is_file () # type: ignore[attr-defined]
1117+ )
1118+
10811119 def locate_file (self , path : str | os .PathLike [str ]) -> SimplePath :
10821120 return self ._path .parent / path
10831121
@@ -1148,6 +1186,25 @@ def version(distribution_name: str) -> str:
11481186 return distribution (distribution_name ).version
11491187
11501188
1189+ def _safe_has_metadata (dist : IDistribution ) -> bool :
1190+ """
1191+ ``dist._has_metadata()``, for a distribution that may not supply it.
1192+
1193+ A provider's own ``Distribution`` subclass derives from
1194+ ``importlib.metadata`` and is passed through unadapted (#486), so
1195+ it has none; read its metadata as the default would. It supplies
1196+ no ``_metadata_text`` either, hence the unbound call.
1197+
1198+ Such a distribution cannot be asked for ``metadata`` behind a trap
1199+ instead. The ``MetadataNotFound`` it raises is the one from
1200+ ``importlib.metadata``, a different class from this package's,
1201+ which no trap of ours would catch.
1202+ """
1203+ with suppress (AttributeError ):
1204+ return dist ._has_metadata () # type: ignore[attr-defined]
1205+ return bool (Distribution ._metadata_text (dist )) # type: ignore[arg-type]
1206+
1207+
11511208_unique = functools .partial (
11521209 unique_everseen ,
11531210 key = operator .attrgetter ('_normalized_name' ),
@@ -1167,7 +1224,8 @@ def entry_points(**params) -> EntryPoints:
11671224 :return: EntryPoints for all installed packages.
11681225 """
11691226 eps = itertools .chain .from_iterable (
1170- dist .entry_points for dist in _unique (distributions ())
1227+ dist .entry_points
1228+ for dist in _unique (Distribution ._prefer_valid (distributions ()))
11711229 )
11721230 return EntryPoints (eps ).select (** params )
11731231
0 commit comments