• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# don't import any costly modules
2import sys
3import os
4
5
6is_pypy = '__pypy__' in sys.builtin_module_names
7
8
9def warn_distutils_present():
10    if 'distutils' not in sys.modules:
11        return
12    if is_pypy and sys.version_info < (3, 7):
13        # PyPy for 3.6 unconditionally imports distutils, so bypass the warning
14        # https://foss.heptapod.net/pypy/pypy/-/blob/be829135bc0d758997b3566062999ee8b23872b4/lib-python/3/site.py#L250
15        return
16    import warnings
17    warnings.warn(
18        "Distutils was imported before Setuptools, but importing Setuptools "
19        "also replaces the `distutils` module in `sys.modules`. This may lead "
20        "to undesirable behaviors or errors. To avoid these issues, avoid "
21        "using distutils directly, ensure that setuptools is installed in the "
22        "traditional way (e.g. not an editable install), and/or make sure "
23        "that setuptools is always imported before distutils.")
24
25
26def clear_distutils():
27    if 'distutils' not in sys.modules:
28        return
29    import warnings
30    warnings.warn("Setuptools is replacing distutils.")
31    mods = [
32        name for name in sys.modules
33        if name == "distutils" or name.startswith("distutils.")
34    ]
35    for name in mods:
36        del sys.modules[name]
37
38
39def enabled():
40    """
41    Allow selection of distutils by environment variable.
42    """
43    which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'local')
44    return which == 'local'
45
46
47def ensure_local_distutils():
48    import importlib
49    clear_distutils()
50
51    # With the DistutilsMetaFinder in place,
52    # perform an import to cause distutils to be
53    # loaded from setuptools._distutils. Ref #2906.
54    with shim():
55        importlib.import_module('distutils')
56
57    # check that submodules load as expected
58    core = importlib.import_module('distutils.core')
59    assert '_distutils' in core.__file__, core.__file__
60    assert 'setuptools._distutils.log' not in sys.modules
61
62
63def do_override():
64    """
65    Ensure that the local copy of distutils is preferred over stdlib.
66
67    See https://github.com/pypa/setuptools/issues/417#issuecomment-392298401
68    for more motivation.
69    """
70    if enabled():
71        warn_distutils_present()
72        ensure_local_distutils()
73
74
75class _TrivialRe:
76    def __init__(self, *patterns):
77        self._patterns = patterns
78
79    def match(self, string):
80        return all(pat in string for pat in self._patterns)
81
82
83class DistutilsMetaFinder:
84    def find_spec(self, fullname, path, target=None):
85        if path is not None:
86            return
87
88        method_name = 'spec_for_{fullname}'.format(**locals())
89        method = getattr(self, method_name, lambda: None)
90        return method()
91
92    def spec_for_distutils(self):
93        if self.is_cpython():
94            return
95
96        import importlib
97        import importlib.abc
98        import importlib.util
99
100        try:
101            mod = importlib.import_module('setuptools._distutils')
102        except Exception:
103            # There are a couple of cases where setuptools._distutils
104            # may not be present:
105            # - An older Setuptools without a local distutils is
106            #   taking precedence. Ref #2957.
107            # - Path manipulation during sitecustomize removes
108            #   setuptools from the path but only after the hook
109            #   has been loaded. Ref #2980.
110            # In either case, fall back to stdlib behavior.
111            return
112
113        class DistutilsLoader(importlib.abc.Loader):
114
115            def create_module(self, spec):
116                mod.__name__ = 'distutils'
117                return mod
118
119            def exec_module(self, module):
120                pass
121
122        return importlib.util.spec_from_loader(
123            'distutils', DistutilsLoader(), origin=mod.__file__
124        )
125
126    @staticmethod
127    def is_cpython():
128        """
129        Suppress supplying distutils for CPython (build and tests).
130        Ref #2965 and #3007.
131        """
132        return os.path.isfile('pybuilddir.txt')
133
134    def spec_for_pip(self):
135        """
136        Ensure stdlib distutils when running under pip.
137        See pypa/pip#8761 for rationale.
138        """
139        if self.pip_imported_during_build():
140            return
141        clear_distutils()
142        self.spec_for_distutils = lambda: None
143
144    @classmethod
145    def pip_imported_during_build(cls):
146        """
147        Detect if pip is being imported in a build script. Ref #2355.
148        """
149        import traceback
150        return any(
151            cls.frame_file_is_setup(frame)
152            for frame, line in traceback.walk_stack(None)
153        )
154
155    @staticmethod
156    def frame_file_is_setup(frame):
157        """
158        Return True if the indicated frame suggests a setup.py file.
159        """
160        # some frames may not have __file__ (#2940)
161        return frame.f_globals.get('__file__', '').endswith('setup.py')
162
163
164DISTUTILS_FINDER = DistutilsMetaFinder()
165
166
167def add_shim():
168    DISTUTILS_FINDER in sys.meta_path or insert_shim()
169
170
171class shim:
172    def __enter__(self):
173        insert_shim()
174
175    def __exit__(self, exc, value, tb):
176        remove_shim()
177
178
179def insert_shim():
180    sys.meta_path.insert(0, DISTUTILS_FINDER)
181
182
183def remove_shim():
184    try:
185        sys.meta_path.remove(DISTUTILS_FINDER)
186    except ValueError:
187        pass
188