diff --git a/docs/changelog/1774.feature.rst b/docs/changelog/1774.feature.rst new file mode 100644 index 000000000..1761a46cd --- /dev/null +++ b/docs/changelog/1774.feature.rst @@ -0,0 +1,2 @@ +Support for Windows embeddable Python package: includes ``python.zip`` +in the creator sources. diff --git a/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py b/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py index fcd92b82f..3bbc494a6 100644 --- a/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py +++ b/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py @@ -1,6 +1,9 @@ from __future__ import absolute_import, unicode_literals import abc +import fnmatch +from itertools import chain +from operator import methodcaller as method from textwrap import dedent from six import add_metaclass @@ -53,11 +56,20 @@ def setup_meta(cls, interpreter): @classmethod def sources(cls, interpreter): - for src in super(CPython3Windows, cls).sources(interpreter): - yield src - if not cls.has_shim(interpreter): - for src in cls.include_dll_and_pyd(interpreter): - yield src + if cls.has_shim(interpreter): + refs = cls.executables(interpreter) + else: + refs = chain( + cls.executables(interpreter), + cls.dll_and_pyd(interpreter), + cls.python_zip(interpreter), + ) + for ref in refs: + yield ref + + @classmethod + def executables(cls, interpreter): + return super(CPython3Windows, cls).sources(interpreter) @classmethod def has_shim(cls, interpreter): @@ -79,13 +91,32 @@ def host_python(cls, interpreter): return super(CPython3Windows, cls).host_python(interpreter) @classmethod - def include_dll_and_pyd(cls, interpreter): + def dll_and_pyd(cls, interpreter): dll_folder = Path(interpreter.system_prefix) / "DLLs" host_exe_folder = Path(interpreter.system_executable).parent for folder in [host_exe_folder, dll_folder]: for file in folder.iterdir(): if file.suffix in (".pyd", ".dll"): - yield PathRefToDest(file, dest=cls.to_dll_and_pyd) + yield PathRefToDest(file, cls.to_bin) - def to_dll_and_pyd(self, src): - return self.bin_dir / src.name + @classmethod + def python_zip(cls, interpreter): + """ + "python{VERSION}.zip" contains compiled *.pyc std lib packages, where + "VERSION" is `py_version_nodot` var from the `sysconfig` module. + :see: https://docs.python.org/3/using/windows.html#the-embeddable-package + :see: `discovery.py_info.PythonInfo` class (interpreter). + :see: `python -m sysconfig` output. + + :note: The embeddable Python distribution for Windows includes + "python{VERSION}.zip" and "python{VERSION}._pth" files. User can + move/rename *zip* file and edit `sys.path` by editing *_pth* file. + Here the `pattern` is used only for the default *zip* file name! + """ + pattern = "*python{}.zip".format(interpreter.version_nodot) + matches = fnmatch.filter(interpreter.path, pattern) + matched_paths = map(Path, matches) + existing_paths = filter(method("exists"), matched_paths) + path = next(existing_paths, None) + if path is not None: + yield PathRefToDest(path, cls.to_bin) diff --git a/src/virtualenv/discovery/py_info.py b/src/virtualenv/discovery/py_info.py index df2cc13fc..144a1d1aa 100644 --- a/src/virtualenv/discovery/py_info.py +++ b/src/virtualenv/discovery/py_info.py @@ -47,6 +47,10 @@ def abs_path(v): self.version_info = VersionInfo(*list(u(i) for i in sys.version_info)) self.architecture = 64 if sys.maxsize > 2**32 else 32 + # Used to determine some file names. + # See `CPython3Windows.python_zip()`. + self.version_nodot = sysconfig.get_config_var("py_version_nodot") + self.version = u(sys.version) self.os = u(os.name) diff --git a/src/virtualenv/seed/wheels/embed/setuptools-62.3.2-py3-none-any.whl b/src/virtualenv/seed/wheels/embed/setuptools-62.3.2-py3-none-any.whl new file mode 100644 index 000000000..49453e92d Binary files /dev/null and b/src/virtualenv/seed/wheels/embed/setuptools-62.3.2-py3-none-any.whl differ diff --git a/src/virtualenv/util/path/_pathlib/via_os_path.py b/src/virtualenv/util/path/_pathlib/via_os_path.py index b876f025e..c2b8a0dd0 100644 --- a/src/virtualenv/util/path/_pathlib/via_os_path.py +++ b/src/virtualenv/util/path/_pathlib/via_os_path.py @@ -1,5 +1,6 @@ from __future__ import absolute_import, unicode_literals +import fnmatch import os import platform from contextlib import contextmanager @@ -10,7 +11,7 @@ class Path(object): - def __init__(self, path): + def __init__(self, path=""): if isinstance(path, Path): _path = path._path else: @@ -147,5 +148,13 @@ def chmod(self, mode): def absolute(self): return Path(os.path.abspath(self._path)) + def rglob(self, pattern): + """ + Rough emulation of the origin method. Just for searching fixture files. + """ + for root, _dirs, files in os.walk(self._path): + for filename in fnmatch.filter(files, pattern): + yield Path(os.path.join(root, filename)) + __all__ = ("Path",) diff --git a/tests/conftest.py b/tests/conftest.py index ad8643b9c..c22bace56 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,7 +14,7 @@ from virtualenv.app_data import AppDataDiskFolder from virtualenv.discovery.builtin import get_interpreter from virtualenv.discovery.py_info import PythonInfo -from virtualenv.info import IS_PYPY, IS_WIN, fs_supports_symlink +from virtualenv.info import IS_PYPY, IS_WIN, PY2, fs_supports_symlink from virtualenv.report import LOGGER from virtualenv.util.path import Path from virtualenv.util.six import ensure_str, ensure_text @@ -388,3 +388,11 @@ def skip_if_test_in_system(session_app_data): current = PythonInfo.current(session_app_data) if current.system_executable is not None: pytest.skip("test not valid if run under system") + + +def pytest_ignore_collect(path): + """ + We can't just skip these tests due to syntax errors that occurs during + collecting tests under a Python 2 host. + """ + return PY2 and str(path).endswith("test_cpython3_win.py") diff --git a/tests/unit/create/via_global_ref/builtin/conftest.py b/tests/unit/create/via_global_ref/builtin/conftest.py new file mode 100644 index 000000000..3c1063697 --- /dev/null +++ b/tests/unit/create/via_global_ref/builtin/conftest.py @@ -0,0 +1,25 @@ +import sys + +import pytest +from testing import path +from testing.py_info import read_fixture + +from virtualenv.util.path import Path + +# Allows to import from `testing` into test submodules. +sys.path.append(str(Path(__file__).parent)) + + +@pytest.fixture +def py_info(py_info_name): + return read_fixture(py_info_name) + + +@pytest.fixture +def mock_files(mocker): + return lambda paths, files: path.mock_files(mocker, paths, files) + + +@pytest.fixture +def mock_pypy_libs(mocker): + return lambda pypy, libs: path.mock_pypy_libs(mocker, pypy, libs) diff --git a/tests/unit/create/via_global_ref/builtin/cpython/cpython3_win_embed.json b/tests/unit/create/via_global_ref/builtin/cpython/cpython3_win_embed.json new file mode 100644 index 000000000..e8d0d01c9 --- /dev/null +++ b/tests/unit/create/via_global_ref/builtin/cpython/cpython3_win_embed.json @@ -0,0 +1,61 @@ +{ + "platform": "win32", + "implementation": "CPython", + "version_info": { + "major": 3, + "minor": 10, + "micro": 4, + "releaselevel": "final", + "serial": 0 + }, + "architecture": 64, + "version_nodot": "310", + "version": "3.10.4 (tags/v3.10.4:9d38120, Mar 23 2022, 23:13:41) [MSC v.1929 64 bit (AMD64)]", + "os": "nt", + "prefix": "c:\\path\\to\\python", + "base_prefix": "c:\\path\\to\\python", + "real_prefix": null, + "base_exec_prefix": "c:\\path\\to\\python", + "exec_prefix": "c:\\path\\to\\python", + "executable": "c:\\path\\to\\python\\python.exe", + "original_executable": "c:\\path\\to\\python\\python.exe", + "system_executable": "c:\\path\\to\\python\\python.exe", + "has_venv": false, + "path": [ + "c:\\path\\to\\python\\Scripts\\virtualenv.exe", + "c:\\path\\to\\python\\python310.zip", + "c:\\path\\to\\python", + "c:\\path\\to\\python\\Lib\\site-packages" + ], + "file_system_encoding": "utf-8", + "stdout_encoding": "utf-8", + "sysconfig_scheme": null, + "sysconfig_paths": { + "stdlib": "{installed_base}/Lib", + "platstdlib": "{base}/Lib", + "purelib": "{base}/Lib/site-packages", + "platlib": "{base}/Lib/site-packages", + "include": "{installed_base}/Include", + "scripts": "{base}/Scripts", + "data": "{base}" + }, + "distutils_install": { + "purelib": "Lib\\site-packages", + "platlib": "Lib\\site-packages", + "headers": "Include\\UNKNOWN", + "scripts": "Scripts", + "data": "" + }, + "sysconfig": { + "makefile_filename": "c:\\path\\to\\python\\Lib\\config\\Makefile" + }, + "sysconfig_vars": { + "PYTHONFRAMEWORK": "", + "installed_base": "c:\\path\\to\\python", + "base": "c:\\path\\to\\python" + }, + "system_stdlib": "c:\\path\\to\\python\\Lib", + "system_stdlib_platform": "c:\\path\\to\\python\\Lib", + "max_size": 9223372036854775807, + "_creators": null +} diff --git a/tests/unit/create/via_global_ref/builtin/cpython/test_cpython3_win.py b/tests/unit/create/via_global_ref/builtin/cpython/test_cpython3_win.py new file mode 100644 index 000000000..087d5902b --- /dev/null +++ b/tests/unit/create/via_global_ref/builtin/cpython/test_cpython3_win.py @@ -0,0 +1,99 @@ +import pytest +from testing.helpers import contains_exe, contains_ref +from testing.path import join as path + +from virtualenv.create.via_global_ref.builtin.cpython.cpython3 import CPython3Windows + +CPYTHON3_PATH = ( + "virtualenv.create.via_global_ref.builtin.cpython.common.Path", + "virtualenv.create.via_global_ref.builtin.cpython.cpython3.Path", +) + + +@pytest.mark.parametrize("py_info_name", ["cpython3_win_embed"]) +def test_2_exe_on_default_py_host(py_info, mock_files): + mock_files(CPYTHON3_PATH, [py_info.system_executable]) + sources = tuple(CPython3Windows.sources(interpreter=py_info)) + # Default Python exe. + assert contains_exe(sources, py_info.system_executable) + # Should always exist. + assert contains_exe(sources, path(py_info.prefix, "pythonw.exe")) + + +@pytest.mark.parametrize("py_info_name", ["cpython3_win_embed"]) +def test_3_exe_on_not_default_py_host(py_info, mock_files): + # Not default python host. + py_info.system_executable = path(py_info.prefix, "python666.exe") + mock_files(CPYTHON3_PATH, [py_info.system_executable]) + sources = tuple(CPython3Windows.sources(interpreter=py_info)) + # Not default Python exe linked to both the default name and origin. + assert contains_exe(sources, py_info.system_executable, "python.exe") + assert contains_exe(sources, py_info.system_executable, "python666.exe") + # Should always exist. + assert contains_exe(sources, path(py_info.prefix, "pythonw.exe")) + + +@pytest.mark.parametrize("py_info_name", ["cpython3_win_embed"]) +def test_only_shim(py_info, mock_files): + shim = path(py_info.system_stdlib, "venv\\scripts\\nt\\python.exe") + py_files = ( + path(py_info.prefix, "libcrypto-1_1.dll"), + path(py_info.prefix, "libffi-7.dll"), + path(py_info.prefix, "_asyncio.pyd"), + path(py_info.prefix, "_bz2.pyd"), + ) + mock_files(CPYTHON3_PATH, [shim, *py_files]) + sources = tuple(CPython3Windows.sources(interpreter=py_info)) + assert CPython3Windows.has_shim(interpreter=py_info) + assert contains_exe(sources, shim) + assert not contains_exe(sources, py_info.system_executable) + for file in py_files: + assert not contains_ref(sources, file) + + +@pytest.mark.parametrize("py_info_name", ["cpython3_win_embed"]) +def test_exe_dll_pyd_without_shim(py_info, mock_files): + py_files = ( + path(py_info.prefix, "libcrypto-1_1.dll"), + path(py_info.prefix, "libffi-7.dll"), + path(py_info.prefix, "_asyncio.pyd"), + path(py_info.prefix, "_bz2.pyd"), + ) + mock_files(CPYTHON3_PATH, py_files) + sources = tuple(CPython3Windows.sources(interpreter=py_info)) + assert not CPython3Windows.has_shim(interpreter=py_info) + assert contains_exe(sources, py_info.system_executable) + for file in py_files: + assert contains_ref(sources, file) + + +@pytest.mark.parametrize("py_info_name", ["cpython3_win_embed"]) +def test_python_zip_if_exists_and_set_in_path(py_info, mock_files): + python_zip_name = "python{}.zip".format(py_info.version_nodot) + python_zip = path(py_info.prefix, python_zip_name) + mock_files(CPYTHON3_PATH, [python_zip]) + sources = tuple(CPython3Windows.sources(interpreter=py_info)) + assert python_zip in py_info.path + assert contains_ref(sources, python_zip) + + +@pytest.mark.parametrize("py_info_name", ["cpython3_win_embed"]) +def test_no_python_zip_if_exists_and_not_set_in_path(py_info, mock_files): + python_zip_name = "python{}.zip".format(py_info.version_nodot) + python_zip = path(py_info.prefix, python_zip_name) + py_info.path.remove(python_zip) + mock_files(CPYTHON3_PATH, [python_zip]) + sources = tuple(CPython3Windows.sources(interpreter=py_info)) + assert python_zip not in py_info.path + assert not contains_ref(sources, python_zip) + + +@pytest.mark.parametrize("py_info_name", ["cpython3_win_embed"]) +def test_no_python_zip_if_not_exists(py_info, mock_files): + python_zip_name = "python{}.zip".format(py_info.version_nodot) + python_zip = path(py_info.prefix, python_zip_name) + # No `python_zip`, just python.exe file. + mock_files(CPYTHON3_PATH, [py_info.system_executable]) + sources = tuple(CPython3Windows.sources(interpreter=py_info)) + assert python_zip in py_info.path + assert not contains_ref(sources, python_zip) diff --git a/tests/unit/create/via_global_ref/builtin/pypy/test_pypy3.py b/tests/unit/create/via_global_ref/builtin/pypy/test_pypy3.py index c4d6860e7..eb35d7f3e 100644 --- a/tests/unit/create/via_global_ref/builtin/pypy/test_pypy3.py +++ b/tests/unit/create/via_global_ref/builtin/pypy/test_pypy3.py @@ -1,104 +1,51 @@ from __future__ import absolute_import, unicode_literals -import fnmatch +import pytest +from testing.helpers import contains_exe, contains_ref +from testing.path import join as path from virtualenv.create.via_global_ref.builtin.pypy.pypy3 import PyPy3Posix -from virtualenv.create.via_global_ref.builtin.ref import ExePathRefToDest, PathRefToDest -from virtualenv.discovery.py_info import PythonInfo -from virtualenv.util.path import Path - - -class FakePath(Path): - """ - A Path() fake that only knows about files in existing_paths and the - directories that contain them. - """ - - existing_paths = [] - - if hasattr(Path(""), "_flavour"): - _flavour = Path("")._flavour - - def exists(self): - return self.as_posix() in self.existing_paths or self.is_dir() - - def glob(self, glob): - pattern = self.as_posix() + "/" + glob - for path in fnmatch.filter(self.existing_paths, pattern): - yield FakePath(path) - - def is_dir(self): - prefix = self.as_posix() + "/" - return any(True for path in self.existing_paths if path.startswith(prefix)) - - def iterdir(self): - prefix = self.as_posix() + "/" - for path in self.existing_paths: - if path.startswith(prefix) and "/" not in path[len(prefix) :]: - yield FakePath(path) - - def resolve(self): - return self - - def __div__(self, key): - return FakePath(super(FakePath, self).__div__(key)) - - def __truediv__(self, key): - return FakePath(super(FakePath, self).__truediv__(key)) - - -def assert_contains_exe(sources, src): - """Assert that the one and only executeable in sources is src""" - exes = [source for source in sources if isinstance(source, ExePathRefToDest)] - assert len(exes) == 1 - exe = exes[0] - assert exe.src.as_posix() == src - - -def assert_contains_ref(sources, src): - """Assert that src appears in sources""" - assert any(source for source in sources if isinstance(source, PathRefToDest) and source.src.as_posix() == src) - - -def inject_fake_path(mocker, existing_paths): - """Inject FakePath in all the correct places, and set existing_paths""" - FakePath.existing_paths = existing_paths - mocker.patch("virtualenv.create.via_global_ref.builtin.pypy.common.Path", FakePath) - mocker.patch("virtualenv.create.via_global_ref.builtin.pypy.pypy3.Path", FakePath) - - -def _load_pypi_info(name): - return PythonInfo._from_json((Path(__file__).parent / "{}.json".format(name)).read_text()) - - -def test_portable_pypy3_virtualenvs_get_their_libs(mocker): - paths = ["/tmp/pypy3.8-v7.3.8-linux64/bin/pypy", "/tmp/pypy3.8-v7.3.8-linux64/lib/libgdbm.so.4"] - inject_fake_path(mocker, paths) - path = Path("/tmp/pypy3.8-v7.3.8-linux64/bin/libpypy3-c.so") - mocker.patch.object(PyPy3Posix, "_shared_libs", return_value=[path]) - - sources = list(PyPy3Posix.sources(interpreter=_load_pypi_info("portable_pypy38"))) - assert_contains_exe(sources, "/tmp/pypy3.8-v7.3.8-linux64/bin/pypy") +from virtualenv.info import IS_WIN, PY2 + +PYPY3_PATH = ( + "virtualenv.create.via_global_ref.builtin.pypy.common.Path", + "virtualenv.create.via_global_ref.builtin.pypy.pypy3.Path", +) + + +# In `PyPy3Posix.sources()` `host_lib` will be broken in Python 2 for Windows, +# so `py_file` will not be in sources. +@pytest.mark.skipif(PY2 and IS_WIN, reason="Can't convert PosixPath") +@pytest.mark.parametrize("py_info_name", ["portable_pypy38"]) +def test_portable_pypy3_virtualenvs_get_their_libs(py_info, mock_files, mock_pypy_libs): + py_file = path(py_info.prefix, "lib/libgdbm.so.4") + mock_files(PYPY3_PATH, [py_info.system_executable, py_file]) + lib_file = path(py_info.prefix, "bin/libpypy3-c.so") + mock_pypy_libs(PyPy3Posix, [lib_file]) + sources = tuple(PyPy3Posix.sources(interpreter=py_info)) assert len(sources) > 2 - assert_contains_ref(sources, "/tmp/pypy3.8-v7.3.8-linux64/bin/libpypy3-c.so") - assert_contains_ref(sources, "/tmp/pypy3.8-v7.3.8-linux64/lib/libgdbm.so.4") + assert contains_exe(sources, py_info.system_executable) + assert contains_ref(sources, py_file) + assert contains_ref(sources, lib_file) -def test_debian_pypy37_virtualenvs(mocker): +@pytest.mark.parametrize("py_info_name", ["deb_pypy37"]) +def test_debian_pypy37_virtualenvs(py_info, mock_files, mock_pypy_libs): # Debian's pypy3 layout, installed to /usr, before 3.8 allowed a /usr prefix - inject_fake_path(mocker, ["/usr/bin/pypy3"]) - mocker.patch.object(PyPy3Posix, "_shared_libs", return_value=[Path("/usr/lib/pypy3/bin/libpypy3-c.so")]) - sources = list(PyPy3Posix.sources(interpreter=_load_pypi_info("deb_pypy37"))) - assert_contains_exe(sources, "/usr/bin/pypy3") - assert_contains_ref(sources, "/usr/lib/pypy3/bin/libpypy3-c.so") + mock_files(PYPY3_PATH, [py_info.system_executable]) + lib_file = path(py_info.prefix, "bin/libpypy3-c.so") + mock_pypy_libs(PyPy3Posix, [lib_file]) + sources = tuple(PyPy3Posix.sources(interpreter=py_info)) assert len(sources) == 2 + assert contains_exe(sources, py_info.system_executable) + assert contains_ref(sources, lib_file) -def test_debian_pypy38_virtualenvs_exclude_usr(mocker): - inject_fake_path(mocker, ["/usr/bin/pypy3", "/usr/lib/foo"]) +@pytest.mark.parametrize("py_info_name", ["deb_pypy38"]) +def test_debian_pypy38_virtualenvs_exclude_usr(py_info, mock_files, mock_pypy_libs): + mock_files(PYPY3_PATH, [py_info.system_executable, "/usr/lib/foo"]) # libpypy3-c.so lives on the ld search path - mocker.patch.object(PyPy3Posix, "_shared_libs", return_value=[]) - - sources = list(PyPy3Posix.sources(interpreter=_load_pypi_info("deb_pypy38"))) - assert_contains_exe(sources, "/usr/bin/pypy3") + mock_pypy_libs(PyPy3Posix, []) + sources = tuple(PyPy3Posix.sources(interpreter=py_info)) assert len(sources) == 1 + assert contains_exe(sources, py_info.system_executable) diff --git a/tests/unit/create/via_global_ref/builtin/testing/__init__.py b/tests/unit/create/via_global_ref/builtin/testing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/create/via_global_ref/builtin/testing/helpers.py b/tests/unit/create/via_global_ref/builtin/testing/helpers.py new file mode 100644 index 000000000..c42dd39b9 --- /dev/null +++ b/tests/unit/create/via_global_ref/builtin/testing/helpers.py @@ -0,0 +1,38 @@ +from functools import reduce + +from virtualenv.create.via_global_ref.builtin.ref import ExePathRefToDest, PathRef +from virtualenv.util.path import Path + + +def is_ref(source): + return isinstance(source, PathRef) + + +def is_exe(source): + return type(source) is ExePathRefToDest + + +def has_src(src): + return lambda ref: ref.src.as_posix() == Path(src).as_posix() + + +def has_target(target): + return lambda ref: ref.base == target + + +def apply_filter(values, function): + return filter(function, values) + + +def filterby(filters, sources): + return reduce(apply_filter, filters, sources) + + +def contains_exe(sources, src, target=None): + filters = is_exe, has_src(src), target and has_target(target) + return any(filterby(filters, sources)) + + +def contains_ref(sources, src): + filters = is_ref, has_src(src) + return any(filterby(filters, sources)) diff --git a/tests/unit/create/via_global_ref/builtin/testing/path.py b/tests/unit/create/via_global_ref/builtin/testing/path.py new file mode 100644 index 000000000..ece41cf51 --- /dev/null +++ b/tests/unit/create/via_global_ref/builtin/testing/path.py @@ -0,0 +1,93 @@ +from abc import ABCMeta, abstractmethod +from itertools import chain +from operator import attrgetter as attr + +from six import add_metaclass + +from virtualenv.util.path import Path + + +def is_name(path): + return str(path) == path.name + + +@add_metaclass(ABCMeta) +class FakeDataABC(object): + """Provides data to mock the `Path`""" + + @property + @abstractmethod + def filelist(self): + raise NotImplementedError("Collection of (str) file paths to mock") + + @property + def fake_files(self): + return map(type(self), self.filelist) + + @property + def fake_dirs(self): + return set(chain(*map(attr("parents"), self.fake_files))) + + @property + def contained_fake_names(self): + return filter(is_name, self.fake_content) + + @property + def fake_content(self): + return filter(None, map(self.fake_child, self.fake_files)) + + def fake_child(self, path): + try: + return path.relative_to(self) + except ValueError: + return None + + +class PathMockABC(FakeDataABC, Path): + """Mocks the behavior of `Path`""" + + _flavour = getattr(Path(), "_flavour", None) + + if hasattr(_flavour, "altsep"): + # Allows to pass some tests for Windows via PosixPath. + _flavour.altsep = _flavour.altsep or "\\" + + def exists(self): + return self.is_file() or self.is_dir() + + def is_file(self): + return self in self.fake_files + + def is_dir(self): + return self in self.fake_dirs + + def resolve(self): + return self + + def iterdir(self): + for path in map(self.joinpath, self.contained_fake_names): + yield path + + +def MetaPathMock(filelist): + """ + Metaclass that creates a `PathMock` class with the `filelist` defined. + """ + return type("PathMock", (PathMockABC,), {"filelist": filelist}) + + +def mock_files(mocker, pathlist, filelist): + PathMock = MetaPathMock(set(filelist)) + for path in pathlist: + mocker.patch(path, PathMock) + + +def mock_pypy_libs(mocker, pypy_creator_cls, libs): + paths = tuple(set(map(Path, libs))) + mocker.patch.object(pypy_creator_cls, "_shared_libs", return_value=paths) + + +def join(*chunks): + line = "".join(chunks) + sep = ("\\" in line and "\\") or ("/" in line and "/") or "/" + return sep.join(chunks) diff --git a/tests/unit/create/via_global_ref/builtin/testing/py_info.py b/tests/unit/create/via_global_ref/builtin/testing/py_info.py new file mode 100644 index 000000000..20dafb69b --- /dev/null +++ b/tests/unit/create/via_global_ref/builtin/testing/py_info.py @@ -0,0 +1,19 @@ +from virtualenv.discovery.py_info import PythonInfo +from virtualenv.info import PY2 +from virtualenv.util.path import Path + + +def fixture_file(fixture_name): + file_mask = "*{}.json".format(fixture_name) + files = Path(__file__).parent.parent.rglob(file_mask) + try: + return next(files) + except StopIteration: + # Fixture file was not found in the testing root and its subdirs. + error = NameError if PY2 else FileNotFoundError + raise error(file_mask) + + +def read_fixture(fixture_name): + fixture_json = fixture_file(fixture_name).read_text() + return PythonInfo._from_json(fixture_json)