From 6d7e3a8592f70c9a13dac0bdec6ad3f8da1c6c5e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 8 May 2019 10:02:31 -0400 Subject: [PATCH] Update implementation based on importlib_metadata 0.10 --- Doc/library/importlib_metadata.rst | 69 ++++++------ Lib/importlib/metadata/_hooks.py | 41 ++++--- Lib/importlib/metadata/api.py | 11 +- Lib/importlib/metadata/zipp.py | 110 ------------------- Lib/test/test_importlib/fixtures.py | 37 +++++-- Lib/test/test_importlib/test_metadata_api.py | 15 ++- 6 files changed, 113 insertions(+), 170 deletions(-) delete mode 100644 Lib/importlib/metadata/zipp.py diff --git a/Doc/library/importlib_metadata.rst b/Doc/library/importlib_metadata.rst index 93dad3df1f14..3ab66cdfb705 100644 --- a/Doc/library/importlib_metadata.rst +++ b/Doc/library/importlib_metadata.rst @@ -22,6 +22,13 @@ By default, package metadata can live on the file system or in wheels on ``sys.path``. Through an extension mechanism, the metadata can live almost anywhere. +.. note:: Although this package supports loading metadata from wheels + on ``sys.path``, that support is provisional and does not serve to + contravene the `PEP 427 directive + `_, + which states that relying on this format is discouraged, and use is + at your own risk. + Overview ======== @@ -56,40 +63,6 @@ You can also get a :ref:`distribution's version number `, list its :ref:`requirements`_. -Distributions -============= - -.. CAUTION:: The ``Distribution`` class described here may or may not end up - in the final stable public API. Consider this class `provisional - `_ until the 1.0 - release. - -While the above API is the most common and convenient usage, you can get all -of that information from the ``Distribution`` class. A ``Distribution`` is an -abstract object that represents the metadata for a Python package. You can -get the ``Distribution`` instance:: - - >>> from importlib_metadata import distribution - >>> dist = distribution('wheel') - -Thus, an alternative way to get the version number is through the -``Distribution`` instance:: - - >>> dist.version - '0.32.3' - -There are all kinds of additional metadata available on the ``Distribution`` -instance:: - - >>> d.metadata['Requires-Python'] - '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*' - >>> d.metadata['License'] - 'MIT' - -The full set of available metadata is not described here. See `PEP 566 -`_ for additional details. - - Functional API ============== @@ -198,6 +171,34 @@ function. Note that this returns an iterator:: ["pytest (>=3.0.0) ; extra == 'test'"] +Distributions +============= + +While the above API is the most common and convenient usage, you can get all +of that information from the ``Distribution`` class. A ``Distribution`` is an +abstract object that represents the metadata for a Python package. You can +get the ``Distribution`` instance:: + + >>> from importlib_metadata import distribution + >>> dist = distribution('wheel') + +Thus, an alternative way to get the version number is through the +``Distribution`` instance:: + + >>> dist.version + '0.32.3' + +There are all kinds of additional metadata available on the ``Distribution`` +instance:: + + >>> d.metadata['Requires-Python'] + '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*' + >>> d.metadata['License'] + 'MIT' + +The full set of available metadata is not described here. See `PEP 566 +`_ for additional details. + Extending the search algorithm ============================== diff --git a/Lib/importlib/metadata/_hooks.py b/Lib/importlib/metadata/_hooks.py index 5f9bdb54a2e1..e624844217df 100644 --- a/Lib/importlib/metadata/_hooks.py +++ b/Lib/importlib/metadata/_hooks.py @@ -1,8 +1,8 @@ import re import sys +import zipfile import itertools -from . import zipp from .api import Distribution from .abc import DistributionFinder from contextlib import suppress @@ -25,14 +25,12 @@ class NullFinder(DistributionFinder): return None -@install -class MetadataPathFinder(NullFinder): +class MetadataPathBaseFinder(NullFinder): """A degenerate finder for distribution packages on the file system. This finder supplies only a find_distributions() method for versions of Python that do not have a PathFinder find_distributions(). """ - search_template = r'{pattern}(-.*)?\.(dist|egg)-info' def find_distributions(self, name=None, path=None): """Return an iterable of all Distribution instances capable of @@ -56,21 +54,34 @@ class MetadataPathFinder(NullFinder): for path in map(Path, paths) ) + @classmethod + def _predicate(cls, pattern, root, item): + return re.match(pattern, str(item.name), flags=re.IGNORECASE) + @classmethod def _search_path(cls, root, pattern): if not root.is_dir(): return () normalized = pattern.replace('-', '_') + matcher = cls.search_template.format(pattern=normalized) + return (item for item in root.iterdir() + if cls._predicate(matcher, root, item)) + + +@install +class MetadataPathFinder(MetadataPathBaseFinder): + search_template = r'{pattern}(-.*)?\.(dist|egg)-info' + + +@install +class MetadataPathEggInfoFileFinder(MetadataPathBaseFinder): + search_template = r'{pattern}(-.*)?\.egg-info' + + @classmethod + def _predicate(cls, pattern, root, item): return ( - item - for item in root.iterdir() - if item.is_dir() - and re.match( - cls.search_template.format(pattern=normalized), - str(item.name), - flags=re.IGNORECASE, - ) - ) + (root / item).is_file() and + re.match(pattern, str(item.name), flags=re.IGNORECASE)) class PathDistribution(Distribution): @@ -79,7 +90,7 @@ class PathDistribution(Distribution): self._path = path def read_text(self, filename): - with suppress(FileNotFoundError): + with suppress(FileNotFoundError, NotADirectoryError): with self._path.joinpath(filename).open(encoding='utf-8') as fp: return fp.read() return None @@ -125,7 +136,7 @@ class WheelMetadataFinder(NullFinder): class WheelDistribution(Distribution): def __init__(self, archive): - self._archive = zipp.Path(archive) + self._archive = zipfile.Path(archive) name, version = archive.name.split('-')[0:2] self._dist_info = '{}-{}.dist-info'.format(name, version) diff --git a/Lib/importlib/metadata/api.py b/Lib/importlib/metadata/api.py index df19aff438bb..b95bc454cc6f 100644 --- a/Lib/importlib/metadata/api.py +++ b/Lib/importlib/metadata/api.py @@ -179,7 +179,14 @@ class Distribution: The returned object will have keys that name the various bits of metadata. See PEP 566 for details. """ - text = self.read_text('METADATA') or self.read_text('PKG-INFO') + text = ( + self.read_text('METADATA') + or self.read_text('PKG-INFO') + # This last clause is here to support old egg-info files. Its + # effect is to just end up using the PathDistribution's self._path + # (which points to the egg-info file) attribute unchanged. + or self.read_text('') + ) return email.message_from_string(text) @property @@ -230,7 +237,7 @@ class Distribution: def _read_egg_info_reqs(self): source = self.read_text('requires.txt') - return self._deps_from_requires_text(source) + return source and self._deps_from_requires_text(source) @classmethod def _deps_from_requires_text(cls, source): diff --git a/Lib/importlib/metadata/zipp.py b/Lib/importlib/metadata/zipp.py deleted file mode 100644 index ffd129f63b05..000000000000 --- a/Lib/importlib/metadata/zipp.py +++ /dev/null @@ -1,110 +0,0 @@ -""" ->>> root = Path(getfixture('zipfile_abcde')) ->>> a, b = root.iterdir() ->>> a -Path('abcde.zip', 'a.txt') ->>> b -Path('abcde.zip', 'b/') ->>> b.name -'b' ->>> c = b / 'c.txt' ->>> c -Path('abcde.zip', 'b/c.txt') ->>> c.name -'c.txt' ->>> c.read_text() -'content of c' ->>> c.exists() -True ->>> (b / 'missing.txt').exists() -False ->>> str(c) -'abcde.zip/b/c.txt' -""" - -from __future__ import division - -import io -import sys -import posixpath -import zipfile -import operator -import functools - -__metaclass__ = type - - -class Path: - __repr = '{self.__class__.__name__}({self.root.filename!r}, {self.at!r})' - - def __init__(self, root, at=''): - self.root = root if isinstance(root, zipfile.ZipFile) \ - else zipfile.ZipFile(self._pathlib_compat(root)) - self.at = at - - @staticmethod - def _pathlib_compat(path): - """ - For path-like objects, convert to a filename for compatibility - on Python 3.6.1 and earlier. - """ - try: - return path.__fspath__() - except AttributeError: - return str(path) - - @property - def open(self): - return functools.partial(self.root.open, self.at) - - @property - def name(self): - return posixpath.basename(self.at.rstrip('/')) - - def read_text(self, *args, **kwargs): - with self.open() as strm: - return io.TextIOWrapper(strm, *args, **kwargs).read() - - def read_bytes(self): - with self.open() as strm: - return strm.read() - - def _is_child(self, path): - return posixpath.dirname(path.at.rstrip('/')) == self.at.rstrip('/') - - def _next(self, at): - return Path(self.root, at) - - def is_dir(self): - return not self.at or self.at.endswith('/') - - def is_file(self): - return not self.is_dir() - - def exists(self): - return self.at in self.root.namelist() - - def iterdir(self): - if not self.is_dir(): - raise ValueError("Can't listdir a file") - names = map(operator.attrgetter('filename'), self.root.infolist()) - subs = map(self._next, names) - return filter(self._is_child, subs) - - def __str__(self): - return posixpath.join(self.root.filename, self.at) - - def __repr__(self): - return self.__repr.format(self=self) - - def __truediv__(self, add): - add = self._pathlib_compat(add) - next = posixpath.join(self.at, add) - next_dir = posixpath.join(self.at, add, '') - names = self.root.namelist() - return self._next( - next_dir if next not in names and next_dir in names else next - ) - - if sys.version_info < (3,): - __div__ = __truediv__ \ No newline at end of file diff --git a/Lib/test/test_importlib/fixtures.py b/Lib/test/test_importlib/fixtures.py index 8a8b4add31ca..737ea4be7aa3 100644 --- a/Lib/test/test_importlib/fixtures.py +++ b/Lib/test/test_importlib/fixtures.py @@ -127,6 +127,27 @@ class EggInfoPkg(SiteDir): build_files(EggInfoPkg.files, prefix=self.site_dir) +class EggInfoFile(SiteDir): + files = { + "egginfo_file.egg-info": """ + Metadata-Version: 1.0 + Name: egginfo_file + Version: 0.1 + Summary: An example package + Home-page: www.example.com + Author: Eric Haffa-Vee + Author-email: eric@example.coms + License: UNKNOWN + Description: UNKNOWN + Platform: UNKNOWN + """, + } + + def setUp(self): + super(EggInfoFile, self).setUp() + build_files(EggInfoFile.files, prefix=self.site_dir) + + class LocalPackage: def setUp(self): self.fixtures = ExitStack() @@ -136,15 +157,15 @@ class LocalPackage: def build_files(file_defs, prefix=pathlib.Path()): - """ - Build a set of files/directories, as described by the - file_defs dictionary. - Each key/value pair in the dictionary is interpreted as - a filename/contents - pair. If the contents value is a dictionary, a directory - is created, and the - dictionary interpreted as the files within it, recursively. + """Build a set of files/directories, as described by the + + file_defs dictionary. Each key/value pair in the dictionary is + interpreted as a filename/contents pair. If the contents value is a + dictionary, a directory is created, and the dictionary interpreted + as the files within it, recursively. + For example: + {"README.txt": "A README file", "foo": { "__init__.py": "", diff --git a/Lib/test/test_importlib/test_metadata_api.py b/Lib/test/test_importlib/test_metadata_api.py index a6aa395831d9..4aba1af7392b 100644 --- a/Lib/test/test_importlib/test_metadata_api.py +++ b/Lib/test/test_importlib/test_metadata_api.py @@ -8,7 +8,12 @@ from collections.abc import Iterator from . import fixtures -class APITests(fixtures.EggInfoPkg, fixtures.DistInfoPkg, unittest.TestCase): +class APITests( + fixtures.EggInfoPkg, + fixtures.DistInfoPkg, + fixtures.EggInfoFile, + unittest.TestCase): + version_pattern = r'\d+\.\d+(\.\d)?' def test_retrieves_version_of_self(self): @@ -85,6 +90,14 @@ class APITests(fixtures.EggInfoPkg, fixtures.DistInfoPkg, unittest.TestCase): def test_files_egg_info(self): self._test_files(importlib.metadata.files('egginfo-pkg')) + def test_version_egg_info_file(self): + version = importlib_metadata.version('egginfo-file') + self.assertEqual(version, '0.1') + + def test_requires_egg_info_file(self): + requirements = importlib_metadata.requires('egginfo-file') + self.assertIsNone(requirements) + def test_requires(self): deps = importlib.metadata.requires('egginfo-pkg') assert any( -- 2.47.3