From ac1128a0d1a14d7a0ebfb9075ea6ee662242c1ac Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 22 Oct 2026 11:53:10 -0700 Subject: [PATCH 1/5] gh-123976: Document backport caveats for custom providers Describe how the separate class hierarchies in importlib.metadata and the importlib_metadata backport affect custom providers: which objects the backport substitutes, which it passes through with a warning, and why a provider should implement the whole interface and rely on the shared protocols rather than on a particular base class. Co-Authored-By: Claude Opus 5 --- Doc/library/importlib.metadata.rst | 50 +++++++++++++++++++ ...-10-22-18-51-05.gh-issue-123976.GKLjFT.rst | 2 + 2 files changed, 52 insertions(+) create mode 100644 Misc/NEWS.d/next/Documentation/2026-10-22-18-51-05.gh-issue-123976.GKLjFT.rst diff --git a/Doc/library/importlib.metadata.rst b/Doc/library/importlib.metadata.rst index 1378a131f0ce65..95d05bbc4cde3f 100644 --- a/Doc/library/importlib.metadata.rst +++ b/Doc/library/importlib.metadata.rst @@ -735,3 +735,53 @@ packages served by the ``DatabaseImporter``, assuming that the The ``DatabaseDistribution`` may also provide other metadata files, like ``RECORD`` (required for :attr:`!Distribution.files`) or override the implementation of :attr:`!Distribution.files`. See the source for more inspiration. + +Because ``DatabaseDistribution`` derives from :class:`Distribution` as defined +here, consumers that use the :pypi:`importlib_metadata` backport instead of +this module receive objects from a hierarchy they do not expect. See +:ref:`metadata-backport-caveats`. + +.. _metadata-backport-caveats: + +Caveats for backports +--------------------- + +The :pypi:`importlib_metadata` package backports this module and supplies its +own :class:`!Distribution`, :class:`!PackagePath`, and metadata classes. Those +classes are separate from the ones in :mod:`!importlib.metadata`; neither set +derives from the other. + +A custom provider has no say in which of the two a consumer uses. Both +libraries discover distributions by querying the finders on +:data:`sys.meta_path`, so a single provider installed there serves both. A +consumer calling ``importlib_metadata.distributions()`` expects that package's +objects, but a provider deriving from the :class:`Distribution` class +documented here supplies objects from the other hierarchy. + +The backport settles most of this on the consumer's behalf, substituting its +own class for each object it recognizes, such as a :class:`!PathDistribution`, +a :class:`PackagePath`, or an :class:`email.message.Message`. It cannot do so +for an arbitrary :class:`Distribution` subclass from a custom provider, having +no way to reconstruct that subclass. Such an object is passed through +unchanged, and the backport emits a :exc:`UserWarning` reporting the +unrecognized subclass. + +A passed-through object still serves the consumer, provided it implements the +whole interface rather than part of it. So, when writing a custom provider: + +* Supply objects that present the whole interface, not part of it. Deriving + from :class:`Distribution`, as the example above does, achieves this: the + base class builds the rest of the interface on + :meth:`~Distribution.read_text` and :meth:`~Distribution.locate_file`. A + provider that assembles its objects some other way supplies every member + itself. +* Where the provider itself consumes metadata, and in the guidance offered to + its users, rely on the object satisfying that interface rather than on its + type. The backport publishes ``IDistribution``, ``IPackagePath``, and + ``PackageMetadata`` protocols for this purpose, which both its classes and + the ones documented here satisfy. + +Deriving from whichever of the two libraries happens to be importable is not a +general answer. A provider cannot know whether a consumer will import the +backport, and importing it to find out activates it in environments that would +otherwise never have used it. diff --git a/Misc/NEWS.d/next/Documentation/2026-10-22-18-51-05.gh-issue-123976.GKLjFT.rst b/Misc/NEWS.d/next/Documentation/2026-10-22-18-51-05.gh-issue-123976.GKLjFT.rst new file mode 100644 index 00000000000000..14f6adcda1996c --- /dev/null +++ b/Misc/NEWS.d/next/Documentation/2026-10-22-18-51-05.gh-issue-123976.GKLjFT.rst @@ -0,0 +1,2 @@ +Document how custom ``importlib.metadata`` providers interact with the +``importlib_metadata`` backport. From ab57ac7aa45fae3217f77f1b530401830a8a4d9d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 22 Oct 2026 14:40:59 -0700 Subject: [PATCH 2/5] gh-123976: Warn only on an incomplete provider importlib_metadata now decides the warning by conformance to IDistribution rather than by class identity, so a provider that presents the whole interface draws no warning and the warning names what a class omits. Co-Authored-By: Claude Opus 5 --- Doc/library/importlib.metadata.rst | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Doc/library/importlib.metadata.rst b/Doc/library/importlib.metadata.rst index 95d05bbc4cde3f..6184df1760b738 100644 --- a/Doc/library/importlib.metadata.rst +++ b/Doc/library/importlib.metadata.rst @@ -760,21 +760,21 @@ documented here supplies objects from the other hierarchy. The backport settles most of this on the consumer's behalf, substituting its own class for each object it recognizes, such as a :class:`!PathDistribution`, -a :class:`PackagePath`, or an :class:`email.message.Message`. It cannot do so -for an arbitrary :class:`Distribution` subclass from a custom provider, having -no way to reconstruct that subclass. Such an object is passed through -unchanged, and the backport emits a :exc:`UserWarning` reporting the -unrecognized subclass. +a :class:`PackagePath`, or an :class:`email.message.Message`. It has no way to +reconstruct an arbitrary :class:`Distribution` subclass from a custom +provider, so it passes such an object through unchanged. That serves the +consumer as long as the object presents the whole interface; where the class +omits a member the interface requires, the backport emits a +:exc:`UserWarning` naming what is missing. -A passed-through object still serves the consumer, provided it implements the -whole interface rather than part of it. So, when writing a custom provider: +So, when writing a custom provider: * Supply objects that present the whole interface, not part of it. Deriving from :class:`Distribution`, as the example above does, achieves this: the base class builds the rest of the interface on - :meth:`~Distribution.read_text` and :meth:`~Distribution.locate_file`. A - provider that assembles its objects some other way supplies every member - itself. + :meth:`~Distribution.read_text` and :meth:`~Distribution.locate_file`, and + such a provider draws no warning. A provider that assembles its objects + some other way supplies every member itself. * Where the provider itself consumes metadata, and in the guidance offered to its users, rely on the object satisfying that interface rather than on its type. The backport publishes ``IDistribution``, ``IPackagePath``, and From 69a4d4841fde358a1c68bd3592acb90a72cf9a80 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 22 Oct 2026 14:51:41 -0700 Subject: [PATCH 3/5] gh-123976: Separate the consumer and provider caveats The guidance to rely on the interface rather than the type addresses every consumer, not only a provider that consumes metadata, so promote the caveats to their own section and split it by audience. Note that the protocols come from the backport, which this module does not yet present. Co-Authored-By: Claude Opus 5 --- Doc/library/importlib.metadata.rst | 75 ++++++++++++++++-------------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/Doc/library/importlib.metadata.rst b/Doc/library/importlib.metadata.rst index 6184df1760b738..5c6d6b3a91f2b9 100644 --- a/Doc/library/importlib.metadata.rst +++ b/Doc/library/importlib.metadata.rst @@ -736,50 +736,53 @@ The ``DatabaseDistribution`` may also provide other metadata files, like ``RECORD`` (required for :attr:`!Distribution.files`) or override the implementation of :attr:`!Distribution.files`. See the source for more inspiration. -Because ``DatabaseDistribution`` derives from :class:`Distribution` as defined -here, consumers that use the :pypi:`importlib_metadata` backport instead of -this module receive objects from a hierarchy they do not expect. See -:ref:`metadata-backport-caveats`. +``DatabaseDistribution`` derives from :class:`Distribution` as defined here, +which has consequences for consumers that use the :pypi:`importlib_metadata` +backport instead of this module. See :ref:`metadata-backport-providers`. + .. _metadata-backport-caveats: Caveats for backports ---------------------- +===================== The :pypi:`importlib_metadata` package backports this module and supplies its own :class:`!Distribution`, :class:`!PackagePath`, and metadata classes. Those classes are separate from the ones in :mod:`!importlib.metadata`; neither set -derives from the other. - -A custom provider has no say in which of the two a consumer uses. Both -libraries discover distributions by querying the finders on -:data:`sys.meta_path`, so a single provider installed there serves both. A -consumer calling ``importlib_metadata.distributions()`` expects that package's -objects, but a provider deriving from the :class:`Distribution` class -documented here supplies objects from the other hierarchy. - -The backport settles most of this on the consumer's behalf, substituting its -own class for each object it recognizes, such as a :class:`!PathDistribution`, -a :class:`PackagePath`, or an :class:`email.message.Message`. It has no way to -reconstruct an arbitrary :class:`Distribution` subclass from a custom -provider, so it passes such an object through unchanged. That serves the -consumer as long as the object presents the whole interface; where the class -omits a member the interface requires, the backport emits a -:exc:`UserWarning` naming what is missing. - -So, when writing a custom provider: - -* Supply objects that present the whole interface, not part of it. Deriving - from :class:`Distribution`, as the example above does, achieves this: the - base class builds the rest of the interface on - :meth:`~Distribution.read_text` and :meth:`~Distribution.locate_file`, and - such a provider draws no warning. A provider that assembles its objects - some other way supplies every member itself. -* Where the provider itself consumes metadata, and in the guidance offered to - its users, rely on the object satisfying that interface rather than on its - type. The backport publishes ``IDistribution``, ``IPackagePath``, and - ``PackageMetadata`` protocols for this purpose, which both its classes and - the ones documented here satisfy. +derives from the other. Both libraries discover distributions by querying the +finders on :data:`sys.meta_path`, so one custom provider serves consumers of +both and has no say in which of the two a given consumer uses. + +For consumers +------------- + +A consumer calling ``importlib_metadata.distributions()`` expects that +package's objects, but a custom provider may supply objects from the hierarchy +documented here. The backport settles most of this on the consumer's behalf, +substituting its own class for each object it recognizes, such as a +:class:`!PathDistribution`, a :class:`PackagePath`, or an +:class:`email.message.Message`. It has no way to reconstruct an arbitrary +:class:`Distribution` subclass from a custom provider, so it passes such an +object through unchanged. + +Rely on an object satisfying the interface rather than on its type, and do not +test it with :func:`isinstance` against a concrete class. The backport +publishes ``IDistribution``, ``IPackagePath``, and ``PackageMetadata`` +protocols for this purpose, which the classes of both libraries satisfy. +Import them from the backport; this module does not present them yet. + +.. _metadata-backport-providers: + +For custom providers +-------------------- + +A provider's objects reach consumers of both libraries, so supply objects that +present the whole interface, not part of it. Deriving from +:class:`Distribution`, as the example above does, achieves this: the base +class builds the rest of the interface on :meth:`~Distribution.read_text` and +:meth:`~Distribution.locate_file`. Where a class omits a member the interface +requires, the backport emits a :exc:`UserWarning` naming what is missing; a +provider presenting the whole interface draws no warning. Deriving from whichever of the two libraries happens to be importable is not a general answer. A provider cannot know whether a consumer will import the From be9a5dede19969f65fc41e718d6c493e206c019b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 22 Oct 2026 14:56:02 -0700 Subject: [PATCH 4/5] gh-123976: Document the protocols shared with the backport Assume the shared protocols from python/importlib_metadata#505 are presented by this module as well: document IDistribution and IPackagePath, correct PackageMetadata, which is already a protocol here rather than an implementation of the backport's, and write the caveats around the protocols as the interface both libraries satisfy. Co-Authored-By: Claude Opus 5 --- Doc/library/importlib.metadata.rst | 55 +++++++++++++------ ...-10-22-18-51-05.gh-issue-123976.GKLjFT.rst | 5 +- 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/Doc/library/importlib.metadata.rst b/Doc/library/importlib.metadata.rst index 5c6d6b3a91f2b9..3ab8505e1e9ebc 100644 --- a/Doc/library/importlib.metadata.rst +++ b/Doc/library/importlib.metadata.rst @@ -237,11 +237,12 @@ Distribution metadata .. class:: PackageMetadata - A concrete implementation of the - `PackageMetadata protocol `_. + A protocol describing the interface distribution metadata presents. The + :pypi:`importlib_metadata` backport presents the same protocol; see + :ref:`metadata-backport-caveats`. - In addition to providing the defined protocol methods and attributes, subscripting - the instance is equivalent to calling the :meth:`!get` method. + In addition to the protocol methods and attributes, subscripting an + instance is equivalent to calling the :meth:`!get` method. Every `Distribution Package `_ includes some metadata, which you can extract using the :func:`!metadata` function:: @@ -332,6 +333,13 @@ Distribution files check if it ``exists()``, to traverse using ``joinpath()`` and ``parent``, and to retrieve data using ``read_text()`` and ``read_bytes()``. +.. class:: IPackagePath + + A protocol describing the interface a :class:`PackagePath` presents. See + :ref:`metadata-backport-caveats`. + + .. versionadded:: 3.16 + The :func:`!files` function takes a `Distribution Package `_ name and returns all of the files installed by this distribution. For example:: @@ -544,6 +552,17 @@ The same applies for :func:`entry_points` and :func:`files`. A shortcut for ``distribution.locate_file(filename).read_text()``. +.. class:: IDistribution + + A protocol describing the interface a :class:`Distribution` presents. + + A consumer that targets this protocol, rather than a concrete class, works + with distributions from either this module or the + :pypi:`importlib_metadata` backport, however they were supplied. See + :ref:`metadata-backport-caveats`. + + .. versionadded:: 3.16 + .. _distribution-discovery: Distribution Discovery @@ -748,10 +767,15 @@ Caveats for backports The :pypi:`importlib_metadata` package backports this module and supplies its own :class:`!Distribution`, :class:`!PackagePath`, and metadata classes. Those -classes are separate from the ones in :mod:`!importlib.metadata`; neither set -derives from the other. Both libraries discover distributions by querying the -finders on :data:`sys.meta_path`, so one custom provider serves consumers of -both and has no say in which of the two a given consumer uses. +classes are separate from the ones here; neither set derives from the other. +Both libraries discover distributions by querying the finders on +:data:`sys.meta_path`, so one custom provider serves consumers of both and has +no say in which of the two a given consumer uses. + +Both libraries present the same protocols, :class:`IDistribution`, +:class:`IPackagePath`, and :class:`PackageMetadata`, and the classes of both +satisfy them. Those protocols, rather than any concrete class, are the +interface to write against. For consumers ------------- @@ -765,11 +789,10 @@ substituting its own class for each object it recognizes, such as a :class:`Distribution` subclass from a custom provider, so it passes such an object through unchanged. -Rely on an object satisfying the interface rather than on its type, and do not -test it with :func:`isinstance` against a concrete class. The backport -publishes ``IDistribution``, ``IPackagePath``, and ``PackageMetadata`` -protocols for this purpose, which the classes of both libraries satisfy. -Import them from the backport; this module does not present them yet. +Annotate against the protocols, then, and do not test an object with +:func:`isinstance` against a concrete class. Code that needs the protocols on +earlier Python versions can import them from the backport, which presents them +there too. .. _metadata-backport-providers: @@ -777,12 +800,12 @@ For custom providers -------------------- A provider's objects reach consumers of both libraries, so supply objects that -present the whole interface, not part of it. Deriving from +satisfy :class:`IDistribution` whole, not in part. Deriving from :class:`Distribution`, as the example above does, achieves this: the base class builds the rest of the interface on :meth:`~Distribution.read_text` and -:meth:`~Distribution.locate_file`. Where a class omits a member the interface +:meth:`~Distribution.locate_file`. Where a class omits a member the protocol requires, the backport emits a :exc:`UserWarning` naming what is missing; a -provider presenting the whole interface draws no warning. +provider satisfying the protocol whole draws no warning. Deriving from whichever of the two libraries happens to be importable is not a general answer. A provider cannot know whether a consumer will import the diff --git a/Misc/NEWS.d/next/Documentation/2026-10-22-18-51-05.gh-issue-123976.GKLjFT.rst b/Misc/NEWS.d/next/Documentation/2026-10-22-18-51-05.gh-issue-123976.GKLjFT.rst index 14f6adcda1996c..614d2c6595c324 100644 --- a/Misc/NEWS.d/next/Documentation/2026-10-22-18-51-05.gh-issue-123976.GKLjFT.rst +++ b/Misc/NEWS.d/next/Documentation/2026-10-22-18-51-05.gh-issue-123976.GKLjFT.rst @@ -1,2 +1,3 @@ -Document how custom ``importlib.metadata`` providers interact with the -``importlib_metadata`` backport. +Document the ``IDistribution``, ``IPackagePath``, and ``PackageMetadata`` +protocols shared with the ``importlib_metadata`` backport, and how custom +providers and consumers of ``importlib.metadata`` should use them. From 294c0dfd3213b780c0f413d1cff97ef0062bd8db Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 31 Oct 2026 15:57:29 -0400 Subject: [PATCH 5/5] 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 --- Lib/importlib/metadata/__init__.py | 34 ++++---- Lib/importlib/metadata/_meta.py | 82 ++++++++++++++++++- .../test_importlib/metadata/test_protocols.py | 27 ++++++ ...-10-31-15-50-12.gh-issue-160386.qRrnCh.rst | 6 ++ 4 files changed, 134 insertions(+), 15 deletions(-) create mode 100644 Lib/test/test_importlib/metadata/test_protocols.py create mode 100644 Misc/NEWS.d/next/Library/2026-10-31-15-50-12.gh-issue-160386.qRrnCh.rst diff --git a/Lib/importlib/metadata/__init__.py b/Lib/importlib/metadata/__init__.py index 3d6f9cdf6965bb..5680616b68acd2 100644 --- a/Lib/importlib/metadata/__init__.py +++ b/Lib/importlib/metadata/__init__.py @@ -29,16 +29,22 @@ from itertools import starmap from typing import Any -from . import _meta from ._collections import FreezableDefaultDict, Pair from ._context import ExceptionTrap from ._functools import method_cache, noop, pass_none, passthrough from ._itertools import always_iterable, bucket, unique_everseen -from ._meta import PackageMetadata, SimplePath +from ._meta import ( + IDistribution, + IPackagePath, + PackageMetadata, + SimplePath, +) __all__ = [ 'Distribution', 'DistributionFinder', + 'IDistribution', + 'IPackagePath', 'MetadataNotFound', 'PackageMetadata', 'PackageNotFoundError', @@ -210,7 +216,7 @@ class EntryPoint: value: str group: str - dist: Distribution | None = None + dist: IDistribution | None = None def __init__(self, name: str, value: str, group: str) -> None: vars(self).update(name=name, value=value, group=group) @@ -375,7 +381,7 @@ class PackagePath(pathlib.PurePosixPath): hash: FileHash | None size: int - dist: Distribution + dist: IDistribution def read_text(self, encoding: str = 'utf-8') -> str: return self.locate().read_text(encoding=encoding) @@ -538,7 +544,7 @@ def _discover_resolvers(): return filter(None, declared) @property - def metadata(self) -> _meta.PackageMetadata: + def metadata(self) -> PackageMetadata: """Return the parsed metadata for this Distribution. The returned object will have keys that name the various bits of @@ -562,7 +568,7 @@ def metadata(self) -> _meta.PackageMetadata: return self._assemble_message(self._ensure_metadata_present(text)) @staticmethod - def _assemble_message(text: str) -> _meta.PackageMetadata: + def _assemble_message(text: str) -> PackageMetadata: # deferred for performance (python/cpython#109829) from . import _adapters @@ -602,10 +608,10 @@ def entry_points(self) -> EntryPoints: return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self) @property - def files(self) -> list[PackagePath] | None: + def files(self) -> list[IPackagePath] | None: """Files in this distribution. - :return: List of PackagePath for this distribution or None + :return: List of PackagePath-like objects for this distribution or None Result is `None` if the metadata file that enumerates files (i.e. RECORD for dist-info, or installed-files.txt or @@ -1096,7 +1102,7 @@ def _name_from_stem(stem): return name -def distribution(distribution_name: str) -> Distribution: +def distribution(distribution_name: str) -> IDistribution: """Get the ``Distribution`` instance for the named package. :param distribution_name: The name of the distribution package as a string. @@ -1105,7 +1111,7 @@ def distribution(distribution_name: str) -> Distribution: return Distribution.from_name(distribution_name) -def distributions(**kwargs) -> Iterable[Distribution]: +def distributions(**kwargs) -> Iterable[IDistribution]: """Get all ``Distribution`` instances in the current environment. :return: An iterable of ``Distribution`` instances. @@ -1113,7 +1119,7 @@ def distributions(**kwargs) -> Iterable[Distribution]: return Distribution.discover(**kwargs) -def metadata(distribution_name: str) -> _meta.PackageMetadata: +def metadata(distribution_name: str) -> PackageMetadata: """Get the metadata for the named package. :param distribution_name: The name of the distribution package to query. @@ -1157,7 +1163,7 @@ def entry_points(**params) -> EntryPoints: return EntryPoints(eps).select(**params) -def files(distribution_name: str) -> list[PackagePath] | None: +def files(distribution_name: str) -> list[IPackagePath] | None: """Return a list of files for the named package. :param distribution_name: The name of the distribution package to query. @@ -1197,7 +1203,7 @@ def _top_level_declared(dist): return (dist.read_text('top_level.txt') or '').split() -def _topmost(name: PackagePath) -> str | None: +def _topmost(name: IPackagePath) -> str | None: """ Return the top-most parent as long as there is a parent. """ @@ -1205,7 +1211,7 @@ def _topmost(name: PackagePath) -> str | None: return top if rest else None -def _get_toplevel_name(name: PackagePath) -> str: +def _get_toplevel_name(name: IPackagePath) -> str: """ Infer a possibly importable module name from a name presumed on sys.path. diff --git a/Lib/importlib/metadata/_meta.py b/Lib/importlib/metadata/_meta.py index 219b2a86d1755c..d94b433f6ee9ec 100644 --- a/Lib/importlib/metadata/_meta.py +++ b/Lib/importlib/metadata/_meta.py @@ -1,17 +1,19 @@ from __future__ import annotations import os -from collections.abc import Iterator +from collections.abc import Iterable, Iterator from typing import ( Any, Protocol, TypeVar, overload, + runtime_checkable, ) _T = TypeVar("_T") +@runtime_checkable class PackageMetadata(Protocol): def __len__(self) -> int: ... # pragma: no cover @@ -69,3 +71,81 @@ def read_text(self, encoding=None) -> str: ... # pragma: no cover def read_bytes(self) -> bytes: ... # pragma: no cover def exists(self) -> bool: ... # pragma: no cover + + +@runtime_checkable +class IPackagePath(Protocol): + """ + The interface of the paths yielded by ``IDistribution.files``. + + Consumers should prefer this protocol over ``PackagePath``, + as it is also satisfied by the objects that ``importlib.metadata`` + supplies. + """ + + hash: Any | None + size: int | None + dist: IDistribution + + def read_text(self, encoding: str = 'utf-8') -> str: ... # pragma: no cover + + def read_binary(self) -> bytes: ... # pragma: no cover + + def locate(self) -> SimplePath: ... # pragma: no cover + + @property + def parts(self) -> tuple[str, ...]: ... # pragma: no cover + + def __fspath__(self) -> str: ... # pragma: no cover + + +@runtime_checkable +class IDistribution(Protocol): + """ + The interface of the distributions yielded by ``distributions()`` + and friends. + + Consumers should prefer this protocol over ``Distribution``, + as it is also satisfied by the objects that ``importlib.metadata`` + supplies. + """ + + def read_text( + self, filename: str | os.PathLike[str] + ) -> str | None: ... # pragma: no cover + + def locate_file( + self, path: str | os.PathLike[str] + ) -> SimplePath: ... # pragma: no cover + + @property + def metadata(self) -> PackageMetadata: ... # pragma: no cover + + @property + def name(self) -> str: ... # pragma: no cover + + @property + def version(self) -> str: ... # pragma: no cover + + @property + def entry_points(self) -> Any: ... # pragma: no cover + + @property + def files(self) -> list[IPackagePath] | None: ... # pragma: no cover + + @property + def requires(self) -> list[str] | None: ... # pragma: no cover + + @property + def origin(self) -> Any: ... # pragma: no cover + + @classmethod + def discover( + cls, *, context: Any | None = None, **kwargs: Any + ) -> Iterable[IDistribution]: ... # pragma: no cover + + @classmethod + def from_name(cls, name: str) -> IDistribution: ... # pragma: no cover + + @staticmethod + def at(path: str | os.PathLike[str]) -> IDistribution: ... # pragma: no cover diff --git a/Lib/test/test_importlib/metadata/test_protocols.py b/Lib/test/test_importlib/metadata/test_protocols.py new file mode 100644 index 00000000000000..fb9b10131e6663 --- /dev/null +++ b/Lib/test/test_importlib/metadata/test_protocols.py @@ -0,0 +1,27 @@ +import unittest + +from importlib.metadata import ( + IDistribution, + IPackagePath, + PackageMetadata, + distribution, + files, + metadata, +) + +from . import fixtures + + +class ProtocolTests(fixtures.DistInfoPkg, unittest.TestCase): + """ + The objects supplied to consumers satisfy the published protocols. + """ + + def test_distribution_matches_protocol(self): + assert isinstance(distribution('distinfo-pkg'), IDistribution) + + def test_metadata_matches_protocol(self): + assert isinstance(metadata('distinfo-pkg'), PackageMetadata) + + def test_package_path_matches_protocol(self): + assert all(isinstance(file, IPackagePath) for file in files('distinfo-pkg')) diff --git a/Misc/NEWS.d/next/Library/2026-10-31-15-50-12.gh-issue-160386.qRrnCh.rst b/Misc/NEWS.d/next/Library/2026-10-31-15-50-12.gh-issue-160386.qRrnCh.rst new file mode 100644 index 00000000000000..f17149706d85c9 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-31-15-50-12.gh-issue-160386.qRrnCh.rst @@ -0,0 +1,6 @@ +Applied changes to ``importlib.metadata`` from `importlib_metadata 10.0 +`_: +added the ``IDistribution`` and ``IPackagePath`` protocols, so a consumer can +target one interface across ``importlib.metadata`` and the backport rather +than either library's concrete classes, and the functions returning +distributions and files are annotated with them.