From 8ad88984200b2ccddc0a08229dd2f4c14d1a71fc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 20 Mar 2024 17:11:00 -0400 Subject: gh-117089: Move importlib.metadata tests to their own package (#117092) * Ensure importlib.metadata tests do not leak references in sys.modules. * Move importlib.metadata tests to their own package for easier syncing with importlib_metadata. * Update owners and makefile for new directories. * Add blurb --- .github/CODEOWNERS | 2 +- Lib/test/test_importlib/_context.py | 13 - Lib/test/test_importlib/_path.py | 120 ------ Lib/test/test_importlib/data/__init__.py | 0 .../data/example-21.12-py3-none-any.whl | Bin 1455 -> 0 bytes .../test_importlib/data/example-21.12-py3.6.egg | Bin 1497 -> 0 bytes .../data/example2-1.0.0-py3-none-any.whl | Bin 1167 -> 0 bytes Lib/test/test_importlib/fixtures.py | 378 ----------------- Lib/test/test_importlib/metadata/__init__.py | 0 Lib/test/test_importlib/metadata/_context.py | 13 + Lib/test/test_importlib/metadata/_path.py | 120 ++++++ Lib/test/test_importlib/metadata/data/__init__.py | 0 .../metadata/data/example-21.12-py3-none-any.whl | Bin 0 -> 1455 bytes .../metadata/data/example-21.12-py3.6.egg | Bin 0 -> 1497 bytes .../metadata/data/example2-1.0.0-py3-none-any.whl | Bin 0 -> 1167 bytes Lib/test/test_importlib/metadata/fixtures.py | 380 +++++++++++++++++ Lib/test/test_importlib/metadata/stubs.py | 10 + Lib/test/test_importlib/metadata/test_api.py | 322 ++++++++++++++ Lib/test/test_importlib/metadata/test_main.py | 471 +++++++++++++++++++++ Lib/test/test_importlib/metadata/test_zip.py | 62 +++ Lib/test/test_importlib/stubs.py | 10 - Lib/test/test_importlib/test_main.py | 471 --------------------- Lib/test/test_importlib/test_metadata_api.py | 322 -------------- Lib/test/test_importlib/test_zip.py | 62 --- Makefile.pre.in | 3 +- .../2024-03-20-14-19-32.gh-issue-117089.WwR1Z1.rst | 1 + 26 files changed, 1382 insertions(+), 1378 deletions(-) delete mode 100644 Lib/test/test_importlib/_context.py delete mode 100644 Lib/test/test_importlib/_path.py delete mode 100644 Lib/test/test_importlib/data/__init__.py delete mode 100644 Lib/test/test_importlib/data/example-21.12-py3-none-any.whl delete mode 100644 Lib/test/test_importlib/data/example-21.12-py3.6.egg delete mode 100644 Lib/test/test_importlib/data/example2-1.0.0-py3-none-any.whl delete mode 100644 Lib/test/test_importlib/fixtures.py create mode 100644 Lib/test/test_importlib/metadata/__init__.py create mode 100644 Lib/test/test_importlib/metadata/_context.py create mode 100644 Lib/test/test_importlib/metadata/_path.py create mode 100644 Lib/test/test_importlib/metadata/data/__init__.py create mode 100644 Lib/test/test_importlib/metadata/data/example-21.12-py3-none-any.whl create mode 100644 Lib/test/test_importlib/metadata/data/example-21.12-py3.6.egg create mode 100644 Lib/test/test_importlib/metadata/data/example2-1.0.0-py3-none-any.whl create mode 100644 Lib/test/test_importlib/metadata/fixtures.py create mode 100644 Lib/test/test_importlib/metadata/stubs.py create mode 100644 Lib/test/test_importlib/metadata/test_api.py create mode 100644 Lib/test/test_importlib/metadata/test_main.py create mode 100644 Lib/test/test_importlib/metadata/test_zip.py delete mode 100644 Lib/test/test_importlib/stubs.py delete mode 100644 Lib/test/test_importlib/test_main.py delete mode 100644 Lib/test/test_importlib/test_metadata_api.py delete mode 100644 Lib/test/test_importlib/test_zip.py create mode 100644 Misc/NEWS.d/next/Tests/2024-03-20-14-19-32.gh-issue-117089.WwR1Z1.rst diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e8eed40..235bc78 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -119,7 +119,7 @@ Python/dynload_*.c @ericsnowcurrently Lib/test/test_module/ @ericsnowcurrently Doc/c-api/module.rst @ericsnowcurrently **/*importlib/resources/* @jaraco @warsaw @FFY00 -**/importlib/metadata/* @jaraco @warsaw +**/*importlib/metadata/* @jaraco @warsaw # Dates and times **/*datetime* @pganssle @abalkin diff --git a/Lib/test/test_importlib/_context.py b/Lib/test/test_importlib/_context.py deleted file mode 100644 index 8a53eb5..0000000 --- a/Lib/test/test_importlib/_context.py +++ /dev/null @@ -1,13 +0,0 @@ -import contextlib - - -# from jaraco.context 4.3 -class suppress(contextlib.suppress, contextlib.ContextDecorator): - """ - A version of contextlib.suppress with decorator support. - - >>> @suppress(KeyError) - ... def key_error(): - ... {}[''] - >>> key_error() - """ diff --git a/Lib/test/test_importlib/_path.py b/Lib/test/test_importlib/_path.py deleted file mode 100644 index 25c799f..0000000 --- a/Lib/test/test_importlib/_path.py +++ /dev/null @@ -1,120 +0,0 @@ -# from jaraco.path 3.7 - -import functools -import pathlib -from typing import Dict, Protocol, Union -from typing import runtime_checkable - - -class Symlink(str): - """ - A string indicating the target of a symlink. - """ - - -FilesSpec = Dict[str, Union[str, bytes, Symlink, 'FilesSpec']] # type: ignore - - -@runtime_checkable -class TreeMaker(Protocol): - def __truediv__(self, *args, **kwargs): - ... # pragma: no cover - - def mkdir(self, **kwargs): - ... # pragma: no cover - - def write_text(self, content, **kwargs): - ... # pragma: no cover - - def write_bytes(self, content): - ... # pragma: no cover - - def symlink_to(self, target): - ... # pragma: no cover - - -def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker: - return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore - - -def build( - spec: FilesSpec, - prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore -): - """ - Build a set of files/directories, as described by the spec. - - Each key represents a pathname, and the value represents - the content. Content may be a nested directory. - - >>> spec = { - ... 'README.txt': "A README file", - ... "foo": { - ... "__init__.py": "", - ... "bar": { - ... "__init__.py": "", - ... }, - ... "baz.py": "# Some code", - ... "bar.py": Symlink("baz.py"), - ... }, - ... "bing": Symlink("foo"), - ... } - >>> target = getfixture('tmp_path') - >>> build(spec, target) - >>> target.joinpath('foo/baz.py').read_text(encoding='utf-8') - '# Some code' - >>> target.joinpath('bing/bar.py').read_text(encoding='utf-8') - '# Some code' - """ - for name, contents in spec.items(): - create(contents, _ensure_tree_maker(prefix) / name) - - -@functools.singledispatch -def create(content: Union[str, bytes, FilesSpec], path): - path.mkdir(exist_ok=True) - build(content, prefix=path) # type: ignore - - -@create.register -def _(content: bytes, path): - path.write_bytes(content) - - -@create.register -def _(content: str, path): - path.write_text(content, encoding='utf-8') - - -@create.register -def _(content: Symlink, path): - path.symlink_to(content) - - -class Recording: - """ - A TreeMaker object that records everything that would be written. - - >>> r = Recording() - >>> build({'foo': {'foo1.txt': 'yes'}, 'bar.txt': 'abc'}, r) - >>> r.record - ['foo/foo1.txt', 'bar.txt'] - """ - - def __init__(self, loc=pathlib.PurePosixPath(), record=None): - self.loc = loc - self.record = record if record is not None else [] - - def __truediv__(self, other): - return Recording(self.loc / other, self.record) - - def write_text(self, content, **kwargs): - self.record.append(str(self.loc)) - - write_bytes = write_text - - def mkdir(self, **kwargs): - return - - def symlink_to(self, target): - pass diff --git a/Lib/test/test_importlib/data/__init__.py b/Lib/test/test_importlib/data/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/Lib/test/test_importlib/data/example-21.12-py3-none-any.whl b/Lib/test/test_importlib/data/example-21.12-py3-none-any.whl deleted file mode 100644 index 641ab07..0000000 Binary files a/Lib/test/test_importlib/data/example-21.12-py3-none-any.whl and /dev/null differ diff --git a/Lib/test/test_importlib/data/example-21.12-py3.6.egg b/Lib/test/test_importlib/data/example-21.12-py3.6.egg deleted file mode 100644 index cdb298a..0000000 Binary files a/Lib/test/test_importlib/data/example-21.12-py3.6.egg and /dev/null differ diff --git a/Lib/test/test_importlib/data/example2-1.0.0-py3-none-any.whl b/Lib/test/test_importlib/data/example2-1.0.0-py3-none-any.whl deleted file mode 100644 index 5ca9365..0000000 Binary files a/Lib/test/test_importlib/data/example2-1.0.0-py3-none-any.whl and /dev/null differ diff --git a/Lib/test/test_importlib/fixtures.py b/Lib/test/test_importlib/fixtures.py deleted file mode 100644 index 8c97335..0000000 --- a/Lib/test/test_importlib/fixtures.py +++ /dev/null @@ -1,378 +0,0 @@ -import os -import sys -import copy -import json -import shutil -import pathlib -import tempfile -import textwrap -import functools -import contextlib - -from test.support.os_helper import FS_NONASCII -from test.support import requires_zlib - -from . import _path -from ._path import FilesSpec - - -try: - from importlib import resources # type: ignore - - getattr(resources, 'files') - getattr(resources, 'as_file') -except (ImportError, AttributeError): - import importlib_resources as resources # type: ignore - - -@contextlib.contextmanager -def tempdir(): - tmpdir = tempfile.mkdtemp() - try: - yield pathlib.Path(tmpdir) - finally: - shutil.rmtree(tmpdir) - - -@contextlib.contextmanager -def save_cwd(): - orig = os.getcwd() - try: - yield - finally: - os.chdir(orig) - - -@contextlib.contextmanager -def tempdir_as_cwd(): - with tempdir() as tmp: - with save_cwd(): - os.chdir(str(tmp)) - yield tmp - - -@contextlib.contextmanager -def install_finder(finder): - sys.meta_path.append(finder) - try: - yield - finally: - sys.meta_path.remove(finder) - - -class Fixtures: - def setUp(self): - self.fixtures = contextlib.ExitStack() - self.addCleanup(self.fixtures.close) - - -class SiteDir(Fixtures): - def setUp(self): - super().setUp() - self.site_dir = self.fixtures.enter_context(tempdir()) - - -class OnSysPath(Fixtures): - @staticmethod - @contextlib.contextmanager - def add_sys_path(dir): - sys.path[:0] = [str(dir)] - try: - yield - finally: - sys.path.remove(str(dir)) - - def setUp(self): - super().setUp() - self.fixtures.enter_context(self.add_sys_path(self.site_dir)) - - -class SiteBuilder(SiteDir): - def setUp(self): - super().setUp() - for cls in self.__class__.mro(): - with contextlib.suppress(AttributeError): - build_files(cls.files, prefix=self.site_dir) - - -class DistInfoPkg(OnSysPath, SiteBuilder): - files: FilesSpec = { - "distinfo_pkg-1.0.0.dist-info": { - "METADATA": """ - Name: distinfo-pkg - Author: Steven Ma - Version: 1.0.0 - Requires-Dist: wheel >= 1.0 - Requires-Dist: pytest; extra == 'test' - Keywords: sample package - - Once upon a time - There was a distinfo pkg - """, - "RECORD": "mod.py,sha256=abc,20\n", - "entry_points.txt": """ - [entries] - main = mod:main - ns:sub = mod:main - """, - }, - "mod.py": """ - def main(): - print("hello world") - """, - } - - def make_uppercase(self): - """ - Rewrite metadata with everything uppercase. - """ - shutil.rmtree(self.site_dir / "distinfo_pkg-1.0.0.dist-info") - files = copy.deepcopy(DistInfoPkg.files) - info = files["distinfo_pkg-1.0.0.dist-info"] - info["METADATA"] = info["METADATA"].upper() - build_files(files, self.site_dir) - - -class DistInfoPkgEditable(DistInfoPkg): - """ - Package with a PEP 660 direct_url.json. - """ - - some_hash = '524127ce937f7cb65665130c695abd18ca386f60bb29687efb976faa1596fdcc' - files: FilesSpec = { - 'distinfo_pkg-1.0.0.dist-info': { - 'direct_url.json': json.dumps( - { - "archive_info": { - "hash": f"sha256={some_hash}", - "hashes": {"sha256": f"{some_hash}"}, - }, - "url": "file:///path/to/distinfo_pkg-1.0.0.editable-py3-none-any.whl", - } - ) - }, - } - - -class DistInfoPkgWithDot(OnSysPath, SiteBuilder): - files: FilesSpec = { - "pkg_dot-1.0.0.dist-info": { - "METADATA": """ - Name: pkg.dot - Version: 1.0.0 - """, - }, - } - - -class DistInfoPkgWithDotLegacy(OnSysPath, SiteBuilder): - files: FilesSpec = { - "pkg.dot-1.0.0.dist-info": { - "METADATA": """ - Name: pkg.dot - Version: 1.0.0 - """, - }, - "pkg.lot.egg-info": { - "METADATA": """ - Name: pkg.lot - Version: 1.0.0 - """, - }, - } - - -class DistInfoPkgOffPath(SiteBuilder): - files = DistInfoPkg.files - - -class EggInfoPkg(OnSysPath, SiteBuilder): - files: FilesSpec = { - "egginfo_pkg.egg-info": { - "PKG-INFO": """ - Name: egginfo-pkg - Author: Steven Ma - License: Unknown - Version: 1.0.0 - Classifier: Intended Audience :: Developers - Classifier: Topic :: Software Development :: Libraries - Keywords: sample package - Description: Once upon a time - There was an egginfo package - """, - "SOURCES.txt": """ - mod.py - egginfo_pkg.egg-info/top_level.txt - """, - "entry_points.txt": """ - [entries] - main = mod:main - """, - "requires.txt": """ - wheel >= 1.0; python_version >= "2.7" - [test] - pytest - """, - "top_level.txt": "mod\n", - }, - "mod.py": """ - def main(): - print("hello world") - """, - } - - -class EggInfoPkgPipInstalledNoToplevel(OnSysPath, SiteBuilder): - files: FilesSpec = { - "egg_with_module_pkg.egg-info": { - "PKG-INFO": "Name: egg_with_module-pkg", - # SOURCES.txt is made from the source archive, and contains files - # (setup.py) that are not present after installation. - "SOURCES.txt": """ - egg_with_module.py - setup.py - egg_with_module_pkg.egg-info/PKG-INFO - egg_with_module_pkg.egg-info/SOURCES.txt - egg_with_module_pkg.egg-info/top_level.txt - """, - # installed-files.txt is written by pip, and is a strictly more - # accurate source than SOURCES.txt as to the installed contents of - # the package. - "installed-files.txt": """ - ../egg_with_module.py - PKG-INFO - SOURCES.txt - top_level.txt - """, - # missing top_level.txt (to trigger fallback to installed-files.txt) - }, - "egg_with_module.py": """ - def main(): - print("hello world") - """, - } - - -class EggInfoPkgPipInstalledNoModules(OnSysPath, SiteBuilder): - files: FilesSpec = { - "egg_with_no_modules_pkg.egg-info": { - "PKG-INFO": "Name: egg_with_no_modules-pkg", - # SOURCES.txt is made from the source archive, and contains files - # (setup.py) that are not present after installation. - "SOURCES.txt": """ - setup.py - egg_with_no_modules_pkg.egg-info/PKG-INFO - egg_with_no_modules_pkg.egg-info/SOURCES.txt - egg_with_no_modules_pkg.egg-info/top_level.txt - """, - # installed-files.txt is written by pip, and is a strictly more - # accurate source than SOURCES.txt as to the installed contents of - # the package. - "installed-files.txt": """ - PKG-INFO - SOURCES.txt - top_level.txt - """, - # top_level.txt correctly reflects that no modules are installed - "top_level.txt": b"\n", - }, - } - - -class EggInfoPkgSourcesFallback(OnSysPath, SiteBuilder): - files: FilesSpec = { - "sources_fallback_pkg.egg-info": { - "PKG-INFO": "Name: sources_fallback-pkg", - # SOURCES.txt is made from the source archive, and contains files - # (setup.py) that are not present after installation. - "SOURCES.txt": """ - sources_fallback.py - setup.py - sources_fallback_pkg.egg-info/PKG-INFO - sources_fallback_pkg.egg-info/SOURCES.txt - """, - # missing installed-files.txt (i.e. not installed by pip) and - # missing top_level.txt (to trigger fallback to SOURCES.txt) - }, - "sources_fallback.py": """ - def main(): - print("hello world") - """, - } - - -class EggInfoFile(OnSysPath, SiteBuilder): - files: FilesSpec = { - "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 - """, - } - - -# dedent all text strings before writing -orig = _path.create.registry[str] -_path.create.register(str, lambda content, path: orig(DALS(content), path)) - - -build_files = _path.build - - -def build_record(file_defs): - return ''.join(f'{name},,\n' for name in record_names(file_defs)) - - -def record_names(file_defs): - recording = _path.Recording() - _path.build(file_defs, recording) - return recording.record - - -class FileBuilder: - def unicode_filename(self): - return FS_NONASCII or self.skip("File system does not support non-ascii.") - - -def DALS(str): - "Dedent and left-strip" - return textwrap.dedent(str).lstrip() - - -@requires_zlib() -class ZipFixtures: - root = 'test.test_importlib.data' - - def _fixture_on_path(self, filename): - pkg_file = resources.files(self.root).joinpath(filename) - file = self.resources.enter_context(resources.as_file(pkg_file)) - assert file.name.startswith('example'), file.name - sys.path.insert(0, str(file)) - self.resources.callback(sys.path.pop, 0) - - def setUp(self): - # Add self.zip_name to the front of sys.path. - self.resources = contextlib.ExitStack() - self.addCleanup(self.resources.close) - - -def parameterize(*args_set): - """Run test method with a series of parameters.""" - - def wrapper(func): - @functools.wraps(func) - def _inner(self): - for args in args_set: - with self.subTest(**args): - func(self, **args) - - return _inner - - return wrapper diff --git a/Lib/test/test_importlib/metadata/__init__.py b/Lib/test/test_importlib/metadata/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Lib/test/test_importlib/metadata/_context.py b/Lib/test/test_importlib/metadata/_context.py new file mode 100644 index 0000000..8a53eb5 --- /dev/null +++ b/Lib/test/test_importlib/metadata/_context.py @@ -0,0 +1,13 @@ +import contextlib + + +# from jaraco.context 4.3 +class suppress(contextlib.suppress, contextlib.ContextDecorator): + """ + A version of contextlib.suppress with decorator support. + + >>> @suppress(KeyError) + ... def key_error(): + ... {}[''] + >>> key_error() + """ diff --git a/Lib/test/test_importlib/metadata/_path.py b/Lib/test/test_importlib/metadata/_path.py new file mode 100644 index 0000000..25c799f --- /dev/null +++ b/Lib/test/test_importlib/metadata/_path.py @@ -0,0 +1,120 @@ +# from jaraco.path 3.7 + +import functools +import pathlib +from typing import Dict, Protocol, Union +from typing import runtime_checkable + + +class Symlink(str): + """ + A string indicating the target of a symlink. + """ + + +FilesSpec = Dict[str, Union[str, bytes, Symlink, 'FilesSpec']] # type: ignore + + +@runtime_checkable +class TreeMaker(Protocol): + def __truediv__(self, *args, **kwargs): + ... # pragma: no cover + + def mkdir(self, **kwargs): + ... # pragma: no cover + + def write_text(self, content, **kwargs): + ... # pragma: no cover + + def write_bytes(self, content): + ... # pragma: no cover + + def symlink_to(self, target): + ... # pragma: no cover + + +def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker: + return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore + + +def build( + spec: FilesSpec, + prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore +): + """ + Build a set of files/directories, as described by the spec. + + Each key represents a pathname, and the value represents + the content. Content may be a nested directory. + + >>> spec = { + ... 'README.txt': "A README file", + ... "foo": { + ... "__init__.py": "", + ... "bar": { + ... "__init__.py": "", + ... }, + ... "baz.py": "# Some code", + ... "bar.py": Symlink("baz.py"), + ... }, + ... "bing": Symlink("foo"), + ... } + >>> target = getfixture('tmp_path') + >>> build(spec, target) + >>> target.joinpath('foo/baz.py').read_text(encoding='utf-8') + '# Some code' + >>> target.joinpath('bing/bar.py').read_text(encoding='utf-8') + '# Some code' + """ + for name, contents in spec.items(): + create(contents, _ensure_tree_maker(prefix) / name) + + +@functools.singledispatch +def create(content: Union[str, bytes, FilesSpec], path): + path.mkdir(exist_ok=True) + build(content, prefix=path) # type: ignore + + +@create.register +def _(content: bytes, path): + path.write_bytes(content) + + +@create.register +def _(content: str, path): + path.write_text(content, encoding='utf-8') + + +@create.register +def _(content: Symlink, path): + path.symlink_to(content) + + +class Recording: + """ + A TreeMaker object that records everything that would be written. + + >>> r = Recording() + >>> build({'foo': {'foo1.txt': 'yes'}, 'bar.txt': 'abc'}, r) + >>> r.record + ['foo/foo1.txt', 'bar.txt'] + """ + + def __init__(self, loc=pathlib.PurePosixPath(), record=None): + self.loc = loc + self.record = record if record is not None else [] + + def __truediv__(self, other): + return Recording(self.loc / other, self.record) + + def write_text(self, content, **kwargs): + self.record.append(str(self.loc)) + + write_bytes = write_text + + def mkdir(self, **kwargs): + return + + def symlink_to(self, target): + pass diff --git a/Lib/test/test_importlib/metadata/data/__init__.py b/Lib/test/test_importlib/metadata/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Lib/test/test_importlib/metadata/data/example-21.12-py3-none-any.whl b/Lib/test/test_importlib/metadata/data/example-21.12-py3-none-any.whl new file mode 100644 index 0000000..641ab07 Binary files /dev/null and b/Lib/test/test_importlib/metadata/data/example-21.12-py3-none-any.whl differ diff --git a/Lib/test/test_importlib/metadata/data/example-21.12-py3.6.egg b/Lib/test/test_importlib/metadata/data/example-21.12-py3.6.egg new file mode 100644 index 0000000..cdb298a Binary files /dev/null and b/Lib/test/test_importlib/metadata/data/example-21.12-py3.6.egg differ diff --git a/Lib/test/test_importlib/metadata/data/example2-1.0.0-py3-none-any.whl b/Lib/test/test_importlib/metadata/data/example2-1.0.0-py3-none-any.whl new file mode 100644 index 0000000..5ca9365 Binary files /dev/null and b/Lib/test/test_importlib/metadata/data/example2-1.0.0-py3-none-any.whl differ diff --git a/Lib/test/test_importlib/metadata/fixtures.py b/Lib/test/test_importlib/metadata/fixtures.py new file mode 100644 index 0000000..f23ac5d --- /dev/null +++ b/Lib/test/test_importlib/metadata/fixtures.py @@ -0,0 +1,380 @@ +import os +import sys +import copy +import json +import shutil +import pathlib +import tempfile +import textwrap +import functools +import contextlib + +from test.support import import_helper +from test.support.os_helper import FS_NONASCII +from test.support import requires_zlib + +from . import _path +from ._path import FilesSpec + + +try: + from importlib import resources # type: ignore + + getattr(resources, 'files') + getattr(resources, 'as_file') +except (ImportError, AttributeError): + import importlib_resources as resources # type: ignore + + +@contextlib.contextmanager +def tempdir(): + tmpdir = tempfile.mkdtemp() + try: + yield pathlib.Path(tmpdir) + finally: + shutil.rmtree(tmpdir) + + +@contextlib.contextmanager +def save_cwd(): + orig = os.getcwd() + try: + yield + finally: + os.chdir(orig) + + +@contextlib.contextmanager +def tempdir_as_cwd(): + with tempdir() as tmp: + with save_cwd(): + os.chdir(str(tmp)) + yield tmp + + +@contextlib.contextmanager +def install_finder(finder): + sys.meta_path.append(finder) + try: + yield + finally: + sys.meta_path.remove(finder) + + +class Fixtures: + def setUp(self): + self.fixtures = contextlib.ExitStack() + self.addCleanup(self.fixtures.close) + + +class SiteDir(Fixtures): + def setUp(self): + super().setUp() + self.site_dir = self.fixtures.enter_context(tempdir()) + + +class OnSysPath(Fixtures): + @staticmethod + @contextlib.contextmanager + def add_sys_path(dir): + sys.path[:0] = [str(dir)] + try: + yield + finally: + sys.path.remove(str(dir)) + + def setUp(self): + super().setUp() + self.fixtures.enter_context(self.add_sys_path(self.site_dir)) + self.fixtures.enter_context(import_helper.isolated_modules()) + + +class SiteBuilder(SiteDir): + def setUp(self): + super().setUp() + for cls in self.__class__.mro(): + with contextlib.suppress(AttributeError): + build_files(cls.files, prefix=self.site_dir) + + +class DistInfoPkg(OnSysPath, SiteBuilder): + files: FilesSpec = { + "distinfo_pkg-1.0.0.dist-info": { + "METADATA": """ + Name: distinfo-pkg + Author: Steven Ma + Version: 1.0.0 + Requires-Dist: wheel >= 1.0 + Requires-Dist: pytest; extra == 'test' + Keywords: sample package + + Once upon a time + There was a distinfo pkg + """, + "RECORD": "mod.py,sha256=abc,20\n", + "entry_points.txt": """ + [entries] + main = mod:main + ns:sub = mod:main + """, + }, + "mod.py": """ + def main(): + print("hello world") + """, + } + + def make_uppercase(self): + """ + Rewrite metadata with everything uppercase. + """ + shutil.rmtree(self.site_dir / "distinfo_pkg-1.0.0.dist-info") + files = copy.deepcopy(DistInfoPkg.files) + info = files["distinfo_pkg-1.0.0.dist-info"] + info["METADATA"] = info["METADATA"].upper() + build_files(files, self.site_dir) + + +class DistInfoPkgEditable(DistInfoPkg): + """ + Package with a PEP 660 direct_url.json. + """ + + some_hash = '524127ce937f7cb65665130c695abd18ca386f60bb29687efb976faa1596fdcc' + files: FilesSpec = { + 'distinfo_pkg-1.0.0.dist-info': { + 'direct_url.json': json.dumps( + { + "archive_info": { + "hash": f"sha256={some_hash}", + "hashes": {"sha256": f"{some_hash}"}, + }, + "url": "file:///path/to/distinfo_pkg-1.0.0.editable-py3-none-any.whl", + } + ) + }, + } + + +class DistInfoPkgWithDot(OnSysPath, SiteBuilder): + files: FilesSpec = { + "pkg_dot-1.0.0.dist-info": { + "METADATA": """ + Name: pkg.dot + Version: 1.0.0 + """, + }, + } + + +class DistInfoPkgWithDotLegacy(OnSysPath, SiteBuilder): + files: FilesSpec = { + "pkg.dot-1.0.0.dist-info": { + "METADATA": """ + Name: pkg.dot + Version: 1.0.0 + """, + }, + "pkg.lot.egg-info": { + "METADATA": """ + Name: pkg.lot + Version: 1.0.0 + """, + }, + } + + +class DistInfoPkgOffPath(SiteBuilder): + files = DistInfoPkg.files + + +class EggInfoPkg(OnSysPath, SiteBuilder): + files: FilesSpec = { + "egginfo_pkg.egg-info": { + "PKG-INFO": """ + Name: egginfo-pkg + Author: Steven Ma + License: Unknown + Version: 1.0.0 + Classifier: Intended Audience :: Developers + Classifier: Topic :: Software Development :: Libraries + Keywords: sample package + Description: Once upon a time + There was an egginfo package + """, + "SOURCES.txt": """ + mod.py + egginfo_pkg.egg-info/top_level.txt + """, + "entry_points.txt": """ + [entries] + main = mod:main + """, + "requires.txt": """ + wheel >= 1.0; python_version >= "2.7" + [test] + pytest + """, + "top_level.txt": "mod\n", + }, + "mod.py": """ + def main(): + print("hello world") + """, + } + + +class EggInfoPkgPipInstalledNoToplevel(OnSysPath, SiteBuilder): + files: FilesSpec = { + "egg_with_module_pkg.egg-info": { + "PKG-INFO": "Name: egg_with_module-pkg", + # SOURCES.txt is made from the source archive, and contains files + # (setup.py) that are not present after installation. + "SOURCES.txt": """ + egg_with_module.py + setup.py + egg_with_module_pkg.egg-info/PKG-INFO + egg_with_module_pkg.egg-info/SOURCES.txt + egg_with_module_pkg.egg-info/top_level.txt + """, + # installed-files.txt is written by pip, and is a strictly more + # accurate source than SOURCES.txt as to the installed contents of + # the package. + "installed-files.txt": """ + ../egg_with_module.py + PKG-INFO + SOURCES.txt + top_level.txt + """, + # missing top_level.txt (to trigger fallback to installed-files.txt) + }, + "egg_with_module.py": """ + def main(): + print("hello world") + """, + } + + +class EggInfoPkgPipInstalledNoModules(OnSysPath, SiteBuilder): + files: FilesSpec = { + "egg_with_no_modules_pkg.egg-info": { + "PKG-INFO": "Name: egg_with_no_modules-pkg", + # SOURCES.txt is made from the source archive, and contains files + # (setup.py) that are not present after installation. + "SOURCES.txt": """ + setup.py + egg_with_no_modules_pkg.egg-info/PKG-INFO + egg_with_no_modules_pkg.egg-info/SOURCES.txt + egg_with_no_modules_pkg.egg-info/top_level.txt + """, + # installed-files.txt is written by pip, and is a strictly more + # accurate source than SOURCES.txt as to the installed contents of + # the package. + "installed-files.txt": """ + PKG-INFO + SOURCES.txt + top_level.txt + """, + # top_level.txt correctly reflects that no modules are installed + "top_level.txt": b"\n", + }, + } + + +class EggInfoPkgSourcesFallback(OnSysPath, SiteBuilder): + files: FilesSpec = { + "sources_fallback_pkg.egg-info": { + "PKG-INFO": "Name: sources_fallback-pkg", + # SOURCES.txt is made from the source archive, and contains files + # (setup.py) that are not present after installation. + "SOURCES.txt": """ + sources_fallback.py + setup.py + sources_fallback_pkg.egg-info/PKG-INFO + sources_fallback_pkg.egg-info/SOURCES.txt + """, + # missing installed-files.txt (i.e. not installed by pip) and + # missing top_level.txt (to trigger fallback to SOURCES.txt) + }, + "sources_fallback.py": """ + def main(): + print("hello world") + """, + } + + +class EggInfoFile(OnSysPath, SiteBuilder): + files: FilesSpec = { + "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 + """, + } + + +# dedent all text strings before writing +orig = _path.create.registry[str] +_path.create.register(str, lambda content, path: orig(DALS(content), path)) + + +build_files = _path.build + + +def build_record(file_defs): + return ''.join(f'{name},,\n' for name in record_names(file_defs)) + + +def record_names(file_defs): + recording = _path.Recording() + _path.build(file_defs, recording) + return recording.record + + +class FileBuilder: + def unicode_filename(self): + return FS_NONASCII or self.skip("File system does not support non-ascii.") + + +def DALS(str): + "Dedent and left-strip" + return textwrap.dedent(str).lstrip() + + +@requires_zlib() +class ZipFixtures: + root = 'test.test_importlib.metadata.data' + + def _fixture_on_path(self, filename): + pkg_file = resources.files(self.root).joinpath(filename) + file = self.resources.enter_context(resources.as_file(pkg_file)) + assert file.name.startswith('example'), file.name + sys.path.insert(0, str(file)) + self.resources.callback(sys.path.pop, 0) + + def setUp(self): + # Add self.zip_name to the front of sys.path. + self.resources = contextlib.ExitStack() + self.addCleanup(self.resources.close) + + +def parameterize(*args_set): + """Run test method with a series of parameters.""" + + def wrapper(func): + @functools.wraps(func) + def _inner(self): + for args in args_set: + with self.subTest(**args): + func(self, **args) + + return _inner + + return wrapper diff --git a/Lib/test/test_importlib/metadata/stubs.py b/Lib/test/test_importlib/metadata/stubs.py new file mode 100644 index 0000000..e5b011c --- /dev/null +++ b/Lib/test/test_importlib/metadata/stubs.py @@ -0,0 +1,10 @@ +import unittest + + +class fake_filesystem_unittest: + """ + Stubbed version of the pyfakefs module + """ + class TestCase(unittest.TestCase): + def setUpPyfakefs(self): + self.skipTest("pyfakefs not available") diff --git a/Lib/test/test_importlib/metadata/test_api.py b/Lib/test/test_importlib/metadata/test_api.py new file mode 100644 index 0000000..33c6e85 --- /dev/null +++ b/Lib/test/test_importlib/metadata/test_api.py @@ -0,0 +1,322 @@ +import re +import textwrap +import unittest +import warnings +import importlib +import contextlib + +from . import fixtures +from importlib.metadata import ( + Distribution, + PackageNotFoundError, + distribution, + entry_points, + files, + metadata, + requires, + version, +) + + +@contextlib.contextmanager +def suppress_known_deprecation(): + with warnings.catch_warnings(record=True) as ctx: + warnings.simplefilter('default', category=DeprecationWarning) + yield ctx + + +class APITests( + fixtures.EggInfoPkg, + fixtures.EggInfoPkgPipInstalledNoToplevel, + fixtures.EggInfoPkgPipInstalledNoModules, + fixtures.EggInfoPkgSourcesFallback, + fixtures.DistInfoPkg, + fixtures.DistInfoPkgWithDot, + fixtures.EggInfoFile, + unittest.TestCase, +): + version_pattern = r'\d+\.\d+(\.\d)?' + + def test_retrieves_version_of_self(self): + pkg_version = version('egginfo-pkg') + assert isinstance(pkg_version, str) + assert re.match(self.version_pattern, pkg_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(PackageNotFoundError): + distribution('does-not-exist') + + def test_name_normalization(self): + names = 'pkg.dot', 'pkg_dot', 'pkg-dot', 'pkg..dot', 'Pkg.Dot' + for name in names: + with self.subTest(name): + assert distribution(name).metadata['Name'] == 'pkg.dot' + + def test_prefix_not_matched(self): + prefixes = 'p', 'pkg', 'pkg.' + for prefix in prefixes: + with self.subTest(prefix): + with self.assertRaises(PackageNotFoundError): + distribution(prefix) + + def test_for_top_level(self): + tests = [ + ('egginfo-pkg', 'mod'), + ('egg_with_no_modules-pkg', ''), + ] + for pkg_name, expect_content in tests: + with self.subTest(pkg_name): + self.assertEqual( + distribution(pkg_name).read_text('top_level.txt').strip(), + expect_content, + ) + + def test_read_text(self): + tests = [ + ('egginfo-pkg', 'mod\n'), + ('egg_with_no_modules-pkg', '\n'), + ] + for pkg_name, expect_content in tests: + with self.subTest(pkg_name): + top_level = [ + path for path in files(pkg_name) if path.name == 'top_level.txt' + ][0] + self.assertEqual(top_level.read_text(), expect_content) + + def test_entry_points(self): + eps = entry_points() + assert 'entries' in eps.groups + entries = eps.select(group='entries') + assert 'main' in entries.names + ep = entries['main'] + self.assertEqual(ep.value, 'mod:main') + self.assertEqual(ep.extras, []) + + def test_entry_points_distribution(self): + entries = entry_points(group='entries') + for entry in ("main", "ns:sub"): + ep = entries[entry] + self.assertIn(ep.dist.name, ('distinfo-pkg', 'egginfo-pkg')) + self.assertEqual(ep.dist.version, "1.0.0") + + def test_entry_points_unique_packages_normalized(self): + """ + Entry points should only be exposed for the first package + on sys.path with a given name (even when normalized). + """ + alt_site_dir = self.fixtures.enter_context(fixtures.tempdir()) + self.fixtures.enter_context(self.add_sys_path(alt_site_dir)) + alt_pkg = { + "DistInfo_pkg-1.1.0.dist-info": { + "METADATA": """ + Name: distinfo-pkg + Version: 1.1.0 + """, + "entry_points.txt": """ + [entries] + main = mod:altmain + """, + }, + } + fixtures.build_files(alt_pkg, alt_site_dir) + entries = entry_points(group='entries') + assert not any( + ep.dist.name == 'distinfo-pkg' and ep.dist.version == '1.0.0' + for ep in entries + ) + # ns:sub doesn't exist in alt_pkg + assert 'ns:sub' not in entries.names + + def test_entry_points_missing_name(self): + with self.assertRaises(KeyError): + entry_points(group='entries')['missing'] + + def test_entry_points_missing_group(self): + assert entry_points(group='missing') == () + + def test_entry_points_allows_no_attributes(self): + ep = entry_points().select(group='entries', name='main') + with self.assertRaises(AttributeError): + ep.foo = 4 + + def test_metadata_for_this_package(self): + md = metadata('egginfo-pkg') + assert md['author'] == 'Steven Ma' + assert md['LICENSE'] == 'Unknown' + assert md['Name'] == 'egginfo-pkg' + classifiers = md.get_all('Classifier') + assert 'Topic :: Software Development :: Libraries' in classifiers + + def test_missing_key_legacy(self): + """ + Requesting a missing key will still return None, but warn. + """ + md = metadata('distinfo-pkg') + with suppress_known_deprecation(): + assert md['does-not-exist'] is None + + def test_get_key(self): + """ + Getting a key gets the key. + """ + md = metadata('egginfo-pkg') + assert md.get('Name') == 'egginfo-pkg' + + def test_get_missing_key(self): + """ + Requesting a missing key will return None. + """ + md = metadata('distinfo-pkg') + assert md.get('does-not-exist') is None + + @staticmethod + def _test_files(files): + root = files[0].root + for file in files: + assert file.root == root + assert not file.hash or file.hash.value + assert not file.hash or file.hash.mode == 'sha256' + assert not file.size or file.size >= 0 + assert file.locate().exists() + assert isinstance(file.read_binary(), bytes) + if file.name.endswith('.py'): + file.read_text() + + def test_file_hash_repr(self): + util = [p for p in files('distinfo-pkg') if p.name == 'mod.py'][0] + self.assertRegex(repr(util.hash), '') + + def test_files_dist_info(self): + self._test_files(files('distinfo-pkg')) + + def test_files_egg_info(self): + self._test_files(files('egginfo-pkg')) + self._test_files(files('egg_with_module-pkg')) + self._test_files(files('egg_with_no_modules-pkg')) + self._test_files(files('sources_fallback-pkg')) + + def test_version_egg_info_file(self): + self.assertEqual(version('egginfo-file'), '0.1') + + def test_requires_egg_info_file(self): + requirements = requires('egginfo-file') + self.assertIsNone(requirements) + + 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_egg_info_empty(self): + fixtures.build_files( + { + 'requires.txt': '', + }, + self.site_dir.joinpath('egginfo_pkg.egg-info'), + ) + deps = requires('egginfo-pkg') + assert deps == [] + + def test_requires_dist_info(self): + deps = requires('distinfo-pkg') + assert len(deps) == 2 + assert all(deps) + assert 'wheel >= 1.0' in deps + assert "pytest; extra == 'test'" in deps + + def test_more_complex_deps_requires_text(self): + requires = textwrap.dedent( + """ + dep1 + dep2 + + [:python_version < "3"] + dep3 + + [extra1] + dep4 + dep6@ git+https://example.com/python/dep.git@v1.0.0 + + [extra2:python_version < "3"] + dep5 + """ + ) + deps = sorted(Distribution._deps_from_requires_text(requires)) + expected = [ + 'dep1', + 'dep2', + 'dep3; python_version < "3"', + 'dep4; extra == "extra1"', + 'dep5; (python_version < "3") and extra == "extra2"', + 'dep6@ git+https://example.com/python/dep.git@v1.0.0 ; extra == "extra1"', + ] + # It's important that the environment marker expression be + # wrapped in parentheses to avoid the following 'and' binding more + # tightly than some other part of the environment expression. + + assert deps == expected + + def test_as_json(self): + md = metadata('distinfo-pkg').json + assert 'name' in md + assert md['keywords'] == ['sample', 'package'] + desc = md['description'] + assert desc.startswith('Once upon a time\nThere was') + assert len(md['requires_dist']) == 2 + + def test_as_json_egg_info(self): + md = metadata('egginfo-pkg').json + assert 'name' in md + assert md['keywords'] == ['sample', 'package'] + desc = md['description'] + assert desc.startswith('Once upon a time\nThere was') + assert len(md['classifier']) == 2 + + def test_as_json_odd_case(self): + self.make_uppercase() + md = metadata('distinfo-pkg').json + assert 'name' in md + assert len(md['requires_dist']) == 2 + assert md['keywords'] == ['SAMPLE', 'PACKAGE'] + + +class LegacyDots(fixtures.DistInfoPkgWithDotLegacy, unittest.TestCase): + def test_name_normalization(self): + names = 'pkg.dot', 'pkg_dot', 'pkg-dot', 'pkg..dot', 'Pkg.Dot' + for name in names: + with self.subTest(name): + assert distribution(name).metadata['Name'] == 'pkg.dot' + + def test_name_normalization_versionless_egg_info(self): + names = 'pkg.lot', 'pkg_lot', 'pkg-lot', 'pkg..lot', 'Pkg.Lot' + for name in names: + with self.subTest(name): + assert distribution(name).metadata['Name'] == 'pkg.lot' + + +class OffSysPathTests(fixtures.DistInfoPkgOffPath, unittest.TestCase): + def test_find_distributions_specified_path(self): + 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' + + +class InvalidateCache(unittest.TestCase): + def test_invalidate_cache(self): + # No externally observable behavior, but ensures test coverage... + importlib.invalidate_caches() diff --git a/Lib/test/test_importlib/metadata/test_main.py b/Lib/test/test_importlib/metadata/test_main.py new file mode 100644 index 0000000..0a769b8 --- /dev/null +++ b/Lib/test/test_importlib/metadata/test_main.py @@ -0,0 +1,471 @@ +import re +import pickle +import unittest +import warnings +import importlib.metadata +import contextlib +from test.support import os_helper + +try: + import pyfakefs.fake_filesystem_unittest as ffs +except ImportError: + from .stubs import fake_filesystem_unittest as ffs + +from . import fixtures +from ._context import suppress +from ._path import Symlink +from importlib.metadata import ( + Distribution, + EntryPoint, + PackageNotFoundError, + _unique, + distributions, + entry_points, + metadata, + packages_distributions, + version, +) + + +@contextlib.contextmanager +def suppress_known_deprecation(): + with warnings.catch_warnings(record=True) as ctx: + warnings.simplefilter('default', category=DeprecationWarning) + yield ctx + + +class BasicTests(fixtures.DistInfoPkg, unittest.TestCase): + version_pattern = r'\d+\.\d+(\.\d)?' + + def test_retrieves_version_of_self(self): + 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(PackageNotFoundError): + Distribution.from_name('does-not-exist') + + def test_package_not_found_mentions_metadata(self): + """ + When a package is not found, that could indicate that the + package is not installed or that it is installed without + metadata. Ensure the exception mentions metadata to help + guide users toward the cause. See #124. + """ + with self.assertRaises(PackageNotFoundError) as ctx: + Distribution.from_name('does-not-exist') + + assert "metadata" in str(ctx.exception) + + # expected to fail until ABC is enforced + @suppress(AssertionError) + @suppress_known_deprecation() + def test_abc_enforced(self): + with self.assertRaises(TypeError): + type('DistributionSubclass', (Distribution,), {})() + + @fixtures.parameterize( + dict(name=None), + dict(name=''), + ) + def test_invalid_inputs_to_from_name(self, name): + with self.assertRaises(Exception): + Distribution.from_name(name) + + +class ImportTests(fixtures.DistInfoPkg, unittest.TestCase): + def test_import_nonexistent_module(self): + # Ensure that the MetadataPathFinder does not crash an import of a + # non-existent module. + with self.assertRaises(ImportError): + importlib.import_module('does_not_exist') + + def test_resolve(self): + ep = entry_points(group='entries')['main'] + self.assertEqual(ep.load().__name__, "main") + + def test_entrypoint_with_colon_in_name(self): + ep = entry_points(group='entries')['ns:sub'] + self.assertEqual(ep.value, 'mod:main') + + def test_resolve_without_attr(self): + ep = EntryPoint( + name='ep', + value='importlib.metadata', + group='grp', + ) + assert ep.load() is importlib.metadata + + +class NameNormalizationTests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase): + @staticmethod + def make_pkg(name): + """ + Create minimal metadata for a dist-info package with + the indicated name on the file system. + """ + return { + f'{name}.dist-info': { + 'METADATA': 'VERSION: 1.0\n', + }, + } + + def test_dashes_in_dist_name_found_as_underscores(self): + """ + For a package with a dash in the name, the dist-info metadata + uses underscores in the name. Ensure the metadata loads. + """ + fixtures.build_files(self.make_pkg('my_pkg'), self.site_dir) + assert version('my-pkg') == '1.0' + + def test_dist_name_found_as_any_case(self): + """ + Ensure the metadata loads when queried with any case. + """ + pkg_name = 'CherryPy' + fixtures.build_files(self.make_pkg(pkg_name), self.site_dir) + assert version(pkg_name) == '1.0' + assert version(pkg_name.lower()) == '1.0' + assert version(pkg_name.upper()) == '1.0' + + def test_unique_distributions(self): + """ + Two distributions varying only by non-normalized name on + the file system should resolve as the same. + """ + fixtures.build_files(self.make_pkg('abc'), self.site_dir) + before = list(_unique(distributions())) + + alt_site_dir = self.fixtures.enter_context(fixtures.tempdir()) + self.fixtures.enter_context(self.add_sys_path(alt_site_dir)) + fixtures.build_files(self.make_pkg('ABC'), alt_site_dir) + after = list(_unique(distributions())) + + assert len(after) == len(before) + + +class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase): + @staticmethod + def pkg_with_non_ascii_description(site_dir): + """ + Create minimal metadata for a package with non-ASCII in + the description. + """ + contents = { + 'portend.dist-info': { + 'METADATA': 'Description: pôrˈtend', + }, + } + fixtures.build_files(contents, site_dir) + return 'portend' + + @staticmethod + def pkg_with_non_ascii_description_egg_info(site_dir): + """ + Create minimal metadata for an egg-info package with + non-ASCII in the description. + """ + contents = { + 'portend.dist-info': { + 'METADATA': """ + Name: portend + + pôrˈtend""", + }, + } + fixtures.build_files(contents, site_dir) + return 'portend' + + def test_metadata_loads(self): + pkg_name = self.pkg_with_non_ascii_description(self.site_dir) + 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 = metadata(pkg_name) + assert meta['Description'] == 'pôrˈtend' + + +class DiscoveryTests( + fixtures.EggInfoPkg, + fixtures.EggInfoPkgPipInstalledNoToplevel, + fixtures.EggInfoPkgPipInstalledNoModules, + fixtures.EggInfoPkgSourcesFallback, + fixtures.DistInfoPkg, + unittest.TestCase, +): + def test_package_discovery(self): + dists = list(distributions()) + assert all(isinstance(dist, Distribution) for dist in dists) + assert any(dist.metadata['Name'] == 'egginfo-pkg' for dist in dists) + assert any(dist.metadata['Name'] == 'egg_with_module-pkg' for dist in dists) + assert any(dist.metadata['Name'] == 'egg_with_no_modules-pkg' for dist in dists) + assert any(dist.metadata['Name'] == 'sources_fallback-pkg' for dist in dists) + assert any(dist.metadata['Name'] == 'distinfo-pkg' for dist in dists) + + def test_invalid_usage(self): + with self.assertRaises(ValueError): + list(distributions(context='something', name='else')) + + def test_interleaved_discovery(self): + """ + Ensure interleaved searches are safe. + + When the search is cached, it is possible for searches to be + interleaved, so make sure those use-cases are safe. + + Ref #293 + """ + dists = distributions() + next(dists) + version('egginfo-pkg') + next(dists) + + +class DirectoryTest(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase): + def test_egg_info(self): + # make an `EGG-INFO` directory that's unrelated + self.site_dir.joinpath('EGG-INFO').mkdir() + # used to crash with `IsADirectoryError` + with self.assertRaises(PackageNotFoundError): + version('unknown-package') + + def test_egg(self): + egg = self.site_dir.joinpath('foo-3.6.egg') + egg.mkdir() + with self.add_sys_path(egg): + with self.assertRaises(PackageNotFoundError): + version('foo') + + +class MissingSysPath(fixtures.OnSysPath, unittest.TestCase): + site_dir = '/does-not-exist' + + def test_discovery(self): + """ + Discovering distributions should succeed even if + there is an invalid path on sys.path. + """ + importlib.metadata.distributions() + + +class InaccessibleSysPath(fixtures.OnSysPath, ffs.TestCase): + site_dir = '/access-denied' + + def setUp(self): + super().setUp() + self.setUpPyfakefs() + self.fs.create_dir(self.site_dir, perm_bits=000) + + def test_discovery(self): + """ + Discovering distributions should succeed even if + there is an invalid path on sys.path. + """ + list(importlib.metadata.distributions()) + + +class TestEntryPoints(unittest.TestCase): + def __init__(self, *args): + super().__init__(*args) + self.ep = importlib.metadata.EntryPoint( + name='name', value='value', group='group' + ) + + def test_entry_point_pickleable(self): + revived = pickle.loads(pickle.dumps(self.ep)) + assert revived == self.ep + + def test_positional_args(self): + """ + Capture legacy (namedtuple) construction, discouraged. + """ + EntryPoint('name', 'value', 'group') + + def test_immutable(self): + """EntryPoints should be immutable""" + with self.assertRaises(AttributeError): + self.ep.name = 'badactor' + + def test_repr(self): + assert 'EntryPoint' in repr(self.ep) + assert 'name=' in repr(self.ep) + assert "'name'" in repr(self.ep) + + def test_hashable(self): + """EntryPoints should be hashable""" + hash(self.ep) + + def test_module(self): + assert self.ep.module == 'value' + + def test_attr(self): + assert self.ep.attr is None + + def test_sortable(self): + """ + EntryPoint objects are sortable, but result is undefined. + """ + sorted( + [ + EntryPoint(name='b', value='val', group='group'), + EntryPoint(name='a', value='val', group='group'), + ] + ) + + +class FileSystem( + fixtures.OnSysPath, fixtures.SiteDir, fixtures.FileBuilder, unittest.TestCase +): + def test_unicode_dir_on_sys_path(self): + """ + Ensure a Unicode subdirectory of a directory on sys.path + does not crash. + """ + fixtures.build_files( + {self.unicode_filename(): {}}, + prefix=self.site_dir, + ) + list(distributions()) + + +class PackagesDistributionsPrebuiltTest(fixtures.ZipFixtures, unittest.TestCase): + def test_packages_distributions_example(self): + self._fixture_on_path('example-21.12-py3-none-any.whl') + assert packages_distributions()['example'] == ['example'] + + def test_packages_distributions_example2(self): + """ + Test packages_distributions on a wheel built + by trampolim. + """ + self._fixture_on_path('example2-1.0.0-py3-none-any.whl') + assert packages_distributions()['example2'] == ['example2'] + + +class PackagesDistributionsTest( + fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase +): + def test_packages_distributions_neither_toplevel_nor_files(self): + """ + Test a package built without 'top-level.txt' or a file list. + """ + fixtures.build_files( + { + 'trim_example-1.0.0.dist-info': { + 'METADATA': """ + Name: trim_example + Version: 1.0.0 + """, + } + }, + prefix=self.site_dir, + ) + packages_distributions() + + def test_packages_distributions_all_module_types(self): + """ + Test top-level modules detected on a package without 'top-level.txt'. + """ + suffixes = importlib.machinery.all_suffixes() + metadata = dict( + METADATA=""" + Name: all_distributions + Version: 1.0.0 + """, + ) + files = { + 'all_distributions-1.0.0.dist-info': metadata, + } + for i, suffix in enumerate(suffixes): + files.update( + { + f'importable-name {i}{suffix}': '', + f'in_namespace_{i}': { + f'mod{suffix}': '', + }, + f'in_package_{i}': { + '__init__.py': '', + f'mod{suffix}': '', + }, + } + ) + metadata.update(RECORD=fixtures.build_record(files)) + fixtures.build_files(files, prefix=self.site_dir) + + distributions = packages_distributions() + + for i in range(len(suffixes)): + assert distributions[f'importable-name {i}'] == ['all_distributions'] + assert distributions[f'in_namespace_{i}'] == ['all_distributions'] + assert distributions[f'in_package_{i}'] == ['all_distributions'] + + assert not any(name.endswith('.dist-info') for name in distributions) + + @os_helper.skip_unless_symlink + def test_packages_distributions_symlinked_top_level(self) -> None: + """ + Distribution is resolvable from a simple top-level symlink in RECORD. + See #452. + """ + + files: fixtures.FilesSpec = { + "symlinked_pkg-1.0.0.dist-info": { + "METADATA": """ + Name: symlinked-pkg + Version: 1.0.0 + """, + "RECORD": "symlinked,,\n", + }, + ".symlink.target": {}, + "symlinked": Symlink(".symlink.target"), + } + + fixtures.build_files(files, self.site_dir) + assert packages_distributions()['symlinked'] == ['symlinked-pkg'] + + +class PackagesDistributionsEggTest( + fixtures.EggInfoPkg, + fixtures.EggInfoPkgPipInstalledNoToplevel, + fixtures.EggInfoPkgPipInstalledNoModules, + fixtures.EggInfoPkgSourcesFallback, + unittest.TestCase, +): + def test_packages_distributions_on_eggs(self): + """ + Test old-style egg packages with a variation of 'top_level.txt', + 'SOURCES.txt', and 'installed-files.txt', available. + """ + distributions = packages_distributions() + + def import_names_from_package(package_name): + return { + import_name + for import_name, package_names in distributions.items() + if package_name in package_names + } + + # egginfo-pkg declares one import ('mod') via top_level.txt + assert import_names_from_package('egginfo-pkg') == {'mod'} + + # egg_with_module-pkg has one import ('egg_with_module') inferred from + # installed-files.txt (top_level.txt is missing) + assert import_names_from_package('egg_with_module-pkg') == {'egg_with_module'} + + # egg_with_no_modules-pkg should not be associated with any import names + # (top_level.txt is empty, and installed-files.txt has no .py files) + assert import_names_from_package('egg_with_no_modules-pkg') == set() + + # sources_fallback-pkg has one import ('sources_fallback') inferred from + # SOURCES.txt (top_level.txt and installed-files.txt is missing) + assert import_names_from_package('sources_fallback-pkg') == {'sources_fallback'} + + +class EditableDistributionTest(fixtures.DistInfoPkgEditable, unittest.TestCase): + def test_origin(self): + dist = Distribution.from_name('distinfo-pkg') + assert dist.origin.url.endswith('.whl') + assert dist.origin.archive_info.hashes.sha256 diff --git a/Lib/test/test_importlib/metadata/test_zip.py b/Lib/test/test_importlib/metadata/test_zip.py new file mode 100644 index 0000000..276f628 --- /dev/null +++ b/Lib/test/test_importlib/metadata/test_zip.py @@ -0,0 +1,62 @@ +import sys +import unittest + +from . import fixtures +from importlib.metadata import ( + PackageNotFoundError, + distribution, + distributions, + entry_points, + files, + version, +) + + +class TestZip(fixtures.ZipFixtures, unittest.TestCase): + def setUp(self): + super().setUp() + self._fixture_on_path('example-21.12-py3-none-any.whl') + + def test_zip_version(self): + self.assertEqual(version('example'), '21.12') + + def test_zip_version_does_not_match(self): + with self.assertRaises(PackageNotFoundError): + version('definitely-not-installed') + + def test_zip_entry_points(self): + scripts = entry_points(group='console_scripts') + entry_point = scripts['example'] + self.assertEqual(entry_point.value, 'example:main') + entry_point = scripts['Example'] + self.assertEqual(entry_point.value, 'example:main') + + def test_missing_metadata(self): + self.assertIsNone(distribution('example').read_text('does not exist')) + + def test_case_insensitive(self): + self.assertEqual(version('Example'), '21.12') + + def test_files(self): + for file in files('example'): + path = str(file.dist.locate_file(file)) + assert '.whl/' in path, path + + def test_one_distribution(self): + dists = list(distributions(path=sys.path[:1])) + assert len(dists) == 1 + + +class TestEgg(TestZip): + def setUp(self): + super().setUp() + self._fixture_on_path('example-21.12-py3.6.egg') + + def test_files(self): + for file in files('example'): + path = str(file.dist.locate_file(file)) + assert '.egg/' in path, path + + def test_normalized_name(self): + dist = distribution('example') + assert dist._normalized_name == 'example' diff --git a/Lib/test/test_importlib/stubs.py b/Lib/test/test_importlib/stubs.py deleted file mode 100644 index e5b011c..0000000 --- a/Lib/test/test_importlib/stubs.py +++ /dev/null @@ -1,10 +0,0 @@ -import unittest - - -class fake_filesystem_unittest: - """ - Stubbed version of the pyfakefs module - """ - class TestCase(unittest.TestCase): - def setUpPyfakefs(self): - self.skipTest("pyfakefs not available") diff --git a/Lib/test/test_importlib/test_main.py b/Lib/test/test_importlib/test_main.py deleted file mode 100644 index 0a769b8..0000000 --- a/Lib/test/test_importlib/test_main.py +++ /dev/null @@ -1,471 +0,0 @@ -import re -import pickle -import unittest -import warnings -import importlib.metadata -import contextlib -from test.support import os_helper - -try: - import pyfakefs.fake_filesystem_unittest as ffs -except ImportError: - from .stubs import fake_filesystem_unittest as ffs - -from . import fixtures -from ._context import suppress -from ._path import Symlink -from importlib.metadata import ( - Distribution, - EntryPoint, - PackageNotFoundError, - _unique, - distributions, - entry_points, - metadata, - packages_distributions, - version, -) - - -@contextlib.contextmanager -def suppress_known_deprecation(): - with warnings.catch_warnings(record=True) as ctx: - warnings.simplefilter('default', category=DeprecationWarning) - yield ctx - - -class BasicTests(fixtures.DistInfoPkg, unittest.TestCase): - version_pattern = r'\d+\.\d+(\.\d)?' - - def test_retrieves_version_of_self(self): - 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(PackageNotFoundError): - Distribution.from_name('does-not-exist') - - def test_package_not_found_mentions_metadata(self): - """ - When a package is not found, that could indicate that the - package is not installed or that it is installed without - metadata. Ensure the exception mentions metadata to help - guide users toward the cause. See #124. - """ - with self.assertRaises(PackageNotFoundError) as ctx: - Distribution.from_name('does-not-exist') - - assert "metadata" in str(ctx.exception) - - # expected to fail until ABC is enforced - @suppress(AssertionError) - @suppress_known_deprecation() - def test_abc_enforced(self): - with self.assertRaises(TypeError): - type('DistributionSubclass', (Distribution,), {})() - - @fixtures.parameterize( - dict(name=None), - dict(name=''), - ) - def test_invalid_inputs_to_from_name(self, name): - with self.assertRaises(Exception): - Distribution.from_name(name) - - -class ImportTests(fixtures.DistInfoPkg, unittest.TestCase): - def test_import_nonexistent_module(self): - # Ensure that the MetadataPathFinder does not crash an import of a - # non-existent module. - with self.assertRaises(ImportError): - importlib.import_module('does_not_exist') - - def test_resolve(self): - ep = entry_points(group='entries')['main'] - self.assertEqual(ep.load().__name__, "main") - - def test_entrypoint_with_colon_in_name(self): - ep = entry_points(group='entries')['ns:sub'] - self.assertEqual(ep.value, 'mod:main') - - def test_resolve_without_attr(self): - ep = EntryPoint( - name='ep', - value='importlib.metadata', - group='grp', - ) - assert ep.load() is importlib.metadata - - -class NameNormalizationTests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase): - @staticmethod - def make_pkg(name): - """ - Create minimal metadata for a dist-info package with - the indicated name on the file system. - """ - return { - f'{name}.dist-info': { - 'METADATA': 'VERSION: 1.0\n', - }, - } - - def test_dashes_in_dist_name_found_as_underscores(self): - """ - For a package with a dash in the name, the dist-info metadata - uses underscores in the name. Ensure the metadata loads. - """ - fixtures.build_files(self.make_pkg('my_pkg'), self.site_dir) - assert version('my-pkg') == '1.0' - - def test_dist_name_found_as_any_case(self): - """ - Ensure the metadata loads when queried with any case. - """ - pkg_name = 'CherryPy' - fixtures.build_files(self.make_pkg(pkg_name), self.site_dir) - assert version(pkg_name) == '1.0' - assert version(pkg_name.lower()) == '1.0' - assert version(pkg_name.upper()) == '1.0' - - def test_unique_distributions(self): - """ - Two distributions varying only by non-normalized name on - the file system should resolve as the same. - """ - fixtures.build_files(self.make_pkg('abc'), self.site_dir) - before = list(_unique(distributions())) - - alt_site_dir = self.fixtures.enter_context(fixtures.tempdir()) - self.fixtures.enter_context(self.add_sys_path(alt_site_dir)) - fixtures.build_files(self.make_pkg('ABC'), alt_site_dir) - after = list(_unique(distributions())) - - assert len(after) == len(before) - - -class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase): - @staticmethod - def pkg_with_non_ascii_description(site_dir): - """ - Create minimal metadata for a package with non-ASCII in - the description. - """ - contents = { - 'portend.dist-info': { - 'METADATA': 'Description: pôrˈtend', - }, - } - fixtures.build_files(contents, site_dir) - return 'portend' - - @staticmethod - def pkg_with_non_ascii_description_egg_info(site_dir): - """ - Create minimal metadata for an egg-info package with - non-ASCII in the description. - """ - contents = { - 'portend.dist-info': { - 'METADATA': """ - Name: portend - - pôrˈtend""", - }, - } - fixtures.build_files(contents, site_dir) - return 'portend' - - def test_metadata_loads(self): - pkg_name = self.pkg_with_non_ascii_description(self.site_dir) - 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 = metadata(pkg_name) - assert meta['Description'] == 'pôrˈtend' - - -class DiscoveryTests( - fixtures.EggInfoPkg, - fixtures.EggInfoPkgPipInstalledNoToplevel, - fixtures.EggInfoPkgPipInstalledNoModules, - fixtures.EggInfoPkgSourcesFallback, - fixtures.DistInfoPkg, - unittest.TestCase, -): - def test_package_discovery(self): - dists = list(distributions()) - assert all(isinstance(dist, Distribution) for dist in dists) - assert any(dist.metadata['Name'] == 'egginfo-pkg' for dist in dists) - assert any(dist.metadata['Name'] == 'egg_with_module-pkg' for dist in dists) - assert any(dist.metadata['Name'] == 'egg_with_no_modules-pkg' for dist in dists) - assert any(dist.metadata['Name'] == 'sources_fallback-pkg' for dist in dists) - assert any(dist.metadata['Name'] == 'distinfo-pkg' for dist in dists) - - def test_invalid_usage(self): - with self.assertRaises(ValueError): - list(distributions(context='something', name='else')) - - def test_interleaved_discovery(self): - """ - Ensure interleaved searches are safe. - - When the search is cached, it is possible for searches to be - interleaved, so make sure those use-cases are safe. - - Ref #293 - """ - dists = distributions() - next(dists) - version('egginfo-pkg') - next(dists) - - -class DirectoryTest(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase): - def test_egg_info(self): - # make an `EGG-INFO` directory that's unrelated - self.site_dir.joinpath('EGG-INFO').mkdir() - # used to crash with `IsADirectoryError` - with self.assertRaises(PackageNotFoundError): - version('unknown-package') - - def test_egg(self): - egg = self.site_dir.joinpath('foo-3.6.egg') - egg.mkdir() - with self.add_sys_path(egg): - with self.assertRaises(PackageNotFoundError): - version('foo') - - -class MissingSysPath(fixtures.OnSysPath, unittest.TestCase): - site_dir = '/does-not-exist' - - def test_discovery(self): - """ - Discovering distributions should succeed even if - there is an invalid path on sys.path. - """ - importlib.metadata.distributions() - - -class InaccessibleSysPath(fixtures.OnSysPath, ffs.TestCase): - site_dir = '/access-denied' - - def setUp(self): - super().setUp() - self.setUpPyfakefs() - self.fs.create_dir(self.site_dir, perm_bits=000) - - def test_discovery(self): - """ - Discovering distributions should succeed even if - there is an invalid path on sys.path. - """ - list(importlib.metadata.distributions()) - - -class TestEntryPoints(unittest.TestCase): - def __init__(self, *args): - super().__init__(*args) - self.ep = importlib.metadata.EntryPoint( - name='name', value='value', group='group' - ) - - def test_entry_point_pickleable(self): - revived = pickle.loads(pickle.dumps(self.ep)) - assert revived == self.ep - - def test_positional_args(self): - """ - Capture legacy (namedtuple) construction, discouraged. - """ - EntryPoint('name', 'value', 'group') - - def test_immutable(self): - """EntryPoints should be immutable""" - with self.assertRaises(AttributeError): - self.ep.name = 'badactor' - - def test_repr(self): - assert 'EntryPoint' in repr(self.ep) - assert 'name=' in repr(self.ep) - assert "'name'" in repr(self.ep) - - def test_hashable(self): - """EntryPoints should be hashable""" - hash(self.ep) - - def test_module(self): - assert self.ep.module == 'value' - - def test_attr(self): - assert self.ep.attr is None - - def test_sortable(self): - """ - EntryPoint objects are sortable, but result is undefined. - """ - sorted( - [ - EntryPoint(name='b', value='val', group='group'), - EntryPoint(name='a', value='val', group='group'), - ] - ) - - -class FileSystem( - fixtures.OnSysPath, fixtures.SiteDir, fixtures.FileBuilder, unittest.TestCase -): - def test_unicode_dir_on_sys_path(self): - """ - Ensure a Unicode subdirectory of a directory on sys.path - does not crash. - """ - fixtures.build_files( - {self.unicode_filename(): {}}, - prefix=self.site_dir, - ) - list(distributions()) - - -class PackagesDistributionsPrebuiltTest(fixtures.ZipFixtures, unittest.TestCase): - def test_packages_distributions_example(self): - self._fixture_on_path('example-21.12-py3-none-any.whl') - assert packages_distributions()['example'] == ['example'] - - def test_packages_distributions_example2(self): - """ - Test packages_distributions on a wheel built - by trampolim. - """ - self._fixture_on_path('example2-1.0.0-py3-none-any.whl') - assert packages_distributions()['example2'] == ['example2'] - - -class PackagesDistributionsTest( - fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase -): - def test_packages_distributions_neither_toplevel_nor_files(self): - """ - Test a package built without 'top-level.txt' or a file list. - """ - fixtures.build_files( - { - 'trim_example-1.0.0.dist-info': { - 'METADATA': """ - Name: trim_example - Version: 1.0.0 - """, - } - }, - prefix=self.site_dir, - ) - packages_distributions() - - def test_packages_distributions_all_module_types(self): - """ - Test top-level modules detected on a package without 'top-level.txt'. - """ - suffixes = importlib.machinery.all_suffixes() - metadata = dict( - METADATA=""" - Name: all_distributions - Version: 1.0.0 - """, - ) - files = { - 'all_distributions-1.0.0.dist-info': metadata, - } - for i, suffix in enumerate(suffixes): - files.update( - { - f'importable-name {i}{suffix}': '', - f'in_namespace_{i}': { - f'mod{suffix}': '', - }, - f'in_package_{i}': { - '__init__.py': '', - f'mod{suffix}': '', - }, - } - ) - metadata.update(RECORD=fixtures.build_record(files)) - fixtures.build_files(files, prefix=self.site_dir) - - distributions = packages_distributions() - - for i in range(len(suffixes)): - assert distributions[f'importable-name {i}'] == ['all_distributions'] - assert distributions[f'in_namespace_{i}'] == ['all_distributions'] - assert distributions[f'in_package_{i}'] == ['all_distributions'] - - assert not any(name.endswith('.dist-info') for name in distributions) - - @os_helper.skip_unless_symlink - def test_packages_distributions_symlinked_top_level(self) -> None: - """ - Distribution is resolvable from a simple top-level symlink in RECORD. - See #452. - """ - - files: fixtures.FilesSpec = { - "symlinked_pkg-1.0.0.dist-info": { - "METADATA": """ - Name: symlinked-pkg - Version: 1.0.0 - """, - "RECORD": "symlinked,,\n", - }, - ".symlink.target": {}, - "symlinked": Symlink(".symlink.target"), - } - - fixtures.build_files(files, self.site_dir) - assert packages_distributions()['symlinked'] == ['symlinked-pkg'] - - -class PackagesDistributionsEggTest( - fixtures.EggInfoPkg, - fixtures.EggInfoPkgPipInstalledNoToplevel, - fixtures.EggInfoPkgPipInstalledNoModules, - fixtures.EggInfoPkgSourcesFallback, - unittest.TestCase, -): - def test_packages_distributions_on_eggs(self): - """ - Test old-style egg packages with a variation of 'top_level.txt', - 'SOURCES.txt', and 'installed-files.txt', available. - """ - distributions = packages_distributions() - - def import_names_from_package(package_name): - return { - import_name - for import_name, package_names in distributions.items() - if package_name in package_names - } - - # egginfo-pkg declares one import ('mod') via top_level.txt - assert import_names_from_package('egginfo-pkg') == {'mod'} - - # egg_with_module-pkg has one import ('egg_with_module') inferred from - # installed-files.txt (top_level.txt is missing) - assert import_names_from_package('egg_with_module-pkg') == {'egg_with_module'} - - # egg_with_no_modules-pkg should not be associated with any import names - # (top_level.txt is empty, and installed-files.txt has no .py files) - assert import_names_from_package('egg_with_no_modules-pkg') == set() - - # sources_fallback-pkg has one import ('sources_fallback') inferred from - # SOURCES.txt (top_level.txt and installed-files.txt is missing) - assert import_names_from_package('sources_fallback-pkg') == {'sources_fallback'} - - -class EditableDistributionTest(fixtures.DistInfoPkgEditable, unittest.TestCase): - def test_origin(self): - dist = Distribution.from_name('distinfo-pkg') - assert dist.origin.url.endswith('.whl') - assert dist.origin.archive_info.hashes.sha256 diff --git a/Lib/test/test_importlib/test_metadata_api.py b/Lib/test/test_importlib/test_metadata_api.py deleted file mode 100644 index 33c6e85..0000000 --- a/Lib/test/test_importlib/test_metadata_api.py +++ /dev/null @@ -1,322 +0,0 @@ -import re -import textwrap -import unittest -import warnings -import importlib -import contextlib - -from . import fixtures -from importlib.metadata import ( - Distribution, - PackageNotFoundError, - distribution, - entry_points, - files, - metadata, - requires, - version, -) - - -@contextlib.contextmanager -def suppress_known_deprecation(): - with warnings.catch_warnings(record=True) as ctx: - warnings.simplefilter('default', category=DeprecationWarning) - yield ctx - - -class APITests( - fixtures.EggInfoPkg, - fixtures.EggInfoPkgPipInstalledNoToplevel, - fixtures.EggInfoPkgPipInstalledNoModules, - fixtures.EggInfoPkgSourcesFallback, - fixtures.DistInfoPkg, - fixtures.DistInfoPkgWithDot, - fixtures.EggInfoFile, - unittest.TestCase, -): - version_pattern = r'\d+\.\d+(\.\d)?' - - def test_retrieves_version_of_self(self): - pkg_version = version('egginfo-pkg') - assert isinstance(pkg_version, str) - assert re.match(self.version_pattern, pkg_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(PackageNotFoundError): - distribution('does-not-exist') - - def test_name_normalization(self): - names = 'pkg.dot', 'pkg_dot', 'pkg-dot', 'pkg..dot', 'Pkg.Dot' - for name in names: - with self.subTest(name): - assert distribution(name).metadata['Name'] == 'pkg.dot' - - def test_prefix_not_matched(self): - prefixes = 'p', 'pkg', 'pkg.' - for prefix in prefixes: - with self.subTest(prefix): - with self.assertRaises(PackageNotFoundError): - distribution(prefix) - - def test_for_top_level(self): - tests = [ - ('egginfo-pkg', 'mod'), - ('egg_with_no_modules-pkg', ''), - ] - for pkg_name, expect_content in tests: - with self.subTest(pkg_name): - self.assertEqual( - distribution(pkg_name).read_text('top_level.txt').strip(), - expect_content, - ) - - def test_read_text(self): - tests = [ - ('egginfo-pkg', 'mod\n'), - ('egg_with_no_modules-pkg', '\n'), - ] - for pkg_name, expect_content in tests: - with self.subTest(pkg_name): - top_level = [ - path for path in files(pkg_name) if path.name == 'top_level.txt' - ][0] - self.assertEqual(top_level.read_text(), expect_content) - - def test_entry_points(self): - eps = entry_points() - assert 'entries' in eps.groups - entries = eps.select(group='entries') - assert 'main' in entries.names - ep = entries['main'] - self.assertEqual(ep.value, 'mod:main') - self.assertEqual(ep.extras, []) - - def test_entry_points_distribution(self): - entries = entry_points(group='entries') - for entry in ("main", "ns:sub"): - ep = entries[entry] - self.assertIn(ep.dist.name, ('distinfo-pkg', 'egginfo-pkg')) - self.assertEqual(ep.dist.version, "1.0.0") - - def test_entry_points_unique_packages_normalized(self): - """ - Entry points should only be exposed for the first package - on sys.path with a given name (even when normalized). - """ - alt_site_dir = self.fixtures.enter_context(fixtures.tempdir()) - self.fixtures.enter_context(self.add_sys_path(alt_site_dir)) - alt_pkg = { - "DistInfo_pkg-1.1.0.dist-info": { - "METADATA": """ - Name: distinfo-pkg - Version: 1.1.0 - """, - "entry_points.txt": """ - [entries] - main = mod:altmain - """, - }, - } - fixtures.build_files(alt_pkg, alt_site_dir) - entries = entry_points(group='entries') - assert not any( - ep.dist.name == 'distinfo-pkg' and ep.dist.version == '1.0.0' - for ep in entries - ) - # ns:sub doesn't exist in alt_pkg - assert 'ns:sub' not in entries.names - - def test_entry_points_missing_name(self): - with self.assertRaises(KeyError): - entry_points(group='entries')['missing'] - - def test_entry_points_missing_group(self): - assert entry_points(group='missing') == () - - def test_entry_points_allows_no_attributes(self): - ep = entry_points().select(group='entries', name='main') - with self.assertRaises(AttributeError): - ep.foo = 4 - - def test_metadata_for_this_package(self): - md = metadata('egginfo-pkg') - assert md['author'] == 'Steven Ma' - assert md['LICENSE'] == 'Unknown' - assert md['Name'] == 'egginfo-pkg' - classifiers = md.get_all('Classifier') - assert 'Topic :: Software Development :: Libraries' in classifiers - - def test_missing_key_legacy(self): - """ - Requesting a missing key will still return None, but warn. - """ - md = metadata('distinfo-pkg') - with suppress_known_deprecation(): - assert md['does-not-exist'] is None - - def test_get_key(self): - """ - Getting a key gets the key. - """ - md = metadata('egginfo-pkg') - assert md.get('Name') == 'egginfo-pkg' - - def test_get_missing_key(self): - """ - Requesting a missing key will return None. - """ - md = metadata('distinfo-pkg') - assert md.get('does-not-exist') is None - - @staticmethod - def _test_files(files): - root = files[0].root - for file in files: - assert file.root == root - assert not file.hash or file.hash.value - assert not file.hash or file.hash.mode == 'sha256' - assert not file.size or file.size >= 0 - assert file.locate().exists() - assert isinstance(file.read_binary(), bytes) - if file.name.endswith('.py'): - file.read_text() - - def test_file_hash_repr(self): - util = [p for p in files('distinfo-pkg') if p.name == 'mod.py'][0] - self.assertRegex(repr(util.hash), '') - - def test_files_dist_info(self): - self._test_files(files('distinfo-pkg')) - - def test_files_egg_info(self): - self._test_files(files('egginfo-pkg')) - self._test_files(files('egg_with_module-pkg')) - self._test_files(files('egg_with_no_modules-pkg')) - self._test_files(files('sources_fallback-pkg')) - - def test_version_egg_info_file(self): - self.assertEqual(version('egginfo-file'), '0.1') - - def test_requires_egg_info_file(self): - requirements = requires('egginfo-file') - self.assertIsNone(requirements) - - 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_egg_info_empty(self): - fixtures.build_files( - { - 'requires.txt': '', - }, - self.site_dir.joinpath('egginfo_pkg.egg-info'), - ) - deps = requires('egginfo-pkg') - assert deps == [] - - def test_requires_dist_info(self): - deps = requires('distinfo-pkg') - assert len(deps) == 2 - assert all(deps) - assert 'wheel >= 1.0' in deps - assert "pytest; extra == 'test'" in deps - - def test_more_complex_deps_requires_text(self): - requires = textwrap.dedent( - """ - dep1 - dep2 - - [:python_version < "3"] - dep3 - - [extra1] - dep4 - dep6@ git+https://example.com/python/dep.git@v1.0.0 - - [extra2:python_version < "3"] - dep5 - """ - ) - deps = sorted(Distribution._deps_from_requires_text(requires)) - expected = [ - 'dep1', - 'dep2', - 'dep3; python_version < "3"', - 'dep4; extra == "extra1"', - 'dep5; (python_version < "3") and extra == "extra2"', - 'dep6@ git+https://example.com/python/dep.git@v1.0.0 ; extra == "extra1"', - ] - # It's important that the environment marker expression be - # wrapped in parentheses to avoid the following 'and' binding more - # tightly than some other part of the environment expression. - - assert deps == expected - - def test_as_json(self): - md = metadata('distinfo-pkg').json - assert 'name' in md - assert md['keywords'] == ['sample', 'package'] - desc = md['description'] - assert desc.startswith('Once upon a time\nThere was') - assert len(md['requires_dist']) == 2 - - def test_as_json_egg_info(self): - md = metadata('egginfo-pkg').json - assert 'name' in md - assert md['keywords'] == ['sample', 'package'] - desc = md['description'] - assert desc.startswith('Once upon a time\nThere was') - assert len(md['classifier']) == 2 - - def test_as_json_odd_case(self): - self.make_uppercase() - md = metadata('distinfo-pkg').json - assert 'name' in md - assert len(md['requires_dist']) == 2 - assert md['keywords'] == ['SAMPLE', 'PACKAGE'] - - -class LegacyDots(fixtures.DistInfoPkgWithDotLegacy, unittest.TestCase): - def test_name_normalization(self): - names = 'pkg.dot', 'pkg_dot', 'pkg-dot', 'pkg..dot', 'Pkg.Dot' - for name in names: - with self.subTest(name): - assert distribution(name).metadata['Name'] == 'pkg.dot' - - def test_name_normalization_versionless_egg_info(self): - names = 'pkg.lot', 'pkg_lot', 'pkg-lot', 'pkg..lot', 'Pkg.Lot' - for name in names: - with self.subTest(name): - assert distribution(name).metadata['Name'] == 'pkg.lot' - - -class OffSysPathTests(fixtures.DistInfoPkgOffPath, unittest.TestCase): - def test_find_distributions_specified_path(self): - 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' - - -class InvalidateCache(unittest.TestCase): - def test_invalidate_cache(self): - # No externally observable behavior, but ensures test coverage... - importlib.invalidate_caches() diff --git a/Lib/test/test_importlib/test_zip.py b/Lib/test/test_importlib/test_zip.py deleted file mode 100644 index 276f628..0000000 --- a/Lib/test/test_importlib/test_zip.py +++ /dev/null @@ -1,62 +0,0 @@ -import sys -import unittest - -from . import fixtures -from importlib.metadata import ( - PackageNotFoundError, - distribution, - distributions, - entry_points, - files, - version, -) - - -class TestZip(fixtures.ZipFixtures, unittest.TestCase): - def setUp(self): - super().setUp() - self._fixture_on_path('example-21.12-py3-none-any.whl') - - def test_zip_version(self): - self.assertEqual(version('example'), '21.12') - - def test_zip_version_does_not_match(self): - with self.assertRaises(PackageNotFoundError): - version('definitely-not-installed') - - def test_zip_entry_points(self): - scripts = entry_points(group='console_scripts') - entry_point = scripts['example'] - self.assertEqual(entry_point.value, 'example:main') - entry_point = scripts['Example'] - self.assertEqual(entry_point.value, 'example:main') - - def test_missing_metadata(self): - self.assertIsNone(distribution('example').read_text('does not exist')) - - def test_case_insensitive(self): - self.assertEqual(version('Example'), '21.12') - - def test_files(self): - for file in files('example'): - path = str(file.dist.locate_file(file)) - assert '.whl/' in path, path - - def test_one_distribution(self): - dists = list(distributions(path=sys.path[:1])) - assert len(dists) == 1 - - -class TestEgg(TestZip): - def setUp(self): - super().setUp() - self._fixture_on_path('example-21.12-py3.6.egg') - - def test_files(self): - for file in files('example'): - path = str(file.dist.locate_file(file)) - assert '.egg/' in path, path - - def test_normalized_name(self): - dist = distribution('example') - assert dist._normalized_name == 'example' diff --git a/Makefile.pre.in b/Makefile.pre.in index b9f790a..1451cf3 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -2351,10 +2351,11 @@ TESTSUBDIRS= idlelib/idle_test \ test/test_import/data/unwritable \ test/test_importlib \ test/test_importlib/builtin \ - test/test_importlib/data \ test/test_importlib/extension \ test/test_importlib/frozen \ test/test_importlib/import_ \ + test/test_importlib/metadata \ + test/test_importlib/metadata/data \ test/test_importlib/namespace_pkgs \ test/test_importlib/namespace_pkgs/both_portions \ test/test_importlib/namespace_pkgs/both_portions/foo \ diff --git a/Misc/NEWS.d/next/Tests/2024-03-20-14-19-32.gh-issue-117089.WwR1Z1.rst b/Misc/NEWS.d/next/Tests/2024-03-20-14-19-32.gh-issue-117089.WwR1Z1.rst new file mode 100644 index 0000000..ab0baec --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2024-03-20-14-19-32.gh-issue-117089.WwR1Z1.rst @@ -0,0 +1 @@ +Consolidated tests for importlib.metadata in their own ``metadata`` package. -- cgit v0.12