Skip to content

Commit 294c0df

Browse files
jaracoclaude
andcommitted
gh-160386: Apply changes from importlib_metadata 10.0
Adds the IDistribution and IPackagePath protocols, so a consumer can target one interface across this module and the backport rather than either library's concrete classes, and annotates the functions returning distributions and files with them. The backport's compat and _compat packages are excluded from the port as usual, so localize and the conformance warning it raises do not appear here; only the protocols they are defined against do. This is what the caveats documented for gh-123976 in this branch describe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent ce987c6 commit 294c0df

4 files changed

Lines changed: 134 additions & 15 deletions

File tree

‎Lib/importlib/metadata/__init__.py‎

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,22 @@
2929
from itertools import starmap
3030
from typing import Any
3131

32-
from . import _meta
3332
from ._collections import FreezableDefaultDict, Pair
3433
from ._context import ExceptionTrap
3534
from ._functools import method_cache, noop, pass_none, passthrough
3635
from ._itertools import always_iterable, bucket, unique_everseen
37-
from ._meta import PackageMetadata, SimplePath
36+
from ._meta import (
37+
IDistribution,
38+
IPackagePath,
39+
PackageMetadata,
40+
SimplePath,
41+
)
3842

3943
__all__ = [
4044
'Distribution',
4145
'DistributionFinder',
46+
'IDistribution',
47+
'IPackagePath',
4248
'MetadataNotFound',
4349
'PackageMetadata',
4450
'PackageNotFoundError',
@@ -210,7 +216,7 @@ class EntryPoint:
210216
value: str
211217
group: str
212218

213-
dist: Distribution | None = None
219+
dist: IDistribution | None = None
214220

215221
def __init__(self, name: str, value: str, group: str) -> None:
216222
vars(self).update(name=name, value=value, group=group)
@@ -375,7 +381,7 @@ class PackagePath(pathlib.PurePosixPath):
375381

376382
hash: FileHash | None
377383
size: int
378-
dist: Distribution
384+
dist: IDistribution
379385

380386
def read_text(self, encoding: str = 'utf-8') -> str:
381387
return self.locate().read_text(encoding=encoding)
@@ -538,7 +544,7 @@ def _discover_resolvers():
538544
return filter(None, declared)
539545

540546
@property
541-
def metadata(self) -> _meta.PackageMetadata:
547+
def metadata(self) -> PackageMetadata:
542548
"""Return the parsed metadata for this Distribution.
543549
544550
The returned object will have keys that name the various bits of
@@ -562,7 +568,7 @@ def metadata(self) -> _meta.PackageMetadata:
562568
return self._assemble_message(self._ensure_metadata_present(text))
563569

564570
@staticmethod
565-
def _assemble_message(text: str) -> _meta.PackageMetadata:
571+
def _assemble_message(text: str) -> PackageMetadata:
566572
# deferred for performance (python/cpython#109829)
567573
from . import _adapters
568574

@@ -602,10 +608,10 @@ def entry_points(self) -> EntryPoints:
602608
return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self)
603609

604610
@property
605-
def files(self) -> list[PackagePath] | None:
611+
def files(self) -> list[IPackagePath] | None:
606612
"""Files in this distribution.
607613
608-
:return: List of PackagePath for this distribution or None
614+
:return: List of PackagePath-like objects for this distribution or None
609615
610616
Result is `None` if the metadata file that enumerates files
611617
(i.e. RECORD for dist-info, or installed-files.txt or
@@ -1096,7 +1102,7 @@ def _name_from_stem(stem):
10961102
return name
10971103

10981104

1099-
def distribution(distribution_name: str) -> Distribution:
1105+
def distribution(distribution_name: str) -> IDistribution:
11001106
"""Get the ``Distribution`` instance for the named package.
11011107
11021108
:param distribution_name: The name of the distribution package as a string.
@@ -1105,15 +1111,15 @@ def distribution(distribution_name: str) -> Distribution:
11051111
return Distribution.from_name(distribution_name)
11061112

11071113

1108-
def distributions(**kwargs) -> Iterable[Distribution]:
1114+
def distributions(**kwargs) -> Iterable[IDistribution]:
11091115
"""Get all ``Distribution`` instances in the current environment.
11101116
11111117
:return: An iterable of ``Distribution`` instances.
11121118
"""
11131119
return Distribution.discover(**kwargs)
11141120

11151121

1116-
def metadata(distribution_name: str) -> _meta.PackageMetadata:
1122+
def metadata(distribution_name: str) -> PackageMetadata:
11171123
"""Get the metadata for the named package.
11181124
11191125
:param distribution_name: The name of the distribution package to query.
@@ -1157,7 +1163,7 @@ def entry_points(**params) -> EntryPoints:
11571163
return EntryPoints(eps).select(**params)
11581164

11591165

1160-
def files(distribution_name: str) -> list[PackagePath] | None:
1166+
def files(distribution_name: str) -> list[IPackagePath] | None:
11611167
"""Return a list of files for the named package.
11621168
11631169
:param distribution_name: The name of the distribution package to query.
@@ -1197,15 +1203,15 @@ def _top_level_declared(dist):
11971203
return (dist.read_text('top_level.txt') or '').split()
11981204

11991205

1200-
def _topmost(name: PackagePath) -> str | None:
1206+
def _topmost(name: IPackagePath) -> str | None:
12011207
"""
12021208
Return the top-most parent as long as there is a parent.
12031209
"""
12041210
top, *rest = name.parts
12051211
return top if rest else None
12061212

12071213

1208-
def _get_toplevel_name(name: PackagePath) -> str:
1214+
def _get_toplevel_name(name: IPackagePath) -> str:
12091215
"""
12101216
Infer a possibly importable module name from a name presumed on
12111217
sys.path.

‎Lib/importlib/metadata/_meta.py‎

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
from __future__ import annotations
22

33
import os
4-
from collections.abc import Iterator
4+
from collections.abc import Iterable, Iterator
55
from typing import (
66
Any,
77
Protocol,
88
TypeVar,
99
overload,
10+
runtime_checkable,
1011
)
1112

1213
_T = TypeVar("_T")
1314

1415

16+
@runtime_checkable
1517
class PackageMetadata(Protocol):
1618
def __len__(self) -> int: ... # pragma: no cover
1719

@@ -69,3 +71,81 @@ def read_text(self, encoding=None) -> str: ... # pragma: no cover
6971
def read_bytes(self) -> bytes: ... # pragma: no cover
7072

7173
def exists(self) -> bool: ... # pragma: no cover
74+
75+
76+
@runtime_checkable
77+
class IPackagePath(Protocol):
78+
"""
79+
The interface of the paths yielded by ``IDistribution.files``.
80+
81+
Consumers should prefer this protocol over ``PackagePath``,
82+
as it is also satisfied by the objects that ``importlib.metadata``
83+
supplies.
84+
"""
85+
86+
hash: Any | None
87+
size: int | None
88+
dist: IDistribution
89+
90+
def read_text(self, encoding: str = 'utf-8') -> str: ... # pragma: no cover
91+
92+
def read_binary(self) -> bytes: ... # pragma: no cover
93+
94+
def locate(self) -> SimplePath: ... # pragma: no cover
95+
96+
@property
97+
def parts(self) -> tuple[str, ...]: ... # pragma: no cover
98+
99+
def __fspath__(self) -> str: ... # pragma: no cover
100+
101+
102+
@runtime_checkable
103+
class IDistribution(Protocol):
104+
"""
105+
The interface of the distributions yielded by ``distributions()``
106+
and friends.
107+
108+
Consumers should prefer this protocol over ``Distribution``,
109+
as it is also satisfied by the objects that ``importlib.metadata``
110+
supplies.
111+
"""
112+
113+
def read_text(
114+
self, filename: str | os.PathLike[str]
115+
) -> str | None: ... # pragma: no cover
116+
117+
def locate_file(
118+
self, path: str | os.PathLike[str]
119+
) -> SimplePath: ... # pragma: no cover
120+
121+
@property
122+
def metadata(self) -> PackageMetadata: ... # pragma: no cover
123+
124+
@property
125+
def name(self) -> str: ... # pragma: no cover
126+
127+
@property
128+
def version(self) -> str: ... # pragma: no cover
129+
130+
@property
131+
def entry_points(self) -> Any: ... # pragma: no cover
132+
133+
@property
134+
def files(self) -> list[IPackagePath] | None: ... # pragma: no cover
135+
136+
@property
137+
def requires(self) -> list[str] | None: ... # pragma: no cover
138+
139+
@property
140+
def origin(self) -> Any: ... # pragma: no cover
141+
142+
@classmethod
143+
def discover(
144+
cls, *, context: Any | None = None, **kwargs: Any
145+
) -> Iterable[IDistribution]: ... # pragma: no cover
146+
147+
@classmethod
148+
def from_name(cls, name: str) -> IDistribution: ... # pragma: no cover
149+
150+
@staticmethod
151+
def at(path: str | os.PathLike[str]) -> IDistribution: ... # pragma: no cover
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import unittest
2+
3+
from importlib.metadata import (
4+
IDistribution,
5+
IPackagePath,
6+
PackageMetadata,
7+
distribution,
8+
files,
9+
metadata,
10+
)
11+
12+
from . import fixtures
13+
14+
15+
class ProtocolTests(fixtures.DistInfoPkg, unittest.TestCase):
16+
"""
17+
The objects supplied to consumers satisfy the published protocols.
18+
"""
19+
20+
def test_distribution_matches_protocol(self):
21+
assert isinstance(distribution('distinfo-pkg'), IDistribution)
22+
23+
def test_metadata_matches_protocol(self):
24+
assert isinstance(metadata('distinfo-pkg'), PackageMetadata)
25+
26+
def test_package_path_matches_protocol(self):
27+
assert all(isinstance(file, IPackagePath) for file in files('distinfo-pkg'))
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Applied changes to ``importlib.metadata`` from `importlib_metadata 10.0
2+
<https://importlib-metadata.readthedocs.io/en/latest/history.html#v10-0-0>`_:
3+
added the ``IDistribution`` and ``IPackagePath`` protocols, so a consumer can
4+
target one interface across ``importlib.metadata`` and the backport rather
5+
than either library's concrete classes, and the functions returning
6+
distributions and files are annotated with them.

0 commit comments

Comments
 (0)