bpo-38086: Sync importlib.metadata with importlib_metadata 0.21. (GH-15840)

https://gitlab.com/python-devs/importlib_metadata/-/tags/0.21
This commit is contained in:
Jason R. Coombs 2019-09-10 14:53:31 +01:00 committed by Brett Cannon
parent 97d7906e30
commit 17499d8270
8 changed files with 712 additions and 661 deletions

View File

@ -172,10 +172,10 @@ Distribution requirements
-------------------------
To get the full set of requirements for a distribution, use the ``requires()``
function. Note that this returns an iterator::
function::
>>> list(requires('wheel')) # doctest: +SKIP
["pytest (>=3.0.0) ; extra == 'test'"]
>>> requires('wheel') # doctest: +SKIP
["pytest (>=3.0.0) ; extra == 'test'", "pytest-cov ; extra == 'test'"]
Distributions
@ -224,23 +224,25 @@ The abstract class :py:class:`importlib.abc.MetaPathFinder` defines the
interface expected of finders by Python's import system.
``importlib.metadata`` extends this protocol by looking for an optional
``find_distributions`` callable on the finders from
``sys.meta_path``. If the finder has this method, it must return
an iterator over instances of the ``Distribution`` abstract class. This
method must have the signature::
``sys.meta_path`` and presents this extended interface as the
``DistributionFinder`` abstract base class, which defines this abstract
method::
def find_distributions(name=None, path=None):
@abc.abstractmethod
def find_distributions(context=DistributionFinder.Context()):
"""Return an iterable of all Distribution instances capable of
loading the metadata for packages matching the name
(or all names if not supplied) along the paths in the list
of directories ``path`` (defaults to sys.path).
loading the metadata for packages for the indicated ``context``.
"""
The ``DistributionFinder.Context`` object provides ``.path`` and ``.name``
properties indicating the path to search and names to match and may
supply other relevant context.
What this means in practice is that to support finding distribution package
metadata in locations other than the file system, you should derive from
``Distribution`` and implement the ``load_metadata()`` method. This takes a
single argument which is the name of the package whose metadata is being
found. This instance of the ``Distribution`` base abstract class is what your
finder's ``find_distributions()`` method should return.
``Distribution`` and implement the ``load_metadata()`` method. Then from
your finder, return instances of this derived ``Distribution`` in the
``find_distributions()`` method.
.. _`entry point API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points

View File

@ -352,4 +352,4 @@ whatsnew/changelog,,::,error::BytesWarning
whatsnew/changelog,,::,default::BytesWarning
whatsnew/changelog,,::,default::DeprecationWarning
library/importlib.metadata,,:main,"EntryPoint(name='wheel', value='wheel.cli:main', group='console_scripts')"
library/importlib.metadata,,`,of directories ``path`` (defaults to sys.path).
library/importlib.metadata,,`,loading the metadata for packages for the indicated ``context``.

1 c-api/arg :ref PyArg_ParseTuple(args, "O|O:ref", &object, &callback)
352 whatsnew/changelog :: default::BytesWarning
353 whatsnew/changelog :: default::DeprecationWarning
354 library/importlib.metadata :main EntryPoint(name='wheel', value='wheel.cli:main', group='console_scripts')
355 library/importlib.metadata ` of directories ``path`` (defaults to sys.path). loading the metadata for packages for the indicated ``context``.

View File

@ -1370,21 +1370,19 @@ class PathFinder:
return spec.loader
@classmethod
def find_distributions(cls, name=None, path=None):
def find_distributions(self, context=None):
"""
Find distributions.
Return an iterable of all Distribution instances capable of
loading the metadata for packages matching the ``name``
(or all names if not supplied) along the paths in the list
of directories ``path`` (defaults to sys.path).
loading the metadata for packages matching ``context.name``
(or all names if ``None`` indicated) along the paths in the list
of directories ``context.path``.
"""
import re
from importlib.metadata import PathDistribution
if path is None:
path = sys.path
pattern = '.*' if name is None else re.escape(name)
found = cls._search_paths(pattern, path)
from importlib.metadata import PathDistribution, DistributionFinder
if context is None:
context = DistributionFinder.Context()
found = self._search_paths(context.pattern, context.path)
return map(PathDistribution, found)
@classmethod

View File

