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

import virtualenv as _virtualenv

from .path import Path


class VirtualEnvironment(object):
    """
    An abstraction around virtual environments, currently it only uses
    virtualenv but in the future it could use pyvenv.
    """

18 19 20
    def __init__(self, location, template=None, venv_type=None):
        assert template is None or venv_type is None
        assert venv_type in (None, 'virtualenv', 'venv')
21
        self.location = Path(location)
22
        self._venv_type = venv_type or template._venv_type or 'virtualenv'
23 24 25 26 27 28 29
        self._user_site_packages = False
        self._template = template
        self._sitecustomize = None
        self._update_paths()
        self._create()

    def _update_paths(self):
30 31
        home, lib, inc, bin = _virtualenv.path_locations(self.location)
        self.bin = Path(bin)
32 33 34
        self.site = Path(lib) / 'site-packages'
        # Workaround for https://github.com/pypa/virtualenv/issues/306
        if hasattr(sys, "pypy_version_info"):
H
Hugo van Kemenade 已提交
35
            version_dir = '{0}'.format(*sys.version_info)
36 37 38
            self.lib = Path(home, 'lib-python', version_dir)
        else:
            self.lib = Path(lib)
39

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

43 44
    def _create(self, clear=False):
        if clear:
45
            shutil.rmtree(self.location)
46 47 48
        if self._template:
            # On Windows, calling `_virtualenv.path_locations(target)`
            # will have created the `target` directory...
49
            if sys.platform == 'win32' and self.location.exists():
50 51
                self.location.rmdir()
            # Clone virtual environment from template.
52 53 54
            shutil.copytree(
                self._template.location, self.location, symlinks=True
            )
55 56 57 58
            self._sitecustomize = self._template.sitecustomize
            self._user_site_packages = self._template.user_site_packages
        else:
            # Create a new virtual environment.
59 60 61 62 63 64 65 66 67 68 69 70 71
            if self._venv_type == 'virtualenv':
                _virtualenv.create_environment(
                    self.location,
                    no_pip=True,
                    no_wheel=True,
                    no_setuptools=True,
                )
                self._fix_virtualenv_site_module()
            elif self._venv_type == 'venv':
                builder = _venv.EnvBuilder()
                context = builder.ensure_directories(self.location)
                builder.create_configuration(context)
                builder.setup_python(context)
72
                self.site.mkdir(parents=True, exist_ok=True)
73 74 75
            self.sitecustomize = self._sitecustomize
            self.user_site_packages = self._user_site_packages

76
    def _fix_virtualenv_site_module(self):
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
        # 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)

    def _customize_site(self):
        contents = ''
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
        if self._venv_type == 'venv':
            # Enable user site (before system).
            contents += textwrap.dedent(
                '''
                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)
                ''').strip()
142 143 144
        if self._sitecustomize is not None:
            contents += '\n' + self._sitecustomize
        sitecustomize = self.site / "sitecustomize.py"
145
        sitecustomize.write_text(contents)
146 147
        # Make sure bytecode is up-to-date too.
        assert compileall.compile_file(str(sitecustomize), quiet=1, force=True)
148 149 150 151

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

152
    def move(self, location):
153
        shutil.move(self.location, location)
154 155 156 157 158 159 160 161 162 163 164 165
        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()

166
    @property
167 168
    def user_site_packages(self):
        return self._user_site_packages
169

170 171 172
    @user_site_packages.setter
    def user_site_packages(self, value):
        self._user_site_packages = value
173 174 175
        if self._venv_type == 'virtualenv':
            marker = self.lib / "no-global-site-packages.txt"
            if self._user_site_packages:
176
                marker.unlink()
177 178 179 180
            else:
                marker.touch()
        elif self._venv_type == 'venv':
            self._customize_site()