diff --git a/Doc/library/importlib.metadata.rst b/Doc/library/importlib.metadata.rst
index a1af7a754ba4ef..24655181449105 100644
--- a/Doc/library/importlib.metadata.rst
+++ b/Doc/library/importlib.metadata.rst
@@ -13,21 +13,39 @@
**Source code:** :source:`Lib/importlib/metadata/__init__.py`
-``importlib.metadata`` is a library that provides access to installed
-package metadata, such as its entry points or its
-top-level name. Built in part on Python's import system, this library
+``importlib_metadata`` is a library that provides access to
+the metadata of an installed :term:`packaging:Distribution Package`,
+such as its entry points
+or its top-level names (:term:`packaging:Import Package`s, modules, if any).
+Built in part on Python's import system, this library
intends to replace similar functionality in the `entry point
API`_ and `metadata API`_ of ``pkg_resources``. Along with
:mod:`importlib.resources`,
this package can eliminate the need to use the older and less efficient
``pkg_resources`` package.
-By "installed package" we generally mean a third-party package installed into
-Python's ``site-packages`` directory via tools such as `pip
-`_. Specifically,
-it means a package with either a discoverable ``dist-info`` or ``egg-info``
-directory, and metadata defined by :pep:`566` or its older specifications.
-By default, package metadata can live on the file system or in zip archives on
+``importlib_metadata`` operates on third-party *distribution packages*
+installed into Python's ``site-packages`` directory via tools such as
+`pip `_.
+Specifically, it works with distributions with discoverable
+``dist-info`` or ``egg-info`` directories,
+and metadata defined by the :ref:`packaging:core-metadata`.
+
+.. important::
+
+ These are *not* necessarily equivalent to or correspond 1:1 with
+ the top-level *import package* names
+ that can be imported inside Python code.
+ One *distribution package* can contain multiple *import packages*
+ (and single modules),
+ and one top-level *import package*
+ may map to multiple *distribution packages*
+ if it is a namespace package.
+ You can use :ref:`package_distributions() `
+ to get a mapping between them.
+
+By default, distribution metadata can live on the file system
+or in zip archives on
:data:`sys.path`. Through an extension mechanism, the metadata can live almost
anywhere.
@@ -37,12 +55,19 @@ anywhere.
https://importlib-metadata.readthedocs.io/
The documentation for ``importlib_metadata``, which supplies a
backport of ``importlib.metadata``.
+ This includes an `API reference
+ `__
+ for this module's classes and functions,
+ as well as a `migration guide
+ `__
+ for existing users of ``pkg_resources``.
Overview
========
-Let's say you wanted to get the version string for a package you've installed
+Let's say you wanted to get the version string for a
+:term:`packaging:Distribution Package` you've installed
using ``pip``. We start by creating a virtual environment and installing
something into it:
@@ -151,11 +176,10 @@ for more information on entry points, their definition, and usage.
The "selectable" entry points were introduced in ``importlib_metadata``
3.6 and Python 3.10. Prior to those changes, ``entry_points`` accepted
no parameters and always returned a dictionary of entry points, keyed
-by group. For compatibility, if no parameters are passed to entry_points,
-a ``SelectableGroups`` object is returned, implementing that dict
-interface. In the future, calling ``entry_points`` with no parameters
-will return an ``EntryPoints`` object. Users should rely on the selection
-interface to retrieve entry points by group.
+by group. With ``importlib_metadata`` 5.0 and Python 3.12,
+``entry_points`` always returns an ``EntryPoints`` object. See
+`backports.entry_points_selectable `_
+for compatibility options.
.. _metadata:
@@ -163,7 +187,8 @@ interface to retrieve entry points by group.
Distribution metadata
---------------------
-Every distribution includes some metadata, which you can extract using the
+Every :term:`packaging:Distribution Package` includes some metadata,
+which you can extract using the
``metadata()`` function::
>>> wheel_metadata = metadata('wheel') # doctest: +SKIP
@@ -201,7 +226,8 @@ all the metadata in a JSON-compatible form per :PEP:`566`::
Distribution versions
---------------------
-The ``version()`` function is the quickest way to get a distribution's version
+The ``version()`` function is the quickest way to get a
+:term:`packaging:Distribution Package`'s version
number, as a string::
>>> version('wheel') # doctest: +SKIP
@@ -214,7 +240,8 @@ Distribution files
------------------
You can also get the full set of files contained within a distribution. The
-``files()`` function takes a distribution package name and returns all of the
+``files()`` function takes a :term:`packaging:Distribution Package` name
+and returns all of the
files installed by this distribution. Each file object returned is a
``PackagePath``, a :class:`pathlib.PurePath` derived object with additional ``dist``,
``size``, and ``hash`` properties as indicated by the metadata. For example::
@@ -259,19 +286,24 @@ distribution is not known to have the metadata present.
Distribution requirements
-------------------------
-To get the full set of requirements for a distribution, use the ``requires()``
+To get the full set of requirements for a :term:`packaging:Distribution Package`,
+use the ``requires()``
function::
>>> requires('wheel') # doctest: +SKIP
["pytest (>=3.0.0) ; extra == 'test'", "pytest-cov ; extra == 'test'"]
-Package distributions
----------------------
+.. _package-distributions:
+.. _import-distribution-package-mapping:
+
+Mapping import to distribution packages
+---------------------------------------
-A convenience method to resolve the distribution or
-distributions (in the case of a namespace package) for top-level
-Python packages or modules::
+A convenience method to resolve the :term:`packaging:Distribution Package`
+name (or names, in the case of a namespace package)
+that provide each importable top-level
+Python module or :term:`packaging:Import Package`::
>>> packages_distributions()
{'importlib_metadata': ['importlib-metadata'], 'yaml': ['PyYAML'], 'jaraco': ['jaraco.classes', 'jaraco.functools'], ...}
@@ -285,7 +317,8 @@ Distributions
While the above API is the most common and convenient usage, you can get all
of that information from the ``Distribution`` class. A ``Distribution`` is an
-abstract object that represents the metadata for a Python package. You can
+abstract object that represents the metadata for
+a Python :term:`packaging:Distribution Package`. You can
get the ``Distribution`` instance::
>>> from importlib.metadata import distribution # doctest: +SKIP
@@ -305,14 +338,16 @@ instance::
>>> dist.metadata['License'] # doctest: +SKIP
'MIT'
-The full set of available metadata is not described here. See :pep:`566`
-for additional details.
+The full set of available metadata is not described here.
+See the :ref:`packaging:core-metadata` for additional details.
Distribution Discovery
======================
-By default, this package provides built-in support for discovery of metadata for file system and zip file packages. This metadata finder search defaults to ``sys.path``, but varies slightly in how it interprets those values from how other import machinery does. In particular:
+By default, this package provides built-in support for discovery of metadata
+for file system and zip file :term:`packaging:Distribution Package`\s.
+This metadata finder search defaults to ``sys.path``, but varies slightly in how it interprets those values from how other import machinery does. In particular:
- ``importlib.metadata`` does not honor :class:`bytes` objects on ``sys.path``.
- ``importlib.metadata`` will incidentally honor :py:class:`pathlib.Path` objects on ``sys.path`` even though such values will be ignored for imports.
@@ -321,15 +356,18 @@ By default, this package provides built-in support for discovery of metadata for
Extending the search algorithm
==============================
-Because package metadata is not available through :data:`sys.path` searches, or
-package loaders directly, the metadata for a package is found through import
-system :ref:`finders `. To find a distribution package's metadata,
+Because :term:`packaging:Distribution Package` metadata
+is not available through :data:`sys.path` searches, or
+package loaders directly,
+the metadata for a distribution is found through import
+system `finders`_. To find a distribution package's metadata,
``importlib.metadata`` queries the list of :term:`meta path finders ` on
:data:`sys.meta_path`.
-The default ``PathFinder`` for Python includes a hook that calls into
-``importlib.metadata.MetadataPathFinder`` for finding distributions
-loaded from typical file-system-based paths.
+By default ``importlib_metadata`` installs a finder for distribution packages
+found on the file system.
+This finder doesn't actually find any *distributions*,
+but it can find their metadata.
The abstract class :py:class:`importlib.abc.MetaPathFinder` defines the
interface expected of finders by Python's import system.
diff --git a/Lib/importlib/metadata/__init__.py b/Lib/importlib/metadata/__init__.py
index b01de145c33671..274c79a7a28c33 100644
--- a/Lib/importlib/metadata/__init__.py
+++ b/Lib/importlib/metadata/__init__.py
@@ -24,7 +24,7 @@
from importlib import import_module
from importlib.abc import MetaPathFinder
from itertools import starmap
-from typing import List, Mapping, Optional, Union
+from typing import List, Mapping, Optional
__all__ = [
@@ -184,6 +184,10 @@ class EntryPoint(DeprecatedTuple):
following the attr, and following any extras.
"""
+ name: str
+ value: str
+ group: str
+
dist: Optional['Distribution'] = None
def __init__(self, name, value, group):
@@ -218,17 +222,6 @@ def _for(self, dist):
vars(self).update(dist=dist)
return self
- def __iter__(self):
- """
- Supply iter so one may construct dicts of EntryPoints by name.
- """
- msg = (
- "Construction of dict of EntryPoints is deprecated in "
- "favor of EntryPoints."
- )
- warnings.warn(msg, DeprecationWarning)
- return iter((self.name, self))
-
def matches(self, **params):
"""
EntryPoint matches the given parameters.
@@ -274,77 +267,7 @@ def __hash__(self):
return hash(self._key())
-class DeprecatedList(list):
- """
- Allow an otherwise immutable object to implement mutability
- for compatibility.
-
- >>> recwarn = getfixture('recwarn')
- >>> dl = DeprecatedList(range(3))
- >>> dl[0] = 1
- >>> dl.append(3)
- >>> del dl[3]
- >>> dl.reverse()
- >>> dl.sort()
- >>> dl.extend([4])
- >>> dl.pop(-1)
- 4
- >>> dl.remove(1)
- >>> dl += [5]
- >>> dl + [6]
- [1, 2, 5, 6]
- >>> dl + (6,)
- [1, 2, 5, 6]
- >>> dl.insert(0, 0)
- >>> dl
- [0, 1, 2, 5]
- >>> dl == [0, 1, 2, 5]
- True
- >>> dl == (0, 1, 2, 5)
- True
- >>> len(recwarn)
- 1
- """
-
- __slots__ = ()
-
- _warn = functools.partial(
- warnings.warn,
- "EntryPoints list interface is deprecated. Cast to list if needed.",
- DeprecationWarning,
- stacklevel=2,
- )
-
- def _wrap_deprecated_method(method_name: str): # type: ignore
- def wrapped(self, *args, **kwargs):
- self._warn()
- return getattr(super(), method_name)(*args, **kwargs)
-
- return method_name, wrapped
-
- locals().update(
- map(
- _wrap_deprecated_method,
- '__setitem__ __delitem__ append reverse extend pop remove '
- '__iadd__ insert sort'.split(),
- )
- )
-
- def __add__(self, other):
- if not isinstance(other, tuple):
- self._warn()
- other = tuple(other)
- return self.__class__(tuple(self) + other)
-
- def __eq__(self, other):
- if not isinstance(other, tuple):
- self._warn()
- other = tuple(other)
-
- return tuple(self).__eq__(other)
-
-
-class EntryPoints(DeprecatedList):
+class EntryPoints(tuple):
"""
An immutable collection of selectable EntryPoint objects.
"""
@@ -355,14 +278,6 @@ def __getitem__(self, name): # -> EntryPoint:
"""
Get the EntryPoint in self matching name.
"""
- if isinstance(name, int):
- warnings.warn(
- "Accessing entry points by index is deprecated. "
- "Cast to tuple if needed.",
- DeprecationWarning,
- stacklevel=2,
- )
- return super().__getitem__(name)
try:
return next(iter(self.select(name=name)))
except StopIteration:
@@ -386,10 +301,6 @@ def names(self):
def groups(self):
"""
Return the set of all groups of all entry points.
-
- For coverage while SelectableGroups is present.
- >>> EntryPoints().groups
- set()
"""
return {ep.group for ep in self}
@@ -405,101 +316,6 @@ def _from_text(text):
)
-class Deprecated:
- """
- Compatibility add-in for mapping to indicate that
- mapping behavior is deprecated.
-
- >>> recwarn = getfixture('recwarn')
- >>> class DeprecatedDict(Deprecated, dict): pass
- >>> dd = DeprecatedDict(foo='bar')
- >>> dd.get('baz', None)
- >>> dd['foo']
- 'bar'
- >>> list(dd)
- ['foo']
- >>> list(dd.keys())
- ['foo']
- >>> 'foo' in dd
- True
- >>> list(dd.values())
- ['bar']
- >>> len(recwarn)
- 1
- """
-
- _warn = functools.partial(
- warnings.warn,
- "SelectableGroups dict interface is deprecated. Use select.",
- DeprecationWarning,
- stacklevel=2,
- )
-
- def __getitem__(self, name):
- self._warn()
- return super().__getitem__(name)
-
- def get(self, name, default=None):
- self._warn()
- return super().get(name, default)
-
- def __iter__(self):
- self._warn()
- return super().__iter__()
-
- def __contains__(self, *args):
- self._warn()
- return super().__contains__(*args)
-
- def keys(self):
- self._warn()
- return super().keys()
-
- def values(self):
- self._warn()
- return super().values()
-
-
-class SelectableGroups(Deprecated, dict):
- """
- A backward- and forward-compatible result from
- entry_points that fully implements the dict interface.
- """
-
- @classmethod
- def load(cls, eps):
- by_group = operator.attrgetter('group')
- ordered = sorted(eps, key=by_group)
- grouped = itertools.groupby(ordered, by_group)
- return cls((group, EntryPoints(eps)) for group, eps in grouped)
-
- @property
- def _all(self):
- """
- Reconstruct a list of all entrypoints from the groups.
- """
- groups = super(Deprecated, self).values()
- return EntryPoints(itertools.chain.from_iterable(groups))
-
- @property
- def groups(self):
- return self._all.groups
-
- @property
- def names(self):
- """
- for coverage:
- >>> SelectableGroups().names
- set()
- """
- return self._all.names
-
- def select(self, **params):
- if not params:
- return self
- return self._all.select(**params)
-
-
class PackagePath(pathlib.PurePosixPath):
"""A reference to a path in a package"""
@@ -1013,27 +829,19 @@ def version(distribution_name):
"""
-def entry_points(**params) -> Union[EntryPoints, SelectableGroups]:
+def entry_points(**params) -> EntryPoints:
"""Return EntryPoint objects for all installed packages.
Pass selection parameters (group or name) to filter the
result to entry points matching those properties (see
EntryPoints.select()).
- For compatibility, returns ``SelectableGroups`` object unless
- selection parameters are supplied. In the future, this function
- will return ``EntryPoints`` instead of ``SelectableGroups``
- even when no selection parameters are supplied.
-
- For maximum future compatibility, pass selection parameters
- or invoke ``.select`` with parameters on the result.
-
- :return: EntryPoints or SelectableGroups for all installed packages.
+ :return: EntryPoints for all installed packages.
"""
eps = itertools.chain.from_iterable(
dist.entry_points for dist in _unique(distributions())
)
- return SelectableGroups.load(eps).select(**params)
+ return EntryPoints(eps).select(**params)
def files(distribution_name):
diff --git a/Lib/test/test_importlib/test_main.py b/Lib/test/test_importlib/test_main.py
index d9d067c4b23d66..30b68b6ae7d86e 100644
--- a/Lib/test/test_importlib/test_main.py
+++ b/Lib/test/test_importlib/test_main.py
@@ -1,8 +1,6 @@
import re
-import json
import pickle
import unittest
-import warnings
import importlib.metadata
try:
@@ -260,14 +258,6 @@ def test_hashable(self):
"""EntryPoints should be hashable"""
hash(self.ep)
- def test_json_dump(self):
- """
- json should not expect to be able to dump an EntryPoint
- """
- with self.assertRaises(Exception):
- with warnings.catch_warnings(record=True):
- json.dumps(self.ep)
-
def test_module(self):
assert self.ep.module == 'value'
diff --git a/Lib/test/test_importlib/test_metadata_api.py b/Lib/test/test_importlib/test_metadata_api.py
index 69c78e9820c044..71c47e62d27124 100644
--- a/Lib/test/test_importlib/test_metadata_api.py
+++ b/Lib/test/test_importlib/test_metadata_api.py
@@ -124,62 +124,6 @@ def test_entry_points_missing_name(self):
def test_entry_points_missing_group(self):
assert entry_points(group='missing') == ()
- def test_entry_points_dict_construction(self):
- """
- Prior versions of entry_points() returned simple lists and
- allowed casting those lists into maps by name using ``dict()``.
- Capture this now deprecated use-case.
- """
- with suppress_known_deprecation() as caught:
- eps = dict(entry_points(group='entries'))
-
- assert 'main' in eps
- assert eps['main'] == entry_points(group='entries')['main']
-
- # check warning
- expected = next(iter(caught))
- assert expected.category is DeprecationWarning
- assert "Construction of dict of EntryPoints is deprecated" in str(expected)
-
- def test_entry_points_by_index(self):
- """
- Prior versions of Distribution.entry_points would return a
- tuple that allowed access by index.
- Capture this now deprecated use-case
- See python/importlib_metadata#300 and bpo-44246.
- """
- eps = distribution('distinfo-pkg').entry_points
- with suppress_known_deprecation() as caught:
- eps[0]
-
- # check warning
- expected = next(iter(caught))
- assert expected.category is DeprecationWarning
- assert "Accessing entry points by index is deprecated" in str(expected)
-
- def test_entry_points_groups_getitem(self):
- """
- Prior versions of entry_points() returned a dict. Ensure
- that callers using '.__getitem__()' are supported but warned to
- migrate.
- """
- with suppress_known_deprecation():
- entry_points()['entries'] == entry_points(group='entries')
-
- with self.assertRaises(KeyError):
- entry_points()['missing']
-
- def test_entry_points_groups_get(self):
- """
- Prior versions of entry_points() returned a dict. Ensure
- that callers using '.get()' are supported but warned to
- migrate.
- """
- with suppress_known_deprecation():
- entry_points().get('missing', 'default') == 'default'
- entry_points().get('entries', 'default') == entry_points()['entries']
- entry_points().get('missing', ()) == ()
-
def test_entry_points_allows_no_attributes(self):
ep = entry_points().select(group='entries', name='main')
with self.assertRaises(AttributeError):
diff --git a/Misc/NEWS.d/next/Library/2022-10-03-13-25-19.gh-issue-97781.gCLLef.rst b/Misc/NEWS.d/next/Library/2022-10-03-13-25-19.gh-issue-97781.gCLLef.rst
new file mode 100644
index 00000000000000..8c36d9c3afd2dd
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2022-10-03-13-25-19.gh-issue-97781.gCLLef.rst
@@ -0,0 +1,5 @@
+Removed deprecated interfaces in ``importlib.metadata`` (entry points
+accessed as dictionary, implicit dictionary construction of sequence of
+``EntryPoint`` objects, mutablility of ``EntryPoints`` result, access of
+entry point by index). ``entry_points`` now has a simpler, more
+straightforward API (returning ``EntryPoints``).