Skip to content

Commit 1a14f99

Browse files
committed
Implement interfaces for Distribution and PackagePath suitable for downstream consumers.
1 parent 28c50a7 commit 1a14f99

File tree

3 files changed

+82
-15
lines changed

3 files changed

+82
-15
lines changed

importlib_metadata/__init__.py

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

32-
from . import _meta
3332
from ._collections import FreezableDefaultDict, Pair
3433
from ._compat import (
3534
NullFinder,
@@ -38,16 +37,24 @@
3837
)
3938
from ._functools import apply, compose, method_cache, noop, pass_none, passthrough
4039
from ._itertools import always_iterable, bucket, unique_everseen
41-
from ._meta import PackageMetadata, SimplePath
40+
from ._meta import (
41+
IDistribution,
42+
IPackagePath,
43+
PackageMetadata,
44+
SimplePath,
45+
)
4246
from ._typing import md_none
4347
from .compat import py39, py311
4448

4549
__all__ = [
4650
'Distribution',
4751
'DistributionFinder',
52+
'IDistribution',
4853
'PackageMetadata',
4954
'PackageNotFoundError',
5055
'SimplePath',
56+
'PackagePath',
57+
'IPackagePath',
5158
'distribution',
5259
'distributions',
5360
'entry_points',
@@ -208,7 +215,7 @@ class EntryPoint:
208215
value: str
209216
group: str
210217

211-
dist: Distribution | None = None
218+
dist: IDistribution | None = None
212219

213220
def __init__(self, name: str, value: str, group: str) -> None:
214221
vars(self).update(name=name, value=value, group=group)
@@ -374,7 +381,7 @@ class PackagePath(pathlib.PurePosixPath):
374381

375382
hash: FileHash | None
376383
size: int
377-
dist: Distribution
384+
dist: IDistribution
378385

379386
def read_text(self, encoding: str = 'utf-8') -> str:
380387
return self.locate().read_text(encoding=encoding)
@@ -516,7 +523,7 @@ def _discover_resolvers():
516523

517524
@property
518525
@apply(pass_none(localize.message))
519-
def metadata(self) -> _meta.PackageMetadata | None:
526+
def metadata(self) -> PackageMetadata | None:
520527
"""Return the parsed metadata for this Distribution.
521528
522529
The returned object will have keys that name the various bits of
@@ -539,7 +546,7 @@ def metadata(self) -> _meta.PackageMetadata | None:
539546

540547
@staticmethod
541548
@pass_none
542-
def _assemble_message(text: str) -> _meta.PackageMetadata:
549+
def _assemble_message(text: str) -> PackageMetadata:
543550
# deferred for performance (python/cpython#109829)
544551
from . import _adapters
545552

@@ -572,10 +579,10 @@ def entry_points(self) -> EntryPoints:
572579

573580
@property
574581
@apply(pass_none(compose(list, functools.partial(map, localize.package_path))))
575-
def files(self) -> list[PackagePath] | None:
582+
def files(self) -> list[IPackagePath] | None:
576583
"""Files in this distribution.
577584
578-
:return: List of PackagePath for this distribution or None
585+
:return: List of PackagePath-like objects for this distribution or None
579586
580587
Result is `None` if the metadata file that enumerates files
581588
(i.e. RECORD for dist-info, or installed-files.txt or
@@ -1055,7 +1062,7 @@ def _name_from_stem(stem):
10551062
return name
10561063

10571064

1058-
def distribution(distribution_name: str) -> Distribution:
1065+
def distribution(distribution_name: str) -> IDistribution:
10591066
"""Get the ``Distribution`` instance for the named package.
10601067
10611068
:param distribution_name: The name of the distribution package as a string.
@@ -1064,15 +1071,15 @@ def distribution(distribution_name: str) -> Distribution:
10641071
return Distribution.from_name(distribution_name)
10651072

10661073

1067-
def distributions(**kwargs) -> Iterable[Distribution]:
1074+
def distributions(**kwargs) -> Iterable[IDistribution]:
10681075
"""Get all ``Distribution`` instances in the current environment.
10691076
10701077
:return: An iterable of ``Distribution`` instances.
10711078
"""
10721079
return Distribution.discover(**kwargs)
10731080

10741081

1075-
def metadata(distribution_name: str) -> _meta.PackageMetadata | None:
1082+
def metadata(distribution_name: str) -> PackageMetadata | None:
10761083
"""Get the metadata for the named package.
10771084
10781085
:param distribution_name: The name of the distribution package to query.
@@ -1115,7 +1122,7 @@ def entry_points(**params) -> EntryPoints:
11151122
return EntryPoints(eps).select(**params)
11161123

11171124

1118-
def files(distribution_name: str) -> list[PackagePath] | None:
1125+
def files(distribution_name: str) -> list[IPackagePath] | None:
11191126
"""Return a list of files for the named package.
11201127
11211128
:param distribution_name: The name of the distribution package to query.
@@ -1155,15 +1162,15 @@ def _top_level_declared(dist):
11551162
return (dist.read_text('top_level.txt') or '').split()
11561163

11571164

1158-
def _topmost(name: PackagePath) -> str | None:
1165+
def _topmost(name: IPackagePath) -> str | None:
11591166
"""
11601167
Return the top-most parent as long as there is a parent.
11611168
"""
11621169
top, *rest = name.parts
11631170
return top if rest else None
11641171

11651172

1166-
def _get_toplevel_name(name: PackagePath) -> str:
1173+
def _get_toplevel_name(name: IPackagePath) -> str:
11671174
"""
11681175
Infer a possibly importable module name from a name presumed on
11691176
sys.path.

importlib_metadata/_meta.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
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,
@@ -69,3 +69,62 @@ def read_text(self, encoding=None) -> str: ... # pragma: no cover
6969
def read_bytes(self) -> bytes: ... # pragma: no cover
7070

7171
def exists(self) -> bool: ... # pragma: no cover
72+
73+
74+
class IPackagePath(Protocol):
75+
hash: Any | None
76+
size: int | None
77+
dist: IDistribution
78+
79+
def read_text(self, encoding: str = 'utf-8') -> str: ... # pragma: no cover
80+
81+
def read_binary(self) -> bytes: ... # pragma: no cover
82+
83+
def locate(self) -> SimplePath: ... # pragma: no cover
84+
85+
@property
86+
def parts(self) -> tuple[str, ...]: ... # pragma: no cover
87+
88+
def __fspath__(self) -> str: ... # pragma: no cover
89+
90+
91+
class IDistribution(Protocol):
92+
def read_text(
93+
self, filename: str | os.PathLike[str]
94+
) -> str | None: ... # pragma: no cover
95+
96+
def locate_file(
97+
self, path: str | os.PathLike[str]
98+
) -> SimplePath: ... # pragma: no cover
99+
100+
@property
101+
def metadata(self) -> PackageMetadata | None: ... # pragma: no cover
102+
103+
@property
104+
def name(self) -> str: ... # pragma: no cover
105+
106+
@property
107+
def version(self) -> str: ... # pragma: no cover
108+
109+
@property
110+
def entry_points(self) -> Any: ... # pragma: no cover
111+
112+
@property
113+
def files(self) -> list[IPackagePath] | None: ... # pragma: no cover
114+
115+
@property
116+
def requires(self) -> list[str] | None: ... # pragma: no cover
117+
118+
@property
119+
def origin(self) -> Any: ... # pragma: no cover
120+
121+
@classmethod
122+
def discover(
123+
cls, *, context: Any | None = None, **kwargs: Any
124+
) -> Iterable[IDistribution]: ... # pragma: no cover
125+
126+
@classmethod
127+
def from_name(cls, name: str) -> IDistribution: ... # pragma: no cover
128+
129+
@staticmethod
130+
def at(path: str | os.PathLike[str]) -> IDistribution: ... # pragma: no cover

newsfragments/486.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added ``IDistribution`` and ``IPackagePath`` protocols so consumers can target a stable, shared interface across ``importlib.metadata`` and ``importlib_metadata``.

0 commit comments

Comments
 (0)