Skip to content

Commit 97c2f68

Browse files
authored
[3.8] bpo-38086: Sync importlib.metadata with importlib_metadata 0.21. (GH-15840) (#15861)
https://gitlab.com/python-devs/importlib_metadata/-/tags/0.21. (cherry picked from commit 17499d8) Co-authored-by: Jason R. Coombs <jaraco@jaraco.com>
1 parent 313f801 commit 97c2f68

File tree

8 files changed

+709
-658
lines changed

8 files changed

+709
-658
lines changed

Doc/library/importlib.metadata.rst

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -172,10 +172,10 @@ Distribution requirements
172172
-------------------------
173173

174174
To get the full set of requirements for a distribution, use the ``requires()``
175-
function. Note that this returns an iterator::
175+
function::
176176

177-
>>> list(requires('wheel')) # doctest: +SKIP
178-
["pytest (>=3.0.0) ; extra == 'test'"]
177+
>>> requires('wheel') # doctest: +SKIP
178+
["pytest (>=3.0.0) ; extra == 'test'", "pytest-cov ; extra == 'test'"]
179179

180180

181181
Distributions
@@ -224,23 +224,25 @@ The abstract class :py:class:`importlib.abc.MetaPathFinder` defines the
224224
interface expected of finders by Python's import system.
225225
``importlib.metadata`` extends this protocol by looking for an optional
226226
``find_distributions`` callable on the finders from
227-
``sys.meta_path``. If the finder has this method, it must return
228-
an iterator over instances of the ``Distribution`` abstract class. This
229-
method must have the signature::
227+
``sys.meta_path`` and presents this extended interface as the
228+
``DistributionFinder`` abstract base class, which defines this abstract
229+
method::
230230

231-
def find_distributions(name=None, path=None):
231+
@abc.abstractmethod
232+
def find_distributions(context=DistributionFinder.Context()):
232233
"""Return an iterable of all Distribution instances capable of
233-
loading the metadata for packages matching the name
234-
(or all names if not supplied) along the paths in the list
235-
of directories ``path`` (defaults to sys.path).
234+
loading the metadata for packages for the indicated ``context``.
236235
"""
237236

237+
The ``DistributionFinder.Context`` object provides ``.path`` and ``.name``
238+
properties indicating the path to search and names to match and may
239+
supply other relevant context.
240+
238241
What this means in practice is that to support finding distribution package
239242
metadata in locations other than the file system, you should derive from
240-
``Distribution`` and implement the ``load_metadata()`` method. This takes a
241-
single argument which is the name of the package whose metadata is being
242-
found. This instance of the ``Distribution`` base abstract class is what your
243-
finder's ``find_distributions()`` method should return.
243+
``Distribution`` and implement the ``load_metadata()`` method. Then from
244+
your finder, return instances of this derived ``Distribution`` in the
245+
``find_distributions()`` method.
244246

245247

246248
.. _`entry point API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points

Doc/tools/susp-ignored.csv

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,4 +350,4 @@ whatsnew/changelog,,::,error::BytesWarning
350350
whatsnew/changelog,,::,default::BytesWarning
351351
whatsnew/changelog,,::,default::DeprecationWarning
352352
library/importlib.metadata,,:main,"EntryPoint(name='wheel', value='wheel.cli:main', group='console_scripts')"
353-
library/importlib.metadata,,`,of directories ``path`` (defaults to sys.path).
353+
library/importlib.metadata,,`,loading the metadata for packages for the indicated ``context``.

Lib/importlib/_bootstrap_external.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1369,21 +1369,19 @@ def find_module(cls, fullname, path=None):
13691369
return spec.loader
13701370

13711371
@classmethod
1372-
def find_distributions(cls, name=None, path=None):
1372+
def find_distributions(self, context=None):
13731373
"""
13741374
Find distributions.
13751375
13761376
Return an iterable of all Distribution instances capable of
1377-
loading the metadata for packages matching the ``name``
1378-
(or all names if not supplied) along the paths in the list
1379-
of directories ``path`` (defaults to sys.path).
1377+
loading the metadata for packages matching ``context.name``
1378+
(or all names if ``None`` indicated) along the paths in the list
1379+
of directories ``context.path``.
13801380
"""
1381-
import re
1382-
from importlib.metadata import PathDistribution
1383-
if path is None:
1384-
path = sys.path
1385-
pattern = '.*' if name is None else re.escape(name)
1386-
found = cls._search_paths(pattern, path)
1381+
from importlib.metadata import PathDistribution, DistributionFinder
1382+
if context is None:
1383+
context = DistributionFinder.Context()
1384+
found = self._search_paths(context.pattern, context.path)
13871385
return map(PathDistribution, found)
13881386

13891387
@classmethod

Lib/importlib/metadata.py

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
__all__ = [
2121
'Distribution',
22+
'DistributionFinder',
2223
'PackageNotFoundError',
2324
'distribution',
2425
'distributions',
@@ -158,24 +159,41 @@ def from_name(cls, name):
158159
metadata cannot be found.
159160
"""
160161
for resolver in cls._discover_resolvers():
161-
dists = resolver(name)
162+
dists = resolver(DistributionFinder.Context(name=name))
162163
dist = next(dists, None)
163164
if dist is not None:
164165
return dist
165166
else:
166167
raise PackageNotFoundError(name)
167168

168169
@classmethod
169-
def discover(cls):
170+
def discover(cls, **kwargs):
170171
"""Return an iterable of Distribution objects for all packages.
171172
173+
Pass a ``context`` or pass keyword arguments for constructing
174+
a context.
175+
176+
:context: A ``DistributionFinder.Context`` object.
172177
:return: Iterable of Distribution objects for all packages.
173178
"""
179+
context = kwargs.pop('context', None)
180+
if context and kwargs:
181+
raise ValueError("cannot accept context and kwargs")
182+
context = context or DistributionFinder.Context(**kwargs)
174183
return itertools.chain.from_iterable(
175-
resolver()
184+
resolver(context)
176185
for resolver in cls._discover_resolvers()
177186
)
178187

188+
@staticmethod
189+
def at(path):
190+
"""Return a Distribution for the indicated metadata path
191+
192+
:param path: a string or path-like object
193+
:return: a concrete Distribution instance for the path
194+
"""
195+
return PathDistribution(pathlib.Path(path))
196+
179197
@staticmethod
180198
def _discover_resolvers():
181199
"""Search the meta_path for resolvers."""
@@ -215,7 +233,7 @@ def entry_points(self):
215233
def files(self):
216234
"""Files in this distribution.
217235
218-
:return: Iterable of PackagePath for this distribution or None
236+
:return: List of PackagePath for this distribution or None
219237
220238
Result is `None` if the metadata file that enumerates files
221239
(i.e. RECORD for dist-info or SOURCES.txt for egg-info) is
@@ -231,7 +249,7 @@ def make_file(name, hash=None, size_str=None):
231249
result.dist = self
232250
return result
233251

234-
return file_lines and starmap(make_file, csv.reader(file_lines))
252+
return file_lines and list(starmap(make_file, csv.reader(file_lines)))
235253

236254
def _read_files_distinfo(self):
237255
"""
@@ -251,7 +269,8 @@ def _read_files_egginfo(self):
251269
@property
252270
def requires(self):
253271
"""Generated requirements specified for this Distribution"""
254-
return self._read_dist_info_reqs() or self._read_egg_info_reqs()
272+
reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs()
273+
return reqs and list(reqs)
255274

256275
def _read_dist_info_reqs(self):
257276
return self.metadata.get_all('Requires-Dist')
@@ -312,15 +331,35 @@ class DistributionFinder(MetaPathFinder):
312331
A MetaPathFinder capable of discovering installed distributions.
313332
"""
314333

334+
class Context:
335+
336+
name = None
337+
"""
338+
Specific name for which a distribution finder should match.
339+
"""
340+
341+
def __init__(self, **kwargs):
342+
vars(self).update(kwargs)
343+
344+
@property
345+
def path(self):
346+
"""
347+
The path that a distribution finder should search.
348+
"""
349+
return vars(self).get('path', sys.path)
350+
351+
@property
352+
def pattern(self):
353+
return '.*' if self.name is None else re.escape(self.name)
354+
315355
@abc.abstractmethod
316-
def find_distributions(self, name=None, path=None):
356+
def find_distributions(self, context=Context()):
317357
"""
318358
Find distributions.
319359
320360
Return an iterable of all Distribution instances capable of
321-
loading the metadata for packages matching the ``name``
322-
(or all names if not supplied) along the paths in the list
323-
of directories ``path`` (defaults to sys.path).
361+
loading the metadata for packages matching the ``context``,
362+
a DistributionFinder.Context instance.
324363
"""
325364

326365

@@ -352,12 +391,12 @@ def distribution(package):
352391
return Distribution.from_name(package)
353392

354393

355-
def distributions():
394+
def distributions(**kwargs):
356395
"""Get all ``Distribution`` instances in the current environment.
357396
358397
:return: An iterable of ``Distribution`` instances.
359398
"""
360-
return Distribution.discover()
399+
return Distribution.discover(**kwargs)
361400

362401

363402
def metadata(package):

Lib/test/test_importlib/test_main.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,10 @@ def test_package_discovery(self):
162162
for dist in dists
163163
)
164164

165+
def test_invalid_usage(self):
166+
with self.assertRaises(ValueError):
167+
list(distributions(context='something', name='else'))
168+
165169

166170
class DirectoryTest(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
167171
def test_egg_info(self):

Lib/test/test_importlib/test_metadata_api.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import re
22
import textwrap
33
import unittest
4-
import itertools
54

65
from collections.abc import Iterator
76

@@ -61,9 +60,7 @@ def test_metadata_for_this_package(self):
6160
assert 'Topic :: Software Development :: Libraries' in classifiers
6261

6362
@staticmethod
64-
def _test_files(files_iter):
65-
assert isinstance(files_iter, Iterator), files_iter
66-
files = list(files_iter)
63+
def _test_files(files):
6764
root = files[0].root
6865
for file in files:
6966
assert file.root == root
@@ -99,16 +96,18 @@ def test_requires_egg_info_file(self):
9996
requirements = requires('egginfo-file')
10097
self.assertIsNone(requirements)
10198

102-
def test_requires(self):
99+
def test_requires_egg_info(self):
103100
deps = requires('egginfo-pkg')
101+
assert len(deps) == 2
104102
assert any(
105103
dep == 'wheel >= 1.0; python_version >= "2.7"'
106104
for dep in deps
107105
)
108106

109107
def test_requires_dist_info(self):
110-
deps = list(requires('distinfo-pkg'))
111-
assert deps and all(deps)
108+
deps = requires('distinfo-pkg')
109+
assert len(deps) == 2
110+
assert all(deps)
112111
assert 'wheel >= 1.0' in deps
113112
assert "pytest; extra == 'test'" in deps
114113

@@ -143,11 +142,20 @@ def test_more_complex_deps_requires_text(self):
143142

144143
class OffSysPathTests(fixtures.DistInfoPkgOffPath, unittest.TestCase):
145144
def test_find_distributions_specified_path(self):
146-
dists = itertools.chain.from_iterable(
147-
resolver(path=[str(self.site_dir)])
148-
for resolver in Distribution._discover_resolvers()
149-
)
145+
dists = Distribution.discover(path=[str(self.site_dir)])
150146
assert any(
151147
dist.metadata['Name'] == 'distinfo-pkg'
152148
for dist in dists
153149
)
150+
151+
def test_distribution_at_pathlib(self):
152+
"""Demonstrate how to load metadata direct from a directory.
153+
"""
154+
dist_info_path = self.site_dir / 'distinfo_pkg-1.0.0.dist-info'
155+
dist = Distribution.at(dist_info_path)
156+
assert dist.version == '1.0.0'
157+
158+
def test_distribution_at_str(self):
159+
dist_info_path = self.site_dir / 'distinfo_pkg-1.0.0.dist-info'
160+
dist = Distribution.at(str(dist_info_path))
161+
assert dist.version == '1.0.0'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Update importlib.metadata with changes from `importlib_metadata 0.21 <https://gitlab.com/python-devs/importlib_metadata/blob/0.21/importlib_metadata/docs/changelog.rst>`_.

0 commit comments

Comments
 (0)