Skip to content

Commit 07734a4

Browse files
authored
[3.11] gh-98706: Sync with importlib_metadata 4.13.0. (GH-98875)
These changes are already applied to main but have been selected from importlib_metadata 4.x for their bug fixes.
1 parent 46a493e commit 07734a4

File tree

6 files changed

+209
-93
lines changed

6 files changed

+209
-93
lines changed

Doc/library/importlib.metadata.rst

+96-34
Original file line numberDiff line numberDiff line change
@@ -13,29 +13,61 @@
1313

1414
**Source code:** :source:`Lib/importlib/metadata/__init__.py`
1515

16-
``importlib.metadata`` is a library that provides for access to installed
17-
package metadata. Built in part on Python's import system, this library
16+
``importlib_metadata`` is a library that provides access to
17+
the metadata of an installed `Distribution Package <https://packaging.python.org/en/latest/glossary/#term-Distribution-Package>`_,
18+
such as its entry points
19+
or its top-level names (`Import Package <https://packaging.python.org/en/latest/glossary/#term-Import-Package>`_\s, modules, if any).
20+
Built in part on Python's import system, this library
1821
intends to replace similar functionality in the `entry point
1922
API`_ and `metadata API`_ of ``pkg_resources``. Along with
20-
:mod:`importlib.resources` (with new features backported to the
21-
`importlib_resources`_ package), this can eliminate the need to use the older
22-
and less efficient
23+
:mod:`importlib.resources`,
24+
this package can eliminate the need to use the older and less efficient
2325
``pkg_resources`` package.
2426

25-
By "installed package" we generally mean a third-party package installed into
26-
Python's ``site-packages`` directory via tools such as `pip
27-
<https://pypi.org/project/pip/>`_. Specifically,
28-
it means a package with either a discoverable ``dist-info`` or ``egg-info``
29-
directory, and metadata defined by :pep:`566` or its older specifications.
30-
By default, package metadata can live on the file system or in zip archives on
27+
``importlib_metadata`` operates on third-party *distribution packages*
28+
installed into Python's ``site-packages`` directory via tools such as
29+
`pip <https://pypi.org/project/pip/>`_.
30+
Specifically, it works with distributions with discoverable
31+
``dist-info`` or ``egg-info`` directories,
32+
and metadata defined by the `Core metadata specifications <https://packaging.python.org/en/latest/specifications/core-metadata/#core-metadata>`_.
33+
34+
.. important::
35+
36+
These are *not* necessarily equivalent to or correspond 1:1 with
37+
the top-level *import package* names
38+
that can be imported inside Python code.
39+
One *distribution package* can contain multiple *import packages*
40+
(and single modules),
41+
and one top-level *import package*
42+
may map to multiple *distribution packages*
43+
if it is a namespace package.
44+
You can use :ref:`package_distributions() <package-distributions>`
45+
to get a mapping between them.
46+
47+
By default, distribution metadata can live on the file system
48+
or in zip archives on
3149
:data:`sys.path`. Through an extension mechanism, the metadata can live almost
3250
anywhere.
3351

3452

53+
.. seealso::
54+
55+
https://importlib-metadata.readthedocs.io/
56+
The documentation for ``importlib_metadata``, which supplies a
57+
backport of ``importlib.metadata``.
58+
This includes an `API reference
59+
<https://importlib-metadata.readthedocs.io/en/latest/api.html>`__
60+
for this module's classes and functions,
61+
as well as a `migration guide
62+
<https://importlib-metadata.readthedocs.io/en/latest/migration.html>`__
63+
for existing users of ``pkg_resources``.
64+
65+
3566
Overview
3667
========
3768

38-
Let's say you wanted to get the version string for a package you've installed
69+
Let's say you wanted to get the version string for a
70+
`Distribution Package <https://packaging.python.org/en/latest/glossary/#term-Distribution-Package>`_ you've installed
3971
using ``pip``. We start by creating a virtual environment and installing
4072
something into it:
4173

