Skip to content

Commit

Permalink
bpo-38086: Sync importlib.metadata with importlib_metadata 0.21. (GH-…
Browse files Browse the repository at this point in the history
  • Loading branch information
jaraco authored and brettcannon committed Sep 10, 2019
1 parent 97d7906 commit 17499d8
Show file tree
Hide file tree
Showing 8 changed files with 712 additions and 661 deletions.
30 changes: 16 additions & 14 deletions Doc/library/importlib.metadata.rst
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Doc/tools/susp-ignored.csv
Expand Up @@ -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``.
18 changes: 8 additions & 10 deletions Lib/importlib/_bootstrap_external.py
Expand Up @@ -1370,21 +1370,19 @@ def find_module(cls, fullname, path=None):
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
Expand Down
63 changes: 51 additions & 12 deletions Lib/importlib/metadata.py
Expand Up @@ -19,6 +19,7 @@

__all__ = [
'Distribution',
'DistributionFinder',
'PackageNotFoundError',
'distribution',
'distributions',
Expand Down Expand Up @@ -158,24 +159,41 @@ def from_name(cls, name):
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
else:
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."""
Expand Down Expand Up @@ -215,7 +233,7 @@ def entry_points(self):
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
Expand All @@ -231,7 +249,7 @@ def make_file(name, hash=None, size_str=None):
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):
"""
Expand All @@ -251,7 +269,8 @@ def _read_files_egginfo(self):
@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')
Expand Down Expand Up @@ -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.
"""


Expand Down Expand Up @@ -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):
Expand Down
4 changes: 4 additions & 0 deletions Lib/test/test_importlib/test_main.py
Expand Up @@ -162,6 +162,10 @@ def test_package_discovery(self):
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):
Expand Down
30 changes: 19 additions & 11 deletions Lib/test/test_importlib/test_metadata_api.py
@@ -1,7 +1,6 @@
import re
import textwrap
import unittest
import itertools

from collections.abc import Iterator

Expand Down Expand Up @@ -61,9 +60,7 @@ def test_metadata_for_this_package(self):
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
Expand Down Expand Up @@ -99,16 +96,18 @@ def test_requires_egg_info_file(self):
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

Expand Down Expand Up @@ -143,11 +142,20 @@ def test_more_complex_deps_requires_text(self):

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'
@@ -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>`_.

0 comments on commit 17499d8

Please sign in to comment.