Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pip/_internal/build_env.py: 30%

132 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:48 +0000

1"""Build Environment used for isolation during sdist building 

2""" 

3 

4import logging 

5import os 

6import pathlib 

7import site 

8import sys 

9import textwrap 

10from collections import OrderedDict 

11from types import TracebackType 

12from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple, Type, Union 

13 

14from pip._vendor.certifi import where 

15from pip._vendor.packaging.requirements import Requirement 

16from pip._vendor.packaging.version import Version 

17 

18from pip import __file__ as pip_location 

19from pip._internal.cli.spinners import open_spinner 

20from pip._internal.locations import get_platlib, get_purelib, get_scheme 

21from pip._internal.metadata import get_default_environment, get_environment 

22from pip._internal.utils.subprocess import call_subprocess 

23from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds 

24 

25if TYPE_CHECKING: 

26 from pip._internal.index.package_finder import PackageFinder 

27 

28logger = logging.getLogger(__name__) 

29 

30 

31def _dedup(a: str, b: str) -> Union[Tuple[str], Tuple[str, str]]: 

32 return (a, b) if a != b else (a,) 

33 

34 

35class _Prefix: 

36 def __init__(self, path: str) -> None: 

37 self.path = path 

38 self.setup = False 

39 scheme = get_scheme("", prefix=path) 

40 self.bin_dir = scheme.scripts 

41 self.lib_dirs = _dedup(scheme.purelib, scheme.platlib) 

42 

43 

44def get_runnable_pip() -> str: 

45 """Get a file to pass to a Python executable, to run the currently-running pip. 

46 

47 This is used to run a pip subprocess, for installing requirements into the build 

48 environment. 

49 """ 

50 source = pathlib.Path(pip_location).resolve().parent 

51 

52 if not source.is_dir(): 

53 # This would happen if someone is using pip from inside a zip file. In that 

54 # case, we can use that directly. 

55 return str(source) 

56 

57 return os.fsdecode(source / "__pip-runner__.py") 

58 

59 

60def _get_system_sitepackages() -> Set[str]: 

61 """Get system site packages 

62 

63 Usually from site.getsitepackages, 

64 but fallback on `get_purelib()/get_platlib()` if unavailable 

65 (e.g. in a virtualenv created by virtualenv<20) 

66 

67 Returns normalized set of strings. 

68 """ 

69 if hasattr(site, "getsitepackages"): 

70 system_sites = site.getsitepackages() 

71 else: 

72 # virtualenv < 20 overwrites site.py without getsitepackages 

73 # fallback on get_purelib/get_platlib. 

74 # this is known to miss things, but shouldn't in the cases 

75 # where getsitepackages() has been removed (inside a virtualenv) 

76 system_sites = [get_purelib(), get_platlib()] 

77 return {os.path.normcase(path) for path in system_sites} 

78 

79 

80class BuildEnvironment: 

81 """Creates and manages an isolated environment to install build deps""" 

82 

83 def __init__(self) -> None: 

84 temp_dir = TempDirectory(kind=tempdir_kinds.BUILD_ENV, globally_managed=True) 

85 

86 self._prefixes = OrderedDict( 

87 (name, _Prefix(os.path.join(temp_dir.path, name))) 

88 for name in ("normal", "overlay") 

89 ) 

90 

91 self._bin_dirs: List[str] = [] 

92 self._lib_dirs: List[str] = [] 

93 for prefix in reversed(list(self._prefixes.values())): 

94 self._bin_dirs.append(prefix.bin_dir) 

95 self._lib_dirs.extend(prefix.lib_dirs) 

96 

97 # Customize site to: 

98 # - ensure .pth files are honored 

99 # - prevent access to system site packages 

100 system_sites = _get_system_sitepackages() 

101 

102 self._site_dir = os.path.join(temp_dir.path, "site") 

103 if not os.path.exists(self._site_dir): 

104 os.mkdir(self._site_dir) 

105 with open( 

106 os.path.join(self._site_dir, "sitecustomize.py"), "w", encoding="utf-8" 

107 ) as fp: 

108 fp.write( 

109 textwrap.dedent( 

110 """ 

111 import os, site, sys 

112 

113 # First, drop system-sites related paths. 

114 original_sys_path = sys.path[:] 

115 known_paths = set() 

116 for path in {system_sites!r}: 

117 site.addsitedir(path, known_paths=known_paths) 

118 system_paths = set( 

119 os.path.normcase(path) 

120 for path in sys.path[len(original_sys_path):] 

121 ) 

122 original_sys_path = [ 

123 path for path in original_sys_path 

124 if os.path.normcase(path) not in system_paths 

125 ] 

126 sys.path = original_sys_path 

127 

128 # Second, add lib directories. 

129 # ensuring .pth file are processed. 

130 for path in {lib_dirs!r}: 

131 assert not path in sys.path 

132 site.addsitedir(path) 

133 """ 

134 ).format(system_sites=system_sites, lib_dirs=self._lib_dirs) 

135 ) 

136 

137 def __enter__(self) -> None: 

138 self._save_env = { 

139 name: os.environ.get(name, None) 

140 for name in ("PATH", "PYTHONNOUSERSITE", "PYTHONPATH") 

141 } 

142 

143 path = self._bin_dirs[:] 

144 old_path = self._save_env["PATH"] 

145 if old_path: 

146 path.extend(old_path.split(os.pathsep)) 