@@ -54,9 +86,9 @@ You can get the version string for ``wheel`` by running the following:
5486
>>> version('wheel') # doctest: +SKIP
5587
'0.32.3'
5688
57-
You can also get the set of entry points keyed by group, such as
89+
You can also get a collection of entry points selectable by properties of the EntryPoint (typically 'group' or 'name'), such as
5890
``console_scripts``, ``distutils.commands`` and others. Each group contains a
59-
sequence of :ref:`EntryPoint <entry-points>` objects.
91+
collection of :ref:`EntryPoint <entry-points>` objects.
6092

6193
You can get the :ref:`metadata for a distribution <metadata>`::
6294

@@ -91,7 +123,7 @@ Query all entry points::
91123
>>> eps = entry_points() # doctest: +SKIP
92124

93125
The ``entry_points()`` function returns an ``EntryPoints`` object,
94-
a sequence of all ``EntryPoint`` objects with ``names`` and ``groups``
126+
a collection of all ``EntryPoint`` objects with ``names`` and ``groups``
95127
attributes for convenience::
96128

97129
>>> sorted(eps.groups) # doctest: +SKIP
@@ -156,7 +188,8 @@ interface to retrieve entry points by group.
156188
Distribution metadata
157189
---------------------
158190

159-
Every distribution includes some metadata, which you can extract using the
191+
Every `Distribution Package <https://packaging.python.org/en/latest/glossary/#term-Distribution-Package>`_ includes some metadata,
192+
which you can extract using the
160193
``metadata()`` function::
161194

162195
>>> wheel_metadata = metadata('wheel') # doctest: +SKIP
@@ -174,6 +207,13 @@ all the metadata in a JSON-compatible form per :PEP:`566`::
174207
>>> wheel_metadata.json['requires_python']
175208
'>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*'
176209

210+
.. note::
211+
212+
The actual type of the object returned by ``metadata()`` is an
213+
implementation detail and should be accessed only through the interface
214+
described by the
215+
`PackageMetadata protocol <https://importlib-metadata.readthedocs.io/en/latest/api.html#importlib_metadata.PackageMetadata>`_.
216+
177217
.. versionchanged:: 3.10
178218
The ``Description`` is now included in the metadata when presented
179219
through the payload. Line continuation characters have been removed.
@@ -187,7 +227,8 @@ all the metadata in a JSON-compatible form per :PEP:`566`::
187227
Distribution versions
188228
---------------------
189229

190-
The ``version()`` function is the quickest way to get a distribution's version
230+
The ``version()`` function is the quickest way to get a
231+
`Distribution Package <https://packaging.python.org/en/latest/glossary/#term-Distribution-Package>`_'s version
191232
number, as a string::
192233

193234
>>> version('wheel') # doctest: +SKIP
@@ -200,7 +241,8 @@ Distribution files
200241
------------------
201242

202243
You can also get the full set of files contained within a distribution. The
203-
``files()`` function takes a distribution package name and returns all of the
244+
``files()`` function takes a `Distribution Package <https://packaging.python.org/en/latest/glossary/#term-Distribution-Package>`_ name
245+
and returns all of the
204246
files installed by this distribution. Each file object returned is a
205247
``PackagePath``, a :class:`pathlib.PurePath` derived object with additional ``dist``,
206248
``size``, and ``hash`` properties as indicated by the metadata. For example::
@@ -245,19 +287,24 @@ distribution is not known to have the metadata present.
245287
Distribution requirements
246288
-------------------------
247289

248-
To get the full set of requirements for a distribution, use the ``requires()``
290+
To get the full set of requirements for a `Distribution Package <https://packaging.python.org/en/latest/glossary/#term-Distribution-Package>`_,
291+
use the ``requires()``
249292
function::
250293

251294
>>> requires('wheel') # doctest: +SKIP
252295
["pytest (>=3.0.0) ; extra == 'test'", "pytest-cov ; extra == 'test'"]
253296

254297

255-
Package distributions
256-
---------------------
298+
.. _package-distributions:
299+
.. _import-distribution-package-mapping:
257300

