cpython/Lib/test/test_importlib/metadata/fixtures.py

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: [email protected]
            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