Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Windows embedable support #2353

Merged
merged 21 commits into from
Jun 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/changelog/1774.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Support for Windows embeddable Python package: includes ``python<VERSION>.zip``
in the creator sources.
49 changes: 40 additions & 9 deletions src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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)
4 changes: 4 additions & 0 deletions src/virtualenv/discovery/py_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Binary file not shown.
11 changes: 10 additions & 1 deletion src/virtualenv/util/path/_pathlib/via_os_path.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import absolute_import, unicode_literals

import fnmatch
import os
import platform
from contextlib import contextmanager
Expand All @@ -10,7 +11,7 @@


class Path(object):
def __init__(self, path):
def __init__(self, path=""):
if isinstance(path, Path):
_path = path._path
else:
Expand Down Expand Up @@ -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",)
10 changes: 9 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
25 changes: 25 additions & 0 deletions tests/unit/create/via_global_ref/builtin/conftest.py
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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)
Loading