| 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 |