venv.py 6.7 KB
Newer Older
1
import compileall
2
import shutil
3
import subprocess
4
import sys
5
import textwrap
H
Hugo van Kemenade 已提交
6
import venv as _venv
7 8 9 10 11 12

import virtualenv as _virtualenv

from .path import Path


13
class VirtualEnvironment:
14 15 16 17 18
    """
    An abstraction around virtual environments, currently it only uses
    virtualenv but in the future it could use pyvenv.
    """

19 20
    def __init__(self, location, template=None, venv_type=None):
        assert template is None or venv_type is None
P
Pradyun Gedam 已提交
21
        assert venv_type in (None, "virtualenv", "venv")
22
        self.location = Path(location)
P
Pradyun Gedam 已提交
23
        self._venv_type = venv_type or template._venv_type or "virtualenv"
24 25 26 27 28 29 30
        self._user_site_packages = False
        self._template = template
        self._sitecustomize = None
        self._update_paths()
        self._create()

    def _update_paths(self):
31 32
        home, lib, inc, bin = _virtualenv.path_locations(self.location)
        self.bin = Path(bin)
P
Pradyun Gedam 已提交
33
        self.site = Path(lib) / "site-packages"
34 35
        # Workaround for https://github.com/pypa/virtualenv/issues/306
        if hasattr(sys, "pypy_version_info"):
36
            version_dir = str(sys.version_info.major)
P
Pradyun Gedam 已提交
37
            self.lib = Path(home, "lib-python", version_dir)
38 39
        else:
            self.lib = Path(lib)
40

41
    def __repr__(self):
42
        return f"<VirtualEnvironment {self.location}>"
43

44 45
    def _create(self, clear=False):
        if clear:
46
            shutil.rmtree(self.location)
47 48 49
        if self._template:
            # On Windows, calling `_virtualenv.path_locations(target)`
            # will have created the `target` directory...
P
Pradyun Gedam 已提交
50
            if sys.platform == "win32" and self.location.exists():
51 52
                self.location.rmdir()
            # Clone virtual environment from template.
P
Pradyun Gedam 已提交
53
            shutil.copytree(self._template.location, self.location, symlinks=True)
54 55 56 57
            self._sitecustomize = self._template.sitecustomize
            self._user_site_packages = self._template.user_site_packages
        else:
            # Create a new virtual environment.
P
Pradyun Gedam 已提交
58
            if self._venv_type == "virtualenv":
59 60 61 62 63 64 65 66 67 68
                subprocess.check_call(
                    [
                        sys.executable,
                        "-m",
                        "virtualenv",
                        "--no-pip",
                        "--no-wheel",
                        "--no-setuptools",
                        str(self.location),
                    ]
69 70
                )
                self._fix_virtualenv_site_module()
P
Pradyun Gedam 已提交
71
            elif self._venv_type == "venv":
72 73 74 75
                builder = _venv.EnvBuilder()
                context = builder.ensure_directories(self.location)
                builder.create_configuration(context)
                builder.setup_python(context)
76
                self.site.mkdir(parents=True, exist_ok=True)
77 78 79
            self.sitecustomize = self._sitecustomize
            self.user_site_packages = self._user_site_packages

80
    def _fix_virtualenv_site_module(self):
81
        # Patch `site.py` so user site work as expected.
P
Pradyun Gedam 已提交
82
        site_py = self.lib / "site.py"
83 84 85 86 87 88
        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`.
P
Pradyun Gedam 已提交
89
                ("\ndef virtual_addsitepackages(known_paths):\n"),
90
                (
P
Pradyun Gedam 已提交
91 92
                    "\ndef virtual_addsitepackages(known_paths):\n"
                    "    return known_paths\n"
93 94 95 96 97
                ),
            ),
            (
                # Fix sites ordering: user site must be added before system.
                (
P
Pradyun Gedam 已提交
98 99
                    "\n    paths_in_sys = addsitepackages(paths_in_sys)"
                    "\n    paths_in_sys = addusersitepackages(paths_in_sys)\n"
100 101
                ),
                (
P
Pradyun Gedam 已提交
102 103
                    "\n    paths_in_sys = addusersitepackages(paths_in_sys)"
                    "\n    paths_in_sys = addsitepackages(paths_in_sys)\n"
104 105 106 107 108
                ),
            ),
        ):
            assert pattern in site_contents
            site_contents = site_contents.replace(pattern, replace)
P
Pradyun Gedam 已提交
109
        with open(site_py, "w") as fp:
110 111 112 113 114
            fp.write(site_contents)
        # Make sure bytecode is up-to-date too.
        assert compileall.compile_file(str(site_py), quiet=1, force=True)

    def _customize_site(self):
P
Pradyun Gedam 已提交
115 116
        contents = ""
        if self._venv_type == "venv":
117 118
            # Enable user site (before system).
            contents += textwrap.dedent(
P
Pradyun Gedam 已提交
119
                """
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
                import os, site, sys

                if not os.environ.get('PYTHONNOUSERSITE', False):

                    site.ENABLE_USER_SITE = True

                    # 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.
                    site.addsitedir(site.getusersitepackages())

                    # Third, add back system-sites related paths.
                    for path in site.getsitepackages():
                        site.addsitedir(path)
P
Pradyun Gedam 已提交
143 144
                """
            ).strip()
145
        if self._sitecustomize is not None:
P
Pradyun Gedam 已提交
146
            contents += "\n" + self._sitecustomize
147
        sitecustomize = self.site / "sitecustomize.py"
148
        sitecustomize.write_text(contents)
149 150
        # Make sure bytecode is up-to-date too.
        assert compileall.compile_file(str(sitecustomize), quiet=1, force=True)
151 152 153 154

    def clear(self):
        self._create(clear=True)

155
    def move(self, location):
156
        shutil.move(self.location, location)
157 158 159 160 161 162 163 164 165 166 167 168
        self.location = Path(location)
        self._update_paths()

    @property
    def sitecustomize(self):
        return self._sitecustomize

    @sitecustomize.setter
    def sitecustomize(self, value):
        self._sitecustomize = value
        self._customize_site()

169
    @property
170 171
    def user_site_packages(self):
        return self._user_site_packages
172

173 174 175
    @user_site_packages.setter
    def user_site_packages(self, value):
        self._user_site_packages = value
P
Pradyun Gedam 已提交
176
        if self._venv_type == "virtualenv":
177 178
            marker = self.lib / "no-global-site-packages.txt"
            if self._user_site_packages:
179
                marker.unlink()
180 181
            else:
                marker.touch()
P
Pradyun Gedam 已提交
182
        elif self._venv_type == "venv":
183
            self._customize_site()