1# don't import any costly modules

2 import sys

3 import os

4

5

6 is_pypy = '__pypy__' in sys.builtin_module_names

7

8

9 def 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

18 warnings.warn(

19 "Distutils was imported before Setuptools, but importing Setuptools "

20 "also replaces the `distutils` module in `sys.modules`. This may lead "

21 "to undesirable behaviors or errors. To avoid these issues, avoid "

22 "using distutils directly, ensure that setuptools is installed in the "

23 "traditional way (e.g. not an editable install), and/or make sure "

24 "that setuptools is always imported before distutils."

25 )

26

27

28 def clear_distutils():

29 if 'distutils' not in sys.modules:

30 return

31 import warnings

32

33 warnings.warn("Setuptools is replacing distutils.")

34 mods = [

35 name

36 for name in sys.modules

37 if name == "distutils" or name.startswith("distutils.")

38 ]

39 for name in mods:

40 del sys.modules[name]

41

42

43 def enabled():

44 """

45 Allow selection of distutils by environment variable.

46 """

47 which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'local')

48 return which == 'local'

49

50

51 def ensure_local_distutils():

52 import importlib

53

54 clear_distutils()

55

56 # With the DistutilsMetaFinder in place,

57 # perform an import to cause distutils to be

58 # loaded from setuptools._distutils. Ref #2906.

59 with shim():

60 importlib.import_module('distutils')

61

62 # check that submodules load as expected

63 core = importlib.import_module('distutils.core')

64 assert '_distutils' in core.__file__, core.__file__

65 assert 'setuptools._distutils.log' not in sys.modules

66

67

68 def do_override():

69 """

70 Ensure that the local copy of distutils is preferred over stdlib.

71

72 See https://github.com/pypa/setuptools/issues/417#issuecomment-392298401

73 for more motivation.

74 """

75 if enabled():

76 warn_distutils_present()

77 ensure_local_distutils()

78

79

80 class _TrivialRe:

81 def __init__(self, *patterns):

82 self._patterns = patterns

83

84 def match(self, string):

85 return all(pat in string for pat in self._patterns)

86

87

88 class DistutilsMetaFinder:

89 def find_spec(self, fullname, path, target=None):

90 # optimization: only consider top level modules and those

91 # found in the CPython test suite.

92 if path is not None and not fullname.startswith('test.'):

93 return

94

95 method_name = 'spec_for_{fullname}'.format(**locals())

96 method = getattr(self, method_name, lambda: None)

97 return method()

98

99 def spec_for_distutils(self):

100 if self.is_cpython():

101 return

102

103 import importlib

104 import importlib.abc

105 import importlib.util

106

107 try:

108 mod = importlib.import_module('setuptools._distutils')

109 except Exception:

110 # There are a couple of cases where setuptools._distutils

111 # may not be present:

112 # - An older Setuptools without a local distutils is

113 # taking precedence. Ref #2957.

114 # - Path manipulation during sitecustomize removes

115 # setuptools from the path but only after the hook

116 # has been loaded. Ref #2980.

117 # In either case, fall back to stdlib behavior.

118 return

119

120 class DistutilsLoader(importlib.abc.Loader):

121 def create_module(self, spec):

122 mod.__name__ = 'distutils'

123 return mod

124

125 def exec_module(self, module):

126 pass

127

128 return importlib.util.spec_from_loader(

129 'distutils', DistutilsLoader(), origin=mod.__file__

130 )

131

132 @staticmethod

133 def is_cpython():

134 """

135 Suppress supplying distutils for CPython (build and tests).

136 Ref #2965 and #3007.

137 """

138 return os.path.isfile('pybuilddir.txt')

139

140 def spec_for_pip(self):

141 """

142 Ensure stdlib distutils when running under pip.

143 See pypa/pip#8761 for rationale.

144 """

145 if self.pip_imported_during_build():

146 return

147 clear_distutils()

148 self.spec_for_distutils = lambda: None

149

150 @classmethod

151 def pip_imported_during_build(cls):

152 """

153 Detect if pip is being imported in a build script. Ref #2355.

154 """

155 import traceback

156

157 return any(

158 cls.frame_file_is_setup(frame) for frame, line in traceback.walk_stack(None)

159 )

160

161 @staticmethod

162 def frame_file_is_setup(frame):

163 """

164 Return True if the indicated frame suggests a setup.py file.

165 """

166 # some frames may not have __file__ (#2940)

167 return frame.f_globals.get('__file__', '').endswith('setup.py')

168

169 def spec_for_sensitive_tests(self):

170 """

171 Ensure stdlib distutils when running select tests under CPython.

172

173 python/cpython#91169

174 """

175 clear_distutils()

176 self.spec_for_distutils = lambda: None

177

178 sensitive_tests = (

179 [

180 'test.test_distutils',

181 'test.test_peg_generator',

182 'test.test_importlib',

183 ]

184 if sys.version_info < (3, 10)

185 else [

186 'test.test_distutils',

187 ]

188 )

189

190

191 for name in DistutilsMetaFinder.sensitive_tests:

192 setattr(

193 DistutilsMetaFinder,

194 f'spec_for_{name}',

195 DistutilsMetaFinder.spec_for_sensitive_tests,

196 )

197

198

199 DISTUTILS_FINDER = DistutilsMetaFinder()

200

201

202 def add_shim():

203 DISTUTILS_FINDER in sys.meta_path or insert_shim()

204

205

206 class shim:

207 def __enter__(self):

208 insert_shim()

209

210 def __exit__(self, exc, value, tb):

211 remove_shim()

212

213

214 def insert_shim():

215 sys.meta_path.insert(0, DISTUTILS_FINDER)

216

217

218 def remove_shim():

219 try:

220 sys.meta_path.remove(DISTUTILS_FINDER)

221 except ValueError:

222 pass