258-
A convenience method to resolve the distribution or
259-
distributions (in the case of a namespace package) for top-level
260-
Python packages or modules::
301+
Mapping import to distribution packages
302+
---------------------------------------
303+
304+
A convenience method to resolve the `Distribution Package <https://packaging.python.org/en/latest/glossary/#term-Distribution-Package>`_
305+
name (or names, in the case of a namespace package)
306+
that provide each importable top-level
307+
Python module or `Import Package <https://packaging.python.org/en/latest/glossary/#term-Import-Package>`_::
261308

262309
>>> packages_distributions()
263310
{'importlib_metadata': ['importlib-metadata'], 'yaml': ['PyYAML'], 'jaraco': ['jaraco.classes', 'jaraco.functools'], ...}
@@ -271,7 +318,8 @@ Distributions
271318

272319
While the above API is the most common and convenient usage, you can get all
273320
of that information from the ``Distribution`` class. A ``Distribution`` is an
274-
abstract object that represents the metadata for a Python package. You can
321+
abstract object that represents the metadata for
322+
a Python `Distribution Package <https://packaging.python.org/en/latest/glossary/#term-Distribution-Package>`_. You can
275323
get the ``Distribution`` instance::
276324

277325
>>> from importlib.metadata import distribution # doctest: +SKIP
@@ -291,22 +339,36 @@ instance::
291339
>>> dist.metadata['License'] # doctest: +SKIP
292340
'MIT'
293341

294-
The full set of available metadata is not described here. See :pep:`566`
295-
for additional details.
342+
The full set of available metadata is not described here.
343+
See the `Core metadata specifications <https://packaging.python.org/en/latest/specifications/core-metadata/#core-metadata>`_ for additional details.
344+
345+
346+
Distribution Discovery
347+
======================
348+
349+
By default, this package provides built-in support for discovery of metadata
350+
for file system and zip file `Distribution Package <https://packaging.python.org/en/latest/glossary/#term-Distribution-Package>`_\s.
351+
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:
352+
353+
- ``importlib.metadata`` does not honor :class:`bytes` objects on ``sys.path``.
354+
- ``importlib.metadata`` will incidentally honor :py:class:`pathlib.Path` objects on ``sys.path`` even though such values will be ignored for imports.
296355

297356

298357
Extending the search algorithm
299358
==============================
300359

301-
Because package metadata is not available through :data:`sys.path` searches, or
302-
package loaders directly, the metadata for a package is found through import
303-
system :ref:`finders <finders-and-loaders>`. To find a distribution package's metadata,
360+
Because `Distribution Package <https://packaging.python.org/en/latest/glossary/#term-Distribution-Package>`_ metadata
361+
is not available through :data:`sys.path` searches, or
362+
package loaders directly,
363+
the metadata for a distribution is found through import
364+
system `finders`_. To find a distribution package's metadata,
304365
``importlib.metadata`` queries the list of :term:`meta path finders <meta path finder>` on
305366
:data:`sys.meta_path`.
306367

307-
The default ``PathFinder`` for Python includes a hook that calls into
308-
``importlib.metadata.MetadataPathFinder`` for finding distributions
309-
loaded from typical file-system-based paths.
368+
By default ``importlib_metadata`` installs a finder for distribution packages
369+
found on the file system.
370+
This finder doesn't actually find any *distributions*,
371+
but it can find their metadata.
310372

311373
The abstract class :py:class:`importlib.abc.MetaPathFinder` defines the
312374
interface expected of finders by Python's import system.
@@ -335,4 +397,4 @@ a custom finder, return instances of this derived ``Distribution`` in the
335397

336398
.. _`entry point API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points
337399
.. _`metadata API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#metadata-api
338-
.. _`importlib_resources`: https://importlib-resources.readthedocs.io/en/latest/index.html
400+
.. _`finders`: https://docs.python.org/3/reference/import.html#finders-and-loaders

Lib/importlib/metadata/__init__.py

+38-14
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,10 @@ class EntryPoint(DeprecatedTuple):
184184
following the attr, and following any extras.
185185
"""
186186

187+
name: str
188+
value: str
189+
group: str
190+
187191
dist: Optional['Distribution'] = None
188192

189193
def __init__(self, name, value, group):
@@ -543,21 +547,21 @@ def locate_file(self, path):
543547
"""
544548

