_virtualenv.py 4.54 KB
Newer Older
Vladislav Rykov's avatar
Vladislav Rykov committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
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
114
115
"""Patches that are applied at runtime to the virtual environment"""
# -*- coding: utf-8 -*-

import os
import sys

VIRTUALENV_PATCH_FILE = os.path.join(__file__)


def patch_dist(dist):
    """
    Distutils allows user to configure some arguments via a configuration file:
    https://docs.python.org/3/install/index.html#distutils-configuration-files

    Some of this arguments though don't make sense in context of the virtual environment files, let's fix them up.
    """
    # we cannot allow some install config as that would get packages installed outside of the virtual environment
    old_parse_config_files = dist.Distribution.parse_config_files

    def parse_config_files(self, *args, **kwargs):
        result = old_parse_config_files(self, *args, **kwargs)
        install = self.get_option_dict("install")

        if "prefix" in install:  # the prefix governs where to install the libraries
            install["prefix"] = VIRTUALENV_PATCH_FILE, os.path.abspath(sys.prefix)
        for base in ("purelib", "platlib", "headers", "scripts", "data"):
            key = "install_{}".format(base)
            if key in install:  # do not allow global configs to hijack venv paths
                install.pop(key, None)
        return result

    dist.Distribution.parse_config_files = parse_config_files


# Import hook that patches some modules to ignore configuration values that break package installation in case
# of virtual environments.
_DISTUTILS_PATCH = "distutils.dist", "setuptools.dist"
if sys.version_info > (3, 4):
    # https://docs.python.org/3/library/importlib.html#setting-up-an-importer
    from importlib.abc import MetaPathFinder
    from importlib.util import find_spec
    from threading import Lock
    from functools import partial

    class _Finder(MetaPathFinder):
        """A meta path finder that allows patching the imported distutils modules"""

        fullname = None
        lock = Lock()

        def find_spec(self, fullname, path, target=None):
            if fullname in _DISTUTILS_PATCH and self.fullname is None:
                with self.lock:
                    self.fullname = fullname
                    try:
                        spec = find_spec(fullname, path)
                        if spec is not None:
                            # https://www.python.org/dev/peps/pep-0451/#how-loading-will-work
                            is_new_api = hasattr(spec.loader, "exec_module")
                            func_name = "exec_module" if is_new_api else "load_module"
                            old = getattr(spec.loader, func_name)
                            func = self.exec_module if is_new_api else self.load_module
                            if old is not func:
                                try:
                                    setattr(spec.loader, func_name, partial(func, old))
                                except AttributeError:
                                    pass  # C-Extension loaders are r/o such as zipimporter with <python 3.7
                            return spec
                    finally:
                        self.fullname = None

        @staticmethod
        def exec_module(old, module):
            old(module)
            if module.__name__ in _DISTUTILS_PATCH:
                patch_dist(module)

        @staticmethod
        def load_module(old, name):
            module = old(name)
            if module.__name__ in _DISTUTILS_PATCH:
                patch_dist(module)
            return module

    sys.meta_path.insert(0, _Finder())
else:
    # https://www.python.org/dev/peps/pep-0302/
    from imp import find_module
    from pkgutil import ImpImporter, ImpLoader

    class _VirtualenvImporter(object, ImpImporter):
        def __init__(self, path=None):
            object.__init__(self)
            ImpImporter.__init__(self, path)

        def find_module(self, fullname, path=None):
            if fullname in _DISTUTILS_PATCH:
                try:
                    return _VirtualenvLoader(fullname, *find_module(fullname.split(".")[-1], path))
                except ImportError:
                    pass
            return None

    class _VirtualenvLoader(object, ImpLoader):
        def __init__(self, fullname, file, filename, etc):
            object.__init__(self)
            ImpLoader.__init__(self, fullname, file, filename, etc)

        def load_module(self, fullname):
            module = super(_VirtualenvLoader, self).load_module(fullname)
            patch_dist(module)
            module.__loader__ = None  # distlib fallback
            return module

    sys.meta_path.append(_VirtualenvImporter())