[Python-checkins] gh-97781: Apply changes from importlib_metadata 5. (GH-97785)

jaraco webhook-mailer at python.org
Thu Oct 6 15:25:33 EDT 2022


https://github.com/python/cpython/commit/8af04cdef202364541540ed67e204b71e2e759d0
commit: 8af04cdef202364541540ed67e204b71e2e759d0
branch: main
author: Jason R. Coombs <jaraco at jaraco.com>
committer: jaraco <jaraco at jaraco.com>
date: 2022-10-06T15:25:24-04:00
summary:

gh-97781: Apply changes from importlib_metadata 5. (GH-97785)

* gh-97781: Apply changes from importlib_metadata 5.

* Apply changes from upstream

* Apply changes from upstream.

files:
A Misc/NEWS.d/next/Library/2022-10-03-13-25-19.gh-issue-97781.gCLLef.rst
M Doc/library/importlib.metadata.rst
M Lib/importlib/metadata/__init__.py
M Lib/test/test_importlib/test_main.py
M Lib/test/test_importlib/test_metadata_api.py

diff --git a/Doc/library/importlib.metadata.rst b/Doc/library/importlib.metadata.rst
index a1af7a754ba4..094c2688a8cd 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 `Distribution Package <https://packaging.python.org/en/latest/glossary/#term-Distribution-Package>`_,
+such as its entry points
+or its top-level names (`Import Package <https://packaging.python.org/en/latest/glossary/#term-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
-<https://pypi.org/project/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 <https://pypi.org/project/pip/>`_.
+Specifically, it works with distributions with discoverable
+``dist-info`` or ``egg-info`` directories,
+and metadata defined by the `Core metadata specifications <https://packaging.python.org/en/latest/specifications/core-metadata/#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() <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
+      <https://importlib-metadata.readthedocs.io/en/latest/api.html>`__
+      for this module's classes and functions,
+      as well as a `migration guide
+      <https://importlib-metadata.readthedocs.io/en/latest/migration.html>`__
+      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
+`Distribution Package <https://packaging.python.org/en/latest/glossary/#term-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 <https://pypi.org/project/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 `Distribution Package <https://packaging.python.org/en/latest/glossary/#term-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
+`Distribution Package <https://packaging.python.org/en/latest/glossary/#term-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 `Distribution Package <https://packaging.python.org/en/latest/glossary/#term-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 `Distribution Package <https://packaging.python.org/en/latest/glossary/#term-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 `Distribution Package <https://packaging.python.org/en/latest/glossary/#term-Distribution-Package>`_
+name (or names, in the case of a namespace package)
+that provide each importable top-level
+Python module or `Import Package <https://packaging.python.org/en/latest/glossary/#term-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 `Distribution Package <https://packaging.python.org/en/latest/glossary/#term-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 `Core metadata specifications <https://packaging.python.org/en/latest/specifications/core-metadata/#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 `Distribution Package <https://packaging.python.org/en/latest/glossary/#term-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 <finders-and-loaders>`.  To find a distribution package's metadata,
+Because `Distribution Package <https://packaging.python.org/en/latest/glossary/#term-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 <meta path finder>` 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.
@@ -358,4 +396,4 @@ a custom finder, return instances of this derived ``Distribution`` in the
 
 .. _`entry point API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points
 .. _`metadata API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#metadata-api
-.. _`importlib_resources`: https://importlib-resources.readthedocs.io/en/latest/index.html
+.. _`finders`: https://docs.python.org/3/reference/import.html#finders-and-loaders
diff --git a/Lib/importlib/metadata/__init__.py b/Lib/importlib/metadata/__init__.py
index b01de145c336..40ab1a1aaac3 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__ = [
@@ -134,6 +134,7 @@ class DeprecatedTuple:
     1
     """
 
+    # Do not remove prior to 2023-05-01 or Python 3.13
     _warn = functools.partial(
         warnings.warn,
         "EntryPoint tuple interface is deprecated. Access members by name.",
@@ -184,6 +185,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 +223,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 +268,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 +279,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 +302,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 +317,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 +830,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 d9d067c4b23d..30b68b6ae7d8 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 69c78e9820c0..71c47e62d271 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 000000000000..8c36d9c3afd2
--- /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``).



More information about the Python-checkins mailing list