mirror of https://github.com/python/cpython
396 lines
12 KiB
Python
396 lines
12 KiB
Python
import sys
|
|
import copy
|
|
import json
|
|
import shutil
|
|
import pathlib
|
|
import textwrap
|
|
import functools
|
|
import contextlib
|
|
|
|
from test.support import import_helper
|
|
from test.support import os_helper
|
|
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 tmp_path():
|
|
"""
|
|
Like os_helper.temp_dir, but yields a pathlib.Path.
|
|
"""
|
|
with os_helper.temp_dir() as path:
|
|
yield pathlib.Path(path)
|
|
|
|
|
|
@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(tmp_path())
|
|
|
|
|
|
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 EggInfoPkgPipInstalledExternalDataFiles(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.json
|
|
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": """
|
|
../../../etc/jupyter/jupyter_notebook_config.d/relative.json
|
|
/etc/jupyter/jupyter_notebook_config.d/absolute.json
|
|
../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 os_helper.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
|