545549
@classmethod
546-
def from_name(cls, name):
550+
def from_name(cls, name: str):
547551
"""Return the Distribution for the given package name.
548552
549553
:param name: The name of the distribution package to search for.
550554
:return: The Distribution instance (or subclass thereof) for the named
551555
package, if found.
552556
:raises PackageNotFoundError: When the named package's distribution
553557
metadata cannot be found.
558+
:raises ValueError: When an invalid value is supplied for name.
554559
"""
555-
for resolver in cls._discover_resolvers():
556-
dists = resolver(DistributionFinder.Context(name=name))
557-
dist = next(iter(dists), None)
558-
if dist is not None:
559-
return dist
560-
else:
560+
if not name:
561+
raise ValueError("A distribution name is required.")
562+
try:
563+
return next(cls.discover(name=name))
564+
except StopIteration:
561565
raise PackageNotFoundError(name)
562566

563567
@classmethod
@@ -945,13 +949,26 @@ def _normalized_name(self):
945949
normalized name from the file system path.
946950
"""
947951
stem = os.path.basename(str(self._path))
948-
return self._name_from_stem(stem) or super()._normalized_name
952+
return (
953+
pass_none(Prepared.normalize)(self._name_from_stem(stem))
954+
or super()._normalized_name
955+
)
949956

950-
def _name_from_stem(self, stem):
951-
name, ext = os.path.splitext(stem)
957+
@staticmethod
958+
def _name_from_stem(stem):
959+
"""
960+
>>> PathDistribution._name_from_stem('foo-3.0.egg-info')
961+
'foo'
962+
>>> PathDistribution._name_from_stem('CherryPy-3.0.dist-info')
963+
'CherryPy'
964+
>>> PathDistribution._name_from_stem('face.egg-info')
965+
'face'
966+
>>> PathDistribution._name_from_stem('foo.bar')
967+
"""
968+
filename, ext = os.path.splitext(stem)
952969
if ext not in ('.dist-info', '.egg-info'):
953970
return
954-
name, sep, rest = stem.partition('-')
971+
name, sep, rest = filename.partition('-')
955972
return name
956973

957974

@@ -991,6 +1008,15 @@ def version(distribution_name):
9911008
return distribution(distribution_name).version
9921009

9931010

1011+
_unique = functools.partial(
1012+
unique_everseen,
1013+
key=operator.attrgetter('_normalized_name'),
1014+
)
1015+
"""
1016+
Wrapper for ``distributions`` to return unique distributions by name.
1017+
"""
1018+
1019+
9941020
def entry_points(**params) -> Union[EntryPoints, SelectableGroups]:
9951021
"""Return EntryPoint objects for all installed packages.
9961022
@@ -1008,10 +1034,8 @@ def entry_points(**params) -> Union[EntryPoints, SelectableGroups]:
10081034
10091035
:return: EntryPoints or SelectableGroups for all installed packages.
10101036
"""
1011-
norm_name = operator.attrgetter('_normalized_name')
1012-
unique = functools.partial(unique_everseen, key=norm_name)
10131037
eps = itertools.chain.from_iterable(
1014-
dist.entry_points for dist in unique(distributions())
1038+
dist.entry_points for dist in _unique(distributions())
10151039
)
10161040
return SelectableGroups.load(eps).select(**params)
10171041

Lib/test/test_importlib/fixtures.py

+16
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import pathlib
66
import tempfile
77
import textwrap
8+
import functools
89
import contextlib
910

1011
from test.support.os_helper import FS_NONASCII
@@ -296,3 +297,18 @@ def setUp(self):
296297
# Add self.zip_name to the front of sys.path.
297298
self.resources = contextlib.ExitStack()
298299
self.addCleanup(self.resources.close)
300+
301+
302+
def parameterize(*args_set):
303+
"""Run test method with a series of parameters."""
304+
305+
def wrapper(func):
306+
@functools.wraps(func)
307+
def _inner(self):
308+
for args in args_set:
309+
with self.subTest(**args):
310+
func(self, **args)
311+
312+
return _inner
313+
314+
return wrapper

0 commit comments

Comments
 (0)