@ -19,6 +19,7 @@ from itertools import starmap
__all__ = [
'Distribution',
'DistributionFinder',
'PackageNotFoundError',
'distribution',
'distributions',
@ -158,7 +159,7 @@ class Distribution:
metadata cannot be found.
"""
for resolver in cls._discover_resolvers():
dists = resolver(name)
dists = resolver(DistributionFinder.Context(name=name))
dist = next(dists, None)
if dist is not None:
return dist
@ -166,16 +167,33 @@ class Distribution:
raise PackageNotFoundError(name)
@classmethod
def discover(cls):
def discover(cls, **kwargs):
"""Return an iterable of Distribution objects for all packages.
Pass a ``context`` or pass keyword arguments for constructing
a context.
:context: A ``DistributionFinder.Context`` object.
:return: Iterable of Distribution objects for all packages.
"""
context = kwargs.pop('context', None)
if context and kwargs:
raise ValueError("cannot accept context and kwargs")
context = context or DistributionFinder.Context(**kwargs)
return itertools.chain.from_iterable(
resolver()
resolver(context)
for resolver in cls._discover_resolvers()
)
@staticmethod
def at(path):
"""Return a Distribution for the indicated metadata path
:param path: a string or path-like object
:return: a concrete Distribution instance for the path
"""
return PathDistribution(pathlib.Path(path))
@staticmethod
def _discover_resolvers():
"""Search the meta_path for resolvers."""
@ -215,7 +233,7 @@ class Distribution:
def files(self):
"""Files in this distribution.
:return: Iterable of PackagePath for this distribution or None
:return: List of PackagePath for this distribution or None
Result is `None` if the metadata file that enumerates files
(i.e. RECORD for dist-info or SOURCES.txt for egg-info) is
@ -231,7 +249,7 @@ class Distribution:
result.dist = self
return result
return file_lines and starmap(make_file, csv.reader(file_lines))
return file_lines and list(starmap(make_file, csv.reader(file_lines)))
def _read_files_distinfo(self):
"""
@ -251,7 +269,8 @@ class Distribution:
@property
def requires(self):
"""Generated requirements specified for this Distribution"""
return self._read_dist_info_reqs() or self._read_egg_info_reqs()
reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs()
return reqs and list(reqs)
def _read_dist_info_reqs(self):
return self.metadata.get_all('Requires-Dist')
@ -312,15 +331,35 @@ class DistributionFinder(MetaPathFinder):
A MetaPathFinder capable of discovering installed distributions.
"""
class Context:
name = None
"""
Specific name for which a distribution finder should match.
"""
def __init__(self, **kwargs):
vars(self).update(kwargs)
@property
def path(self):
"""
The path that a distribution finder should search.
"""
return vars(self).get('path', sys.path)
@property
def pattern(self):
return '.*' if self.name is None else re.escape(self.name)
@abc.abstractmethod
def find_distributions(self, name=None, path=None):
def find_distributions(self, context=Context()):
"""
Find distributions.
Return an iterable of all Distribution instances capable of
loading the metadata for packages matching the ``name``
(or all names if not supplied) along the paths in the list
of directories ``path`` (defaults to sys.path).
loading the metadata for packages matching the ``context``,
a DistributionFinder.Context instance.
"""
@ -352,12 +391,12 @@ def distribution(package):
return Distribution.from_name(package)
def distributions():
def distributions(**kwargs):
"""Get all ``Distribution`` instances in the current environment.
:return: An iterable of ``Distribution`` instances.
"""
return Distribution.discover()
return Distribution.discover(**kwargs)
def metadata(package):

View File

@ -162,6 +162,10 @@ class DiscoveryTests(fixtures.EggInfoPkg,
for dist in dists
)
def test_invalid_usage(self):
with self.assertRaises(ValueError):
list(distributions(context='something', name='else'))
class DirectoryTest(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
def test_egg_info(self):

View File

@ -1,7 +1,6 @@
import re
import textwrap
import unittest
import itertools
from collections.abc import Iterator
@ -61,9 +60,7 @@ class APITests(
assert 'Topic :: Software Development :: Libraries' in classifiers
@staticmethod
def _test_files(files_iter):
assert isinstance(files_iter, Iterator), files_iter
files = list(files_iter)
def _test_files(files):
root = files[0].root
for file in files:
assert file.root == root
@ -99,16 +96,18 @@ class APITests(
requirements = requires('egginfo-file')
self.assertIsNone(requirements)
def test_requires(self):
def test_requires_egg_info(self):
deps = requires('egginfo-pkg')
assert len(deps) == 2
assert any(
dep == 'wheel >= 1.0; python_version >= "2.7"'
for dep in deps
)
def test_requires_dist_info(self):
deps = list(requires('distinfo-pkg'))
assert deps and all(deps)
deps = requires('distinfo-pkg')
assert len(deps) == 2
assert all(deps)
assert 'wheel >= 1.0' in deps
assert "pytest; extra == 'test'" in deps
@ -143,11 +142,20 @@ class APITests(
class OffSysPathTests(fixtures.DistInfoPkgOffPath, unittest.TestCase):
def test_find_distributions_specified_path(self):
dists = itertools.chain.from_iterable(
resolver(path=[str(self.site_dir)])
for resolver in Distribution._discover_resolvers()
)
dists = Distribution.discover(path=[str(self.site_dir)])
assert any(
dist.metadata['Name'] == 'distinfo-pkg'
for dist in dists
)
def test_distribution_at_pathlib(self):
"""Demonstrate how to load metadata direct from a directory.
"""
dist_info_path = self.site_dir / 'distinfo_pkg-1.0.0.dist-info'
dist = Distribution.at(dist_info_path)
assert dist.version == '1.0.0'
def test_distribution_at_str(self):
dist_info_path = self.site_dir / 'distinfo_pkg-1.0.0.dist-info'
dist = Distribution.at(str(dist_info_path))
assert dist.version == '1.0.0'

View File

@ -0,0 +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>`_.

File diff suppressed because it is too large Load Diff