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()`` 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 >>> requires('wheel') # doctest: +SKIP
["pytest (>=3.0.0) ; extra == 'test'"] ["pytest (>=3.0.0) ; extra == 'test'", "pytest-cov ; extra == 'test'"]
Distributions Distributions
@ -224,23 +224,25 @@ The abstract class :py:class:`importlib.abc.MetaPathFinder` defines the
interface expected of finders by Python's import system. interface expected of finders by Python's import system.
``importlib.metadata`` extends this protocol by looking for an optional ``importlib.metadata`` extends this protocol by looking for an optional
``find_distributions`` callable on the finders from ``find_distributions`` callable on the finders from
``sys.meta_path``. If the finder has this method, it must return ``sys.meta_path`` and presents this extended interface as the
an iterator over instances of the ``Distribution`` abstract class. This ``DistributionFinder`` abstract base class, which defines this abstract
method must have the signature:: 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 """Return an iterable of all Distribution instances capable of
loading the metadata for packages matching the name loading the metadata for packages for the indicated ``context``.
(or all names if not supplied) along the paths in the list
of directories ``path`` (defaults to sys.path).
""" """
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 What this means in practice is that to support finding distribution package
metadata in locations other than the file system, you should derive from metadata in locations other than the file system, you should derive from
``Distribution`` and implement the ``load_metadata()`` method. This takes a ``Distribution`` and implement the ``load_metadata()`` method. Then from
single argument which is the name of the package whose metadata is being your finder, return instances of this derived ``Distribution`` in the
found. This instance of the ``Distribution`` base abstract class is what your ``find_distributions()`` method.
finder's ``find_distributions()`` method should return.
.. _`entry point API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points .. _`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::BytesWarning
whatsnew/changelog,,::,default::DeprecationWarning whatsnew/changelog,,::,default::DeprecationWarning
library/importlib.metadata,,:main,"EntryPoint(name='wheel', value='wheel.cli:main', group='console_scripts')" 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 return spec.loader
@classmethod @classmethod
def find_distributions(cls, name=None, path=None): def find_distributions(self, context=None):
""" """
Find distributions. Find distributions.
Return an iterable of all Distribution instances capable of Return an iterable of all Distribution instances capable of
loading the metadata for packages matching the ``name`` loading the metadata for packages matching ``context.name``
(or all names if not supplied) along the paths in the list (or all names if ``None`` indicated) along the paths in the list
of directories ``path`` (defaults to sys.path). of directories ``context.path``.
""" """
import re from importlib.metadata import PathDistribution, DistributionFinder
from importlib.metadata import PathDistribution if context is None:
if path is None: context = DistributionFinder.Context()
path = sys.path found = self._search_paths(context.pattern, context.path)
pattern = '.*' if name is None else re.escape(name)
found = cls._search_paths(pattern, path)
return map(PathDistribution, found) return map(PathDistribution, found)
@classmethod @classmethod

View File

@ -19,6 +19,7 @@ from itertools import starmap
__all__ = [ __all__ = [
'Distribution', 'Distribution',
'DistributionFinder',
'PackageNotFoundError', 'PackageNotFoundError',
'distribution', 'distribution',
'distributions', 'distributions',
@ -158,7 +159,7 @@ class Distribution:
metadata cannot be found. metadata cannot be found.
""" """
for resolver in cls._discover_resolvers(): for resolver in cls._discover_resolvers():
dists = resolver(name) dists = resolver(DistributionFinder.Context(name=name))
dist = next(dists, None) dist = next(dists, None)
if dist is not None: if dist is not None:
return dist return dist
@ -166,16 +167,33 @@ class Distribution:
raise PackageNotFoundError(name) raise PackageNotFoundError(name)
@classmethod @classmethod
def discover(cls): def discover(cls, **kwargs):
"""Return an iterable of Distribution objects for all packages. """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. :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( return itertools.chain.from_iterable(
resolver() resolver(context)
for resolver in cls._discover_resolvers() 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 @staticmethod
def _discover_resolvers(): def _discover_resolvers():
"""Search the meta_path for resolvers.""" """Search the meta_path for resolvers."""
@ -215,7 +233,7 @@ class Distribution:
def files(self): def files(self):
"""Files in this distribution. """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 Result is `None` if the metadata file that enumerates files
(i.e. RECORD for dist-info or SOURCES.txt for egg-info) is (i.e. RECORD for dist-info or SOURCES.txt for egg-info) is
@ -231,7 +249,7 @@ class Distribution:
result.dist = self result.dist = self
return result 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): def _read_files_distinfo(self):
""" """
@ -251,7 +269,8 @@ class Distribution:
@property @property
def requires(self): def requires(self):
"""Generated requirements specified for this Distribution""" """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): def _read_dist_info_reqs(self):
return self.metadata.get_all('Requires-Dist') return self.metadata.get_all('Requires-Dist')
@ -312,15 +331,35 @@ class DistributionFinder(MetaPathFinder):
A MetaPathFinder capable of discovering installed distributions. 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 @abc.abstractmethod
def find_distributions(self, name=None, path=None): def find_distributions(self, context=Context()):
""" """
Find distributions. Find distributions.
Return an iterable of all Distribution instances capable of Return an iterable of all Distribution instances capable of
loading the metadata for packages matching the ``name`` loading the metadata for packages matching the ``context``,
(or all names if not supplied) along the paths in the list a DistributionFinder.Context instance.
of directories ``path`` (defaults to sys.path).
""" """
@ -352,12 +391,12 @@ def distribution(package):
return Distribution.from_name(package) return Distribution.from_name(package)
def distributions(): def distributions(**kwargs):
"""Get all ``Distribution`` instances in the current environment. """Get all ``Distribution`` instances in the current environment.
:return: An iterable of ``Distribution`` instances. :return: An iterable of ``Distribution`` instances.
""" """
return Distribution.discover() return Distribution.discover(**kwargs)
def metadata(package): def metadata(package):

View File

@ -162,6 +162,10 @@ class DiscoveryTests(fixtures.EggInfoPkg,
for dist in dists 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): class DirectoryTest(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
def test_egg_info(self): def test_egg_info(self):

View File

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