2018-10-08 18:09:53 +02:00
|
|
|
import compileall
|
2022-06-21 18:35:22 +02:00
|
|
|
import os
|
2019-07-20 01:59:53 +02:00
|
|
|
import shutil
|
2022-10-27 05:02:51 +02:00
|
|
|
import subprocess
|
|
|
|
import sys
|
2022-07-22 08:11:35 +02:00
|
|
|
import sysconfig
|
2018-10-09 08:23:03 +02:00
|
|
|
import textwrap
|
2020-12-22 09:22:20 +01:00
|
|
|
import venv as _venv
|
2022-06-07 11:52:38 +02:00
|
|
|
from pathlib import Path
|
2022-10-27 03:15:33 +02:00
|
|
|
from typing import TYPE_CHECKING, Dict, Optional, Union
|
2013-08-22 06:38:23 +02:00
|
|
|
|
|
|
|
import virtualenv as _virtualenv
|
|
|
|
|
2021-08-25 16:02:26 +02:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
# Literal was introduced in Python 3.8.
|
|
|
|
from typing import Literal
|
|
|
|
|
2021-08-28 17:52:10 +02:00
|
|
|
VirtualEnvironmentType = Literal["virtualenv", "venv"]
|
|
|
|
else:
|
|
|
|
VirtualEnvironmentType = str
|
|
|
|
|
2013-08-22 06:38:23 +02:00
|
|
|
|
2020-12-24 22:23:07 +01:00
|
|
|
class VirtualEnvironment:
|
2013-08-22 06:38:23 +02:00
|
|
|
"""
|
|
|
|
An abstraction around virtual environments, currently it only uses
|
|
|
|
virtualenv but in the future it could use pyvenv.
|
|
|
|
"""
|
|
|
|
|
2021-08-25 16:02:26 +02:00
|
|
|
def __init__(
|
|
|
|
self,
|
2022-06-07 11:52:38 +02:00
|
|
|
location: Path,
|
2021-08-25 16:02:26 +02:00
|
|
|
template: Optional["VirtualEnvironment"] = None,
|
2021-08-28 17:52:10 +02:00
|
|
|
venv_type: Optional[VirtualEnvironmentType] = None,
|
2022-10-27 15:34:17 +02:00
|
|
|
) -> None:
|
2022-06-07 11:52:38 +02:00
|
|
|
self.location = location
|
2021-08-25 16:02:26 +02:00
|
|
|
assert template is None or venv_type is None
|
2021-08-28 17:52:10 +02:00
|
|
|
self._venv_type: VirtualEnvironmentType
|
2021-08-25 16:02:26 +02:00
|
|
|
if template is not None:
|
|
|
|
self._venv_type = template._venv_type
|
|
|
|
elif venv_type is not None:
|
|
|
|
self._venv_type = venv_type
|
|
|
|
else:
|
|
|
|
self._venv_type = "virtualenv"
|
2018-10-08 18:09:53 +02:00
|
|
|
self._user_site_packages = False
|
|
|
|
self._template = template
|
2021-08-25 16:02:26 +02:00
|
|
|
self._sitecustomize: Optional[str] = None
|
2018-10-08 18:09:53 +02:00
|
|
|
self._update_paths()
|
|
|
|
self._create()
|
|
|
|
|
2022-10-27 15:34:17 +02:00
|
|
|
@property
|
|
|
|
def _legacy_virtualenv(self) -> bool:
|
|
|
|
if self._venv_type != "virtualenv":
|
|
|
|
return False
|
|
|
|
return int(_virtualenv.__version__.split(".", 1)[0]) < 20
|
|
|
|
|
2022-10-27 05:02:51 +02:00
|
|
|
def __update_paths_legacy(self) -> None:
|
|
|
|
home, lib, inc, bin = _virtualenv.path_locations(self.location)
|
|
|
|
self.bin = Path(bin)
|
|
|
|
self.site = Path(lib) / "site-packages"
|
|
|
|
# Workaround for https://github.com/pypa/virtualenv/issues/306
|
|
|
|
if hasattr(sys, "pypy_version_info"):
|
|
|
|
version_dir = str(sys.version_info.major)
|
|
|
|
self.lib = Path(home, "lib-python", version_dir)
|
|
|
|
else:
|
|
|
|
self.lib = Path(lib)
|
|
|
|
|
2021-08-25 16:02:26 +02:00
|
|
|
def _update_paths(self) -> None:
|
2022-10-27 15:34:17 +02:00
|
|
|
if self._legacy_virtualenv:
|
2022-10-27 05:02:51 +02:00
|
|
|
self.__update_paths_legacy()
|
|
|
|
return
|
2022-07-22 08:11:35 +02:00
|
|
|
bases = {
|
|
|
|
"installed_base": self.location,
|
|
|
|
"installed_platbase": self.location,
|
|
|
|
"base": self.location,
|
|
|
|
"platbase": self.location,
|
|
|
|
}
|
|
|
|
paths = sysconfig.get_paths(vars=bases)
|
|
|
|
self.bin = Path(paths["scripts"])
|
|
|
|
self.site = Path(paths["purelib"])
|
|
|
|
self.lib = Path(paths["stdlib"])
|
2013-10-23 21:21:48 +02:00
|
|
|
|
2021-08-25 16:02:26 +02:00
|
|
|
def __repr__(self) -> str:
|
2020-12-23 20:25:12 +01:00
|
|
|
return f"<VirtualEnvironment {self.location}>"
|
2013-08-22 06:38:23 +02:00
|
|
|
|
2021-08-25 16:02:26 +02:00
|
|
|
def _create(self, clear: bool = False) -> None:
|
2018-10-08 18:09:53 +02:00
|
|
|
if clear:
|
2019-07-20 01:59:53 +02:00
|
|
|
shutil.rmtree(self.location)
|
2018-10-08 18:09:53 +02:00
|
|
|
if self._template:
|
2022-10-27 05:02:51 +02:00
|
|
|
# On Windows, calling `_virtualenv.path_locations(target)`
|
|
|
|
# will have created the `target` directory...
|
2022-10-27 15:34:17 +02:00
|
|
|
if (
|
|
|
|
self._legacy_virtualenv
|
|
|
|
and sys.platform == "win32"
|
|
|
|
and self.location.exists()
|
|
|
|
):
|
2022-10-27 05:02:51 +02:00
|
|
|
self.location.rmdir()
|
2018-10-08 18:09:53 +02:00
|
|
|
# Clone virtual environment from template.
|
2021-04-02 11:21:40 +02:00
|
|
|
shutil.copytree(self._template.location, self.location, symlinks=True)
|
2018-10-08 18:09:53 +02:00
|
|
|
self._sitecustomize = self._template.sitecustomize
|
|
|
|
self._user_site_packages = self._template.user_site_packages
|
|
|
|
else:
|
|
|
|
# Create a new virtual environment.
|
2022-10-27 15:34:17 +02:00
|
|
|
if self._legacy_virtualenv:
|
|
|
|
subprocess.check_call(
|
|
|
|
[
|
|
|
|
sys.executable,
|
|
|
|
"-m",
|
|
|
|
"virtualenv",
|
|
|
|
"--no-pip",
|
|
|
|
"--no-wheel",
|
|
|
|
"--no-setuptools",
|
|
|
|
os.fspath(self.location),
|
|
|
|
]
|
|
|
|
)
|
|
|
|
self._fix_legacy_virtualenv_site_module()
|
|
|
|
elif self._venv_type == "virtualenv":
|
|
|
|
_virtualenv.cli_run(
|
|
|
|
[
|
|
|
|
"--no-pip",
|
|
|
|
"--no-wheel",
|
|
|
|
"--no-setuptools",
|
|
|
|
os.fspath(self.location),
|
|
|
|
],
|
|
|
|
)
|
2021-04-02 11:21:40 +02:00
|
|
|
elif self._venv_type == "venv":
|
2018-10-09 08:23:03 +02:00
|
|
|
builder = _venv.EnvBuilder()
|
|
|
|
context = builder.ensure_directories(self.location)
|
|
|
|
builder.create_configuration(context)
|
|
|
|
builder.setup_python(context)
|
2019-08-17 03:34:17 +02:00
|
|
|
self.site.mkdir(parents=True, exist_ok=True)
|
2022-10-27 15:34:17 +02:00
|
|
|
else:
|
|
|
|
raise RuntimeError(f"Unsupported venv type {self._venv_type!r}")
|
2018-10-08 18:09:53 +02:00
|
|
|
self.sitecustomize = self._sitecustomize
|
|
|
|
self.user_site_packages = self._user_site_packages
|
|
|
|
|
2022-10-27 05:02:51 +02:00
|
|
|
def _fix_legacy_virtualenv_site_module(self) -> None:
|
|
|
|
# Patch `site.py` so user site work as expected.
|
|
|
|
site_py = self.lib / "site.py"
|
|
|
|
with open(site_py) as fp:
|
|
|
|
site_contents = fp.read()
|
|
|
|
for pattern, replace in (
|
|
|
|
(
|
|
|
|
# Ensure enabling user site does not result in adding
|
|
|
|
# the real site-packages' directory to `sys.path`.
|
|
|
|
("\ndef virtual_addsitepackages(known_paths):\n"),
|
|
|
|
(
|
|
|
|
"\ndef virtual_addsitepackages(known_paths):\n"
|
|
|
|
" return known_paths\n"
|
|
|
|
),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
# Fix sites ordering: user site must be added before system.
|
|
|
|
(
|
|
|
|
"\n paths_in_sys = addsitepackages(paths_in_sys)"
|
|
|
|
"\n paths_in_sys = addusersitepackages(paths_in_sys)\n"
|
|
|
|
),
|
|
|
|
(
|
|
|
|
"\n paths_in_sys = addusersitepackages(paths_in_sys)"
|
|
|
|
"\n paths_in_sys = addsitepackages(paths_in_sys)\n"
|
|
|
|
),
|
|
|
|
),
|
|
|
|
):
|
|
|
|
assert pattern in site_contents
|
|
|
|
site_contents = site_contents.replace(pattern, replace)
|
|
|
|
with open(site_py, "w") as fp:
|
|
|
|
fp.write(site_contents)
|
|
|
|
# Make sure bytecode is up-to-date too.
|
|
|
|
assert compileall.compile_file(str(site_py), quiet=1, force=True)
|
|
|
|
|
2021-08-25 16:02:26 +02:00
|
|
|
def _customize_site(self) -> None:
|
2022-10-27 15:34:17 +02:00
|
|
|
if self._legacy_virtualenv:
|
|
|
|
contents = ""
|
|
|
|
else:
|
2022-10-27 05:02:51 +02:00
|
|
|
# Enable user site (before system).
|
|
|
|
contents = textwrap.dedent(
|
|
|
|
f"""
|
|
|
|
import os, site, sys
|
|
|
|
if not os.environ.get('PYTHONNOUSERSITE', False):
|
|
|
|
site.ENABLE_USER_SITE = {self._user_site_packages}
|
|
|
|
# First, drop system-sites related paths.
|
|
|
|
original_sys_path = sys.path[:]
|
|
|
|
known_paths = set()
|
|
|
|
for path in site.getsitepackages():
|
|
|
|
site.addsitedir(path, known_paths=known_paths)
|
|
|
|
system_paths = sys.path[len(original_sys_path):]
|
|
|
|
for path in system_paths:
|
|
|
|
if path in original_sys_path:
|
|
|
|
original_sys_path.remove(path)
|
|
|
|
sys.path = original_sys_path
|
|
|
|
# Second, add user-site.
|
|
|
|
if {self._user_site_packages}:
|
|
|
|
site.addsitedir(site.getusersitepackages())
|
|
|
|
# Third, add back system-sites related paths.
|
|
|
|
for path in site.getsitepackages():
|
|
|
|
site.addsitedir(path)
|
|
|
|
"""
|
|
|
|
).strip()
|
2018-10-08 18:09:53 +02:00
|
|
|
if self._sitecustomize is not None:
|
2021-04-02 11:21:40 +02:00
|
|
|
contents += "\n" + self._sitecustomize
|
2018-10-08 18:09:53 +02:00
|
|
|
sitecustomize = self.site / "sitecustomize.py"
|
2019-07-02 07:00:32 +02:00
|
|
|
sitecustomize.write_text(contents)
|
2018-10-08 18:09:53 +02:00
|
|
|
# Make sure bytecode is up-to-date too.
|
|
|
|
assert compileall.compile_file(str(sitecustomize), quiet=1, force=True)
|
2013-08-22 06:38:23 +02:00
|
|
|
|
2022-10-27 03:15:33 +02:00
|
|
|
def _rewrite_pyvenv_cfg(self, replacements: Dict[str, str]) -> None:
|
|
|
|
pyvenv_cfg = self.location.joinpath("pyvenv.cfg")
|
|
|
|
lines = pyvenv_cfg.read_text(encoding="utf-8").splitlines()
|
|
|
|
|
|
|
|
def maybe_replace_line(line: str) -> str:
|
|
|
|
key = line.split("=", 1)[0].strip()
|
|
|
|
try:
|
|
|
|
value = replacements[key]
|
|
|
|
except KeyError: # No need to replace.
|
|
|
|
return line
|
|
|
|
return f"{key} = {value}"
|
|
|
|
|
|
|
|
lines = [maybe_replace_line(line) for line in lines]
|
|
|
|
pyvenv_cfg.write_text("\n".join(lines), encoding="utf-8")
|
|
|
|
|
2021-08-25 16:02:26 +02:00
|
|
|
def clear(self) -> None:
|
2013-08-22 06:38:23 +02:00
|
|
|
self._create(clear=True)
|
|
|
|
|
2022-06-07 11:52:38 +02:00
|
|
|
def move(self, location: Union[Path, str]) -> None:
|
2022-06-21 18:35:22 +02:00
|
|
|
shutil.move(os.fspath(self.location), location)
|
2018-10-08 18:09:53 +02:00
|
|
|
self.location = Path(location)
|
|
|
|
self._update_paths()
|
|
|
|
|
|
|
|
@property
|
2021-08-25 16:02:26 +02:00
|
|
|
def sitecustomize(self) -> Optional[str]:
|
2018-10-08 18:09:53 +02:00
|
|
|
return self._sitecustomize
|
|
|
|
|
|
|
|
@sitecustomize.setter
|
2021-08-25 16:02:26 +02:00
|
|
|
def sitecustomize(self, value: str) -> None:
|
2018-10-08 18:09:53 +02:00
|
|
|
self._sitecustomize = value
|
|
|
|
self._customize_site()
|
|
|
|
|
2013-08-22 06:38:23 +02:00
|
|
|
@property
|
2021-08-25 16:02:26 +02:00
|
|
|
def user_site_packages(self) -> bool:
|
2018-10-08 18:09:53 +02:00
|
|
|
return self._user_site_packages
|
2013-08-22 06:38:23 +02:00
|
|
|
|
2018-10-08 18:09:53 +02:00
|
|
|
@user_site_packages.setter
|
2021-08-25 16:02:26 +02:00
|
|
|
def user_site_packages(self, value: bool) -> None:
|
2022-07-22 12:04:28 +02:00
|
|
|
self._user_site_packages = value
|
2022-10-27 15:34:17 +02:00
|
|
|
if self._legacy_virtualenv:
|
2022-10-27 05:02:51 +02:00
|
|
|
marker = self.lib / "no-global-site-packages.txt"
|
|
|
|
if self._user_site_packages:
|
|
|
|
marker.unlink()
|
|
|
|
else:
|
|
|
|
marker.touch()
|
2022-10-27 15:34:17 +02:00
|
|
|
else:
|
|
|
|
self._rewrite_pyvenv_cfg(
|
|
|
|
{"include-system-site-packages": str(bool(value)).lower()}
|
|
|
|
)
|
|
|
|
self._customize_site()
|