diff --git a/Doc/library/importlib.metadata.rst b/Doc/library/importlib.metadata.rst index ad17d939d71..8a5b66d04e1 100644 --- a/Doc/library/importlib.metadata.rst +++ b/Doc/library/importlib.metadata.rst @@ -18,17 +18,10 @@ Python's ``site-packages`` directory via tools such as `pip `_. Specifically, it means a package with either a discoverable ``dist-info`` or ``egg-info`` directory, and metadata defined by `PEP 566`_ or its older specifications. -By default, package metadata can live on the file system or in wheels on +By default, package metadata can live on the file system or in zip archives 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 ======== diff --git a/Lib/importlib/metadata/__init__.py b/Lib/importlib/metadata/__init__.py index 314ece65afe..8d4b0a344cd 100644 --- a/Lib/importlib/metadata/__init__.py +++ b/Lib/importlib/metadata/__init__.py @@ -1,17 +1,19 @@ -from .api import distribution, Distribution, PackageNotFoundError # noqa: F401 from .api import ( - metadata, entry_points, version, files, requires, distributions, - ) + Distribution, PackageNotFoundError, distribution, distributions, + entry_points, files, metadata, requires, version) # Import for installation side-effects. from . import _hooks # noqa: F401 __all__ = [ + 'Distribution', + 'PackageNotFoundError', + 'distribution', + 'distributions', 'entry_points', 'files', 'metadata', 'requires', 'version', - 'distributions', ] diff --git a/Lib/importlib/metadata/_hooks.py b/Lib/importlib/metadata/_hooks.py index e624844217d..f6bed1a67fd 100644 --- a/Lib/importlib/metadata/_hooks.py +++ b/Lib/importlib/metadata/_hooks.py @@ -25,12 +25,14 @@ class NullFinder(DistributionFinder): return None -class MetadataPathBaseFinder(NullFinder): +@install +class MetadataPathFinder(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 @@ -51,9 +53,15 @@ class MetadataPathBaseFinder(NullFinder): """ return itertools.chain.from_iterable( cls._search_path(path, pattern) - for path in map(Path, paths) + for path in map(cls._switch_path, paths) ) + @staticmethod + def _switch_path(path): + with suppress(Exception): + return zipfile.Path(path) + return Path(path) + @classmethod def _predicate(cls, pattern, root, item): return re.match(pattern, str(item.name), flags=re.IGNORECASE) @@ -68,82 +76,15 @@ class MetadataPathBaseFinder(NullFinder): 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 ( - (root / item).is_file() and - re.match(pattern, str(item.name), flags=re.IGNORECASE)) - - class PathDistribution(Distribution): def __init__(self, path): """Construct a distribution from a path to the metadata directory.""" self._path = path def read_text(self, filename): - with suppress(FileNotFoundError, NotADirectoryError): - with self._path.joinpath(filename).open(encoding='utf-8') as fp: - return fp.read() - return None + with suppress(FileNotFoundError, NotADirectoryError, KeyError): + return self._path.joinpath(filename).read_text(encoding='utf-8') read_text.__doc__ = Distribution.read_text.__doc__ def locate_file(self, path): return self._path.parent / path - - -@install -class WheelMetadataFinder(NullFinder): - """A degenerate finder for distribution packages in wheels. - - This finder supplies only a find_distributions() method for versions - of Python that do not have a PathFinder find_distributions(). - """ - search_template = r'{pattern}(-.*)?\.whl' - - def find_distributions(self, name=None, path=None): - """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). - """ - if path is None: - path = sys.path - pattern = '.*' if name is None else re.escape(name) - found = self._search_paths(pattern, path) - return map(WheelDistribution, found) - - @classmethod - def _search_paths(cls, pattern, paths): - return ( - path - for path in map(Path, paths) - if re.match( - cls.search_template.format(pattern=pattern), - str(path.name), - flags=re.IGNORECASE, - ) - ) - - -class WheelDistribution(Distribution): - def __init__(self, archive): - self._archive = zipfile.Path(archive) - name, version = archive.name.split('-')[0:2] - self._dist_info = '{}-{}.dist-info'.format(name, version) - - def read_text(self, filename): - target = self._archive / self._dist_info / filename - return target.read_text() if target.exists() else None - read_text.__doc__ = Distribution.read_text.__doc__ - - def locate_file(self, path): - return self._archive / path diff --git a/Lib/importlib/metadata/abc.py b/Lib/importlib/metadata/abc.py index 1785cf3c1c2..845e41afff7 100644 --- a/Lib/importlib/metadata/abc.py +++ b/Lib/importlib/metadata/abc.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import - - import abc from importlib.abc import MetaPathFinder diff --git a/Lib/importlib/metadata/api.py b/Lib/importlib/metadata/api.py index b95bc454cc6..ba5c17120b9 100644 --- a/Lib/importlib/metadata/api.py +++ b/Lib/importlib/metadata/api.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import re import abc import csv diff --git a/Lib/test/test_importlib/test_main.py b/Lib/test/test_importlib/test_main.py index 64270850a9f..926064ed486 100644 --- a/Lib/test/test_importlib/test_main.py +++ b/Lib/test/test_importlib/test_main.py @@ -1,33 +1,31 @@ # coding: utf-8 -from __future__ import unicode_literals import re import textwrap import unittest import importlib -import importlib.metadata from . import fixtures -from importlib.metadata import _hooks +from importlib.metadata import ( + Distribution, PackageNotFoundError, _hooks, api, distributions, + entry_points, metadata, version) class BasicTests(fixtures.DistInfoPkg, unittest.TestCase): version_pattern = r'\d+\.\d+(\.\d)?' def test_retrieves_version_of_self(self): - dist = importlib.metadata.Distribution.from_name('distinfo-pkg') + dist = Distribution.from_name('distinfo-pkg') assert isinstance(dist.version, str) assert re.match(self.version_pattern, dist.version) def test_for_name_does_not_exist(self): - with self.assertRaises(importlib.metadata.PackageNotFoundError): - importlib.metadata.Distribution.from_name('does-not-exist') + with self.assertRaises(PackageNotFoundError): + Distribution.from_name('does-not-exist') def test_new_style_classes(self): - self.assertIsInstance(importlib.metadata.Distribution, type) + self.assertIsInstance(Distribution, type) self.assertIsInstance(_hooks.MetadataPathFinder, type) - self.assertIsInstance(_hooks.WheelMetadataFinder, type) - self.assertIsInstance(_hooks.WheelDistribution, type) class ImportTests(fixtures.DistInfoPkg, unittest.TestCase): @@ -38,17 +36,17 @@ class ImportTests(fixtures.DistInfoPkg, unittest.TestCase): importlib.import_module('does_not_exist') def test_resolve(self): - entries = dict(importlib.metadata.entry_points()['entries']) + entries = dict(entry_points()['entries']) ep = entries['main'] self.assertEqual(ep.load().__name__, "main") def test_resolve_without_attr(self): - ep = importlib.metadata.api.EntryPoint( + ep = api.EntryPoint( name='ep', value='importlib.metadata.api', group='grp', ) - assert ep.load() is importlib.metadata.api + assert ep.load() is api class NameNormalizationTests(fixtures.SiteDir, unittest.TestCase): @@ -71,7 +69,7 @@ class NameNormalizationTests(fixtures.SiteDir, unittest.TestCase): uses underscores in the name. Ensure the metadata loads. """ pkg_name = self.pkg_with_dashes(self.site_dir) - assert importlib.metadata.version(pkg_name) == '1.0' + assert version(pkg_name) == '1.0' @staticmethod def pkg_with_mixed_case(site_dir): @@ -91,9 +89,9 @@ class NameNormalizationTests(fixtures.SiteDir, unittest.TestCase): Ensure the metadata loads when queried with any case. """ pkg_name = self.pkg_with_mixed_case(self.site_dir) - assert importlib.metadata.version(pkg_name) == '1.0' - assert importlib.metadata.version(pkg_name.lower()) == '1.0' - assert importlib.metadata.version(pkg_name.upper()) == '1.0' + assert version(pkg_name) == '1.0' + assert version(pkg_name.lower()) == '1.0' + assert version(pkg_name.upper()) == '1.0' class NonASCIITests(fixtures.SiteDir, unittest.TestCase): @@ -129,22 +127,23 @@ class NonASCIITests(fixtures.SiteDir, unittest.TestCase): def test_metadata_loads(self): pkg_name = self.pkg_with_non_ascii_description(self.site_dir) - meta = importlib.metadata.metadata(pkg_name) + meta = metadata(pkg_name) assert meta['Description'] == 'pôrˈtend' def test_metadata_loads_egg_info(self): pkg_name = self.pkg_with_non_ascii_description_egg_info(self.site_dir) - meta = importlib.metadata.metadata(pkg_name) + meta = metadata(pkg_name) assert meta.get_payload() == 'pôrˈtend\n' -class DiscoveryTests(fixtures.EggInfoPkg, fixtures.DistInfoPkg, +class DiscoveryTests(fixtures.EggInfoPkg, + fixtures.DistInfoPkg, unittest.TestCase): def test_package_discovery(self): - dists = list(importlib.metadata.api.distributions()) + dists = list(distributions()) assert all( - isinstance(dist, importlib.metadata.Distribution) + isinstance(dist, Distribution) for dist in dists ) assert any( diff --git a/Lib/test/test_importlib/test_metadata_api.py b/Lib/test/test_importlib/test_metadata_api.py index 8e4c95cc672..f837a6343eb 100644 --- a/Lib/test/test_importlib/test_metadata_api.py +++ b/Lib/test/test_importlib/test_metadata_api.py @@ -1,11 +1,15 @@ import re import textwrap import unittest -import importlib.metadata from collections.abc import Iterator from . import fixtures +from importlib.metadata import ( + Distribution, PackageNotFoundError, distribution, + entry_points, files, metadata, requires, version, + ) +from importlib.metadata.api import local_distribution class APITests( @@ -17,41 +21,39 @@ class APITests( version_pattern = r'\d+\.\d+(\.\d)?' def test_retrieves_version_of_self(self): - version = importlib.metadata.version('egginfo-pkg') - assert isinstance(version, str) - assert re.match(self.version_pattern, version) + pkg_version = version('egginfo-pkg') + assert isinstance(pkg_version, str) + assert re.match(self.version_pattern, pkg_version) - def test_retrieves_version_of_pip(self): - version = importlib.metadata.version('distinfo-pkg') - assert isinstance(version, str) - assert re.match(self.version_pattern, version) + def test_retrieves_version_of_distinfo_pkg(self): + pkg_version = version('distinfo-pkg') + assert isinstance(pkg_version, str) + assert re.match(self.version_pattern, pkg_version) def test_for_name_does_not_exist(self): - with self.assertRaises(importlib.metadata.PackageNotFoundError): - importlib.metadata.distribution('does-not-exist') + with self.assertRaises(PackageNotFoundError): + distribution('does-not-exist') def test_for_top_level(self): - distribution = importlib.metadata.distribution('egginfo-pkg') self.assertEqual( - distribution.read_text('top_level.txt').strip(), + distribution('egginfo-pkg').read_text('top_level.txt').strip(), 'mod') def test_read_text(self): top_level = [ - path for path in importlib.metadata.files('egginfo-pkg') + path for path in files('egginfo-pkg') if path.name == 'top_level.txt' ][0] self.assertEqual(top_level.read_text(), 'mod\n') def test_entry_points(self): - entires = importlib.metadata.entry_points()['entries'] - entries = dict(entires) + entries = dict(entry_points()['entries']) ep = entries['main'] self.assertEqual(ep.value, 'mod:main') self.assertEqual(ep.extras, []) def test_metadata_for_this_package(self): - md = importlib.metadata.metadata('egginfo-pkg') + md = metadata('egginfo-pkg') assert md['author'] == 'Steven Ma' assert md['LICENSE'] == 'Unknown' assert md['Name'] == 'egginfo-pkg' @@ -77,7 +79,7 @@ class APITests( assertRegex = self.assertRegex util = [ - p for p in importlib.metadata.files('distinfo-pkg') + p for p in files('distinfo-pkg') if p.name == 'mod.py' ][0] assertRegex( @@ -85,28 +87,27 @@ class APITests( '') def test_files_dist_info(self): - self._test_files(importlib.metadata.files('distinfo-pkg')) + self._test_files(files('distinfo-pkg')) def test_files_egg_info(self): - self._test_files(importlib.metadata.files('egginfo-pkg')) + self._test_files(files('egginfo-pkg')) def test_version_egg_info_file(self): - version = importlib.metadata.version('egginfo-file') - self.assertEqual(version, '0.1') + self.assertEqual(version('egginfo-file'), '0.1') def test_requires_egg_info_file(self): - requirements = importlib.metadata.requires('egginfo-file') + requirements = requires('egginfo-file') self.assertIsNone(requirements) def test_requires(self): - deps = importlib.metadata.requires('egginfo-pkg') + deps = requires('egginfo-pkg') assert any( dep == 'wheel >= 1.0; python_version >= "2.7"' for dep in deps ) def test_requires_dist_info(self): - deps = list(importlib.metadata.requires('distinfo-pkg')) + deps = list(requires('distinfo-pkg')) assert deps and all(deps) def test_more_complex_deps_requires_text(self): @@ -123,10 +124,7 @@ class APITests( [extra2:python_version < "3"] dep5 """) - deps = sorted( - importlib.metadata.api.Distribution._deps_from_requires_text( - requires) - ) + deps = sorted(Distribution._deps_from_requires_text(requires)) expected = [ 'dep1', 'dep2', @@ -143,5 +141,5 @@ class APITests( class LocalProjectTests(fixtures.LocalPackage, unittest.TestCase): def test_find_local(self): - dist = importlib.metadata.api.local_distribution() + dist = local_distribution() assert dist.metadata['Name'] == 'egginfo-pkg' diff --git a/Lib/test/test_importlib/test_zip.py b/Lib/test/test_importlib/test_zip.py index 4af027a12be..c7c8c0b1843 100644 --- a/Lib/test/test_importlib/test_zip.py +++ b/Lib/test/test_importlib/test_zip.py @@ -1,14 +1,10 @@ import sys import unittest -import importlib.metadata +from contextlib import ExitStack +from importlib.metadata import distribution, entry_points, files, version from importlib.resources import path -try: - from contextlib import ExitStack -except ImportError: - from contextlib2 import ExitStack - class BespokeLoader: archive = 'bespoke' @@ -27,22 +23,20 @@ class TestZip(unittest.TestCase): self.resources.callback(sys.path.pop, 0) def test_zip_version(self): - self.assertEqual(importlib.metadata.version('example'), '21.12') + self.assertEqual(version('example'), '21.12') def test_zip_entry_points(self): - scripts = dict(importlib.metadata.entry_points()['console_scripts']) + scripts = dict(entry_points()['console_scripts']) entry_point = scripts['example'] self.assertEqual(entry_point.value, 'example:main') def test_missing_metadata(self): - distribution = importlib.metadata.distribution('example') - self.assertIsNone(distribution.read_text('does not exist')) + self.assertIsNone(distribution('example').read_text('does not exist')) def test_case_insensitive(self): - self.assertEqual(importlib.metadata.version('Example'), '21.12') + self.assertEqual(version('Example'), '21.12') def test_files(self): - files = importlib.metadata.files('example') - for file in files: + for file in files('example'): path = str(file.dist.locate_file(file)) assert '.whl/' in path, path