|
27 | 27 | from importlib import import_module |
28 | 28 | from importlib.abc import MetaPathFinder |
29 | 29 | from itertools import starmap |
30 | | -from typing import Any |
| 30 | +from typing import Any, TypeVar |
31 | 31 |
|
32 | 32 | from ._collections import FreezableDefaultDict, Pair |
33 | 33 | from ._compat import ( |
34 | 34 | NullFinder, |
35 | 35 | install, |
36 | 36 | localize, |
37 | 37 | ) |
38 | | -from ._context import ExceptionTrap |
39 | 38 | from ._functools import apply, compose, method_cache, noop, pass_none, passthrough |
40 | 39 | from ._itertools import always_iterable, bucket, unique_everseen |
41 | 40 | from ._meta import ( |
|
67 | 66 | ] |
68 | 67 |
|
69 | 68 |
|
| 69 | +_D = TypeVar('_D', bound=IDistribution) |
| 70 | + |
| 71 | + |
70 | 72 | class PackageNotFoundError(ModuleNotFoundError): |
71 | 73 | """The package was not found.""" |
72 | 74 |
|
@@ -520,18 +522,14 @@ def discover( |
520 | 522 | ) |
521 | 523 |
|
522 | 524 | @staticmethod |
523 | | - def _prefer_valid(dists: Iterable[Distribution]) -> Iterable[Distribution]: |
| 525 | + def _prefer_valid(dists: Iterable[_D]) -> Iterable[_D]: |
524 | 526 | """ |
525 | 527 | Prefer (move to the front) distributions that have metadata. |
526 | 528 |
|
527 | 529 | Ref python/importlib_resources#489. |
528 | 530 | """ |
529 | 531 |
|
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) |
535 | 533 | return itertools.chain(buckets[True], buckets[False]) |
536 | 534 |
|
537 | 535 | @staticmethod |
@@ -566,15 +564,37 @@ def metadata(self) -> PackageMetadata: |
566 | 564 | :raises MetadataNotFound: If no metadata file is present. |
567 | 565 | """ |
568 | 566 |
|
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 ( |
570 | 579 | self.read_text('METADATA') |
571 | 580 | or self.read_text('PKG-INFO') |
572 | 581 | # This last clause is here to support old egg-info files. Its |
573 | 582 | # effect is to just end up using the PathDistribution's self._path |
574 | 583 | # (which points to the egg-info file) attribute unchanged. |
575 | 584 | or self.read_text('') |
576 | 585 | ) |
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()) |
578 | 598 |
|
579 | 599 | @staticmethod |
580 | 600 | def _assemble_message(text: str) -> PackageMetadata: |
@@ -1078,6 +1098,24 @@ def read_text(self, filename: str | os.PathLike[str]) -> str | None: |
1078 | 1098 |
|
1079 | 1099 | read_text.__doc__ = Distribution.read_text.__doc__ |
1080 | 1100 |
|
| 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 | + |
1081 | 1119 | def locate_file(self, path: str | os.PathLike[str]) -> SimplePath: |
1082 | 1120 | return self._path.parent / path |
1083 | 1121 |
|
@@ -1148,6 +1186,25 @@ def version(distribution_name: str) -> str: |
1148 | 1186 | return distribution(distribution_name).version |
1149 | 1187 |
|
1150 | 1188 |
|
| 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 | + |
1151 | 1208 | _unique = functools.partial( |
1152 | 1209 | unique_everseen, |
1153 | 1210 | key=operator.attrgetter('_normalized_name'), |
|
0 commit comments