147 

148 pythonpath = [self._site_dir] 

149 

150 os.environ.update( 

151 { 

152 "PATH": os.pathsep.join(path), 

153 "PYTHONNOUSERSITE": "1", 

154 "PYTHONPATH": os.pathsep.join(pythonpath), 

155 } 

156 ) 

157 

158 def __exit__( 

159 self, 

160 exc_type: Optional[Type[BaseException]], 

161 exc_val: Optional[BaseException], 

162 exc_tb: Optional[TracebackType], 

163 ) -> None: 

164 for varname, old_value in self._save_env.items(): 

165 if old_value is None: 

166 os.environ.pop(varname, None) 

167 else: 

168 os.environ[varname] = old_value 

169 

170 def check_requirements( 

171 self, reqs: Iterable[str] 

172 ) -> Tuple[Set[Tuple[str, str]], Set[str]]: 

173 """Return 2 sets: 

174 - conflicting requirements: set of (installed, wanted) reqs tuples 

175 - missing requirements: set of reqs 

176 """ 

177 missing = set() 

178 conflicting = set() 

179 if reqs: 

180 env = ( 

181 get_environment(self._lib_dirs) 

182 if hasattr(self, "_lib_dirs") 

183 else get_default_environment() 

184 ) 

185 for req_str in reqs: 

186 req = Requirement(req_str) 

187 # We're explicitly evaluating with an empty extra value, since build 

188 # environments are not provided any mechanism to select specific extras. 

189 if req.marker is not None and not req.marker.evaluate({"extra": ""}): 

190 continue 

191 dist = env.get_distribution(req.name) 

192 if not dist: 

193 missing.add(req_str) 

194 continue 

195 if isinstance(dist.version, Version): 

196 installed_req_str = f"{req.name}=={dist.version}" 

197 else: 

198 installed_req_str = f"{req.name}==={dist.version}" 

199 if not req.specifier.contains(dist.version, prereleases=True): 

200 conflicting.add((installed_req_str, req_str)) 

201 # FIXME: Consider direct URL? 

202 return conflicting, missing 

203 

204 def install_requirements( 

205 self, 

206 finder: "PackageFinder", 

207 requirements: Iterable[str], 

208 prefix_as_string: str, 

209 *, 

210 kind: str, 

211 ) -> None: 

212 prefix = self._prefixes[prefix_as_string] 

213 assert not prefix.setup 

214 prefix.setup = True 

215 if not requirements: 

216 return 

217 self._install_requirements( 

218 get_runnable_pip(), 

219 finder, 

220 requirements, 

221 prefix, 

222 kind=kind, 

223 ) 

224 

225 @staticmethod 

226 def _install_requirements( 

227 pip_runnable: str, 

228 finder: "PackageFinder", 

229 requirements: Iterable[str], 

230 prefix: _Prefix, 

231 *, 

232 kind: str, 

233 ) -> None: 

234 args: List[str] = [ 

235 sys.executable, 

236 pip_runnable, 

237 "install", 

238 "--ignore-installed", 

239 "--no-user", 

240 "--prefix", 

241 prefix.path, 

242 "--no-warn-script-location", 

243 ] 

244 if logger.getEffectiveLevel() <= logging.DEBUG: 

245 args.append("-v") 

246 for format_control in ("no_binary", "only_binary"): 

247 formats = getattr(finder.format_control, format_control) 

248 args.extend( 

249 ( 

250 "--" + format_control.replace("_", "-"), 

251 ",".join(sorted(formats or {":none:"})), 

252 ) 

253 ) 

254 

255 index_urls = finder.index_urls 

256 if index_urls: 

257 args.extend(["-i", index_urls[0]]) 

258 for extra_index in index_urls[1:]: 

259 args.extend(["--extra-index-url", extra_index]) 

260 else: 

261 args.append("--no-index") 

262 for link in finder.find_links: 

263 args.extend(["--find-links", link]) 

264 

265 for host in finder.trusted_hosts: 

266 args.extend(["--trusted-host", host]) 

267 if finder.allow_all_prereleases: 

268 args.append("--pre") 

269 if finder.prefer_binary: 

270 args.append("--prefer-binary") 

271 args.append("--") 

272 args.extend(requirements) 

273 extra_environ = {"_PIP_STANDALONE_CERT": where()} 

274 with open_spinner(f"Installing {kind}") as spinner: 

275 call_subprocess( 

276 args, 

277 command_desc=f"pip subprocess to install {kind}", 

278 spinner=spinner, 

279 extra_environ=extra_environ, 

280 ) 

281 

282 

283class NoOpBuildEnvironment(BuildEnvironment): 

284 """A no-op drop-in replacement for BuildEnvironment""" 

285 

286 def __init__(self) -> None: 

287 pass 

288 

289 def __enter__(self) -> None: 

290 pass 

291 

292 def __exit__( 

293 self, 

294 exc_type: Optional[Type[BaseException]], 

295 exc_val: Optional[BaseException], 

296 exc_tb: Optional[TracebackType], 

297 ) -> None: 

298 pass 

299 

300 def cleanup(self) -> None: 

301 pass 

302 

303 def install_requirements( 

304 self, 

305 finder: "PackageFinder", 

306 requirements: Iterable[str], 

307 prefix_as_string: str, 

308 *, 

309 kind: str, 

310 ) -> None: 

311 raise NotImplementedError()