Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pip/_internal/locations/__init__.py: 28%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

163 statements  

1from __future__ import annotations 

2 

3import functools 

4import logging 

5import os 

6import pathlib 

7import sys 

8import sysconfig 

9 

10from pip._internal.models.scheme import SCHEME_KEYS, Scheme 

11from pip._internal.utils.compat import WINDOWS 

12from pip._internal.utils.deprecation import deprecated 

13from pip._internal.utils.virtualenv import running_under_virtualenv 

14 

15from . import _sysconfig 

16from .base import ( 

17 USER_CACHE_DIR, 

18 get_major_minor_version, 

19 get_src_prefix, 

20 is_osx_framework, 

21 site_packages, 

22 user_site, 

23) 

24 

25__all__ = [ 

26 "USER_CACHE_DIR", 

27 "get_bin_prefix", 

28 "get_bin_user", 

29 "get_major_minor_version", 

30 "get_platlib", 

31 "get_purelib", 

32 "get_scheme", 

33 "get_src_prefix", 

34 "site_packages", 

35 "user_site", 

36] 

37 

38 

39logger = logging.getLogger(__name__) 

40 

41 

42_PLATLIBDIR: str = getattr(sys, "platlibdir", "lib") 

43 

44 

45def _should_use_sysconfig() -> bool: 

46 """This function determines the value of _USE_SYSCONFIG. 

47 

48 By default, pip uses sysconfig. 

49 But Python distributors can override this decision by setting: 

50 sysconfig._PIP_USE_SYSCONFIG = True / False 

51 Rationale in https://github.com/pypa/pip/issues/10647 

52 

53 This is a function for testability, but should be constant during any one 

54 run. 

55 """ 

56 return bool(getattr(sysconfig, "_PIP_USE_SYSCONFIG", True)) 

57 

58 

59_USE_SYSCONFIG = _should_use_sysconfig() 

60 

61if not _USE_SYSCONFIG: 

62 # Import distutils lazily to avoid deprecation warnings, 

63 # but import it soon enough that it is in memory and available during 

64 # a pip reinstall. 

65 from . import _distutils 

66 

67# Be noisy about incompatibilities if this platforms "should" be using 

68# sysconfig, but is explicitly opting out and using distutils instead. 

69if _USE_SYSCONFIG: 

70 _MISMATCH_LEVEL = logging.DEBUG 

71else: 

72 _MISMATCH_LEVEL = logging.WARNING 

73 

74 

75def _looks_like_bpo_44860() -> bool: 

76 """The resolution to bpo-44860 will change this incorrect platlib. 

77 

78 See <https://bugs.python.org/issue44860>. 

79 """ 

80 from distutils.command.install import INSTALL_SCHEMES 

81 

82 try: 

83 unix_user_platlib = INSTALL_SCHEMES["unix_user"]["platlib"] 

84 except KeyError: 

85 return False 

86 return unix_user_platlib == "$usersite" 

87 

88 

89def _looks_like_red_hat_patched_platlib_purelib(scheme: dict[str, str]) -> bool: 

90 platlib = scheme["platlib"] 

91 if "/$platlibdir/" in platlib: 

92 platlib = platlib.replace("/$platlibdir/", f"/{_PLATLIBDIR}/") 

93 if "/lib64/" not in platlib: 

94 return False 

95 unpatched = platlib.replace("/lib64/", "/lib/") 

96 return unpatched.replace("$platbase/", "$base/") == scheme["purelib"] 

97 

98 

99@functools.cache 

100def _looks_like_red_hat_lib() -> bool: 

101 """Red Hat patches platlib in unix_prefix and unix_home, but not purelib. 

102 

103 This is the only way I can see to tell a Red Hat-patched Python. 

104 """ 

105 from distutils.command.install import INSTALL_SCHEMES 

106 

107 return all( 

108 k in INSTALL_SCHEMES 

109 and _looks_like_red_hat_patched_platlib_purelib(INSTALL_SCHEMES[k]) 

110 for k in ("unix_prefix", "unix_home") 

111 ) 

112 

113 

114@functools.cache 

115def _looks_like_debian_scheme() -> bool: 

116 """Debian adds two additional schemes.""" 

117 from distutils.command.install import INSTALL_SCHEMES 

118 

119 return "deb_system" in INSTALL_SCHEMES and "unix_local" in INSTALL_SCHEMES 

120 

121 

122@functools.cache 

123def _looks_like_red_hat_scheme() -> bool: 

124 """Red Hat patches ``sys.prefix`` and ``sys.exec_prefix``. 

125 

126 Red Hat's ``00251-change-user-install-location.patch`` changes the install 

127 command's ``prefix`` and ``exec_prefix`` to append ``"/local"``. This is 

128 (fortunately?) done quite unconditionally, so we create a default command 

129 object without any configuration to detect this. 

130 """ 

131 from distutils.command.install import install 

132 from distutils.dist import Distribution 

133 

134 cmd = install(Distribution()) 

135 cmd.finalize_options() 

136 return ( 

137 cmd.exec_prefix == f"{os.path.normpath(sys.exec_prefix)}/local" 

138 and cmd.prefix == f"{os.path.normpath(sys.prefix)}/local" 

139 ) 

140 

141 

142@functools.cache 

143def _looks_like_slackware_scheme() -> bool: 

144 """Slackware patches sysconfig but fails to patch distutils and site. 

145 

146 Slackware changes sysconfig's user scheme to use ``"lib64"`` for the lib 

147 path, but does not do the same to the site module. 

148 """ 

149 if user_site is None: # User-site not available. 

150 return False 

151 try: 

152 paths = sysconfig.get_paths(scheme="posix_user", expand=False) 

153 except KeyError: # User-site not available. 

154 return False 

155 return "/lib64/" in paths["purelib"] and "/lib64/" not in user_site 

156 

157 

158@functools.cache 

159def _looks_like_msys2_mingw_scheme() -> bool: 

160 """MSYS2 patches distutils and sysconfig to use a UNIX-like scheme. 

161 

162 However, MSYS2 incorrectly patches sysconfig ``nt`` scheme. The fix is 

163 likely going to be included in their 3.10 release, so we ignore the warning. 

164 See msys2/MINGW-packages#9319. 

165 

166 MSYS2 MINGW's patch uses lowercase ``"lib"`` instead of the usual uppercase, 

167 and is missing the final ``"site-packages"``. 

168 """ 

169 paths = sysconfig.get_paths("nt", expand=False) 

170 return all( 

171 "Lib" not in p and "lib" in p and not p.endswith("site-packages") 

172 for p in (paths[key] for key in ("platlib", "purelib")) 

173 ) 

174 

175 

176@functools.cache 

177def _warn_mismatched(old: pathlib.Path, new: pathlib.Path, *, key: str) -> None: 

178 issue_url = "https://github.com/pypa/pip/issues/10151" 

179 message = ( 

180 "Value for %s does not match. Please report this to <%s>" 

181 "\ndistutils: %s" 

182 "\nsysconfig: %s" 

183 ) 

184 logger.log(_MISMATCH_LEVEL, message, key, issue_url, old, new) 

185 

186 

187def _warn_if_mismatch(old: pathlib.Path, new: pathlib.Path, *, key: str) -> bool: 

188 if old == new: 

189 return False 

190 _warn_mismatched(old, new, key=key) 

191 return True 

192 

193 

194@functools.cache 

195def _log_context( 

196 *, 

197 user: bool = False, 

198 home: str | None = None, 

199 root: str | None = None, 

200 prefix: str | None = None, 

201) -> None: 

202 parts = [ 

203 "Additional context:", 

204 "user = %r", 

205 "home = %r", 

206 "root = %r", 

207 "prefix = %r", 

208 ] 

209 

210 logger.log(_MISMATCH_LEVEL, "\n".join(parts), user, home, root, prefix) 

211 

212 

213def get_scheme( 

214 dist_name: str, 

215 user: bool = False, 

216 home: str | None = None, 

217 root: str | None = None, 

218 isolated: bool = False, 

219 prefix: str | None = None, 

220) -> Scheme: 

221 new = _sysconfig.get_scheme( 

222 dist_name, 

223 user=user, 

224 home=home, 

225 root=root, 

226 isolated=isolated, 

227 prefix=prefix, 

228 ) 

229 if _USE_SYSCONFIG: 

230 return new 

231 

232 old = _distutils.get_scheme( 

233 dist_name, 

234 user=user, 

235 home=home, 

236 root=root, 

237 isolated=isolated, 

238 prefix=prefix, 

239 ) 

240 

241 warning_contexts = [] 

242 for k in SCHEME_KEYS: 

243 old_v = pathlib.Path(getattr(old, k)) 

244 new_v = pathlib.Path(getattr(new, k)) 

245 

246 if old_v == new_v: 

247 continue 

248 

249 # distutils incorrectly put PyPy packages under ``site-packages/python`` 

250 # in the ``posix_home`` scheme, but PyPy devs said they expect the 

251 # directory name to be ``pypy`` instead. So we treat this as a bug fix 

252 # and not warn about it. See bpo-43307 and python/cpython#24628. 

253 skip_pypy_special_case = ( 

254 sys.implementation.name == "pypy" 

255 and home is not None 

256 and k in ("platlib", "purelib") 

257 and old_v.parent == new_v.parent 

258 and old_v.name.startswith("python") 

259 and new_v.name.startswith("pypy") 

260 ) 

261 if skip_pypy_special_case: 

262 continue 

263 

264 # sysconfig's ``osx_framework_user`` does not include ``pythonX.Y`` in 

265 # the ``include`` value, but distutils's ``headers`` does. We'll let 

266 # CPython decide whether this is a bug or feature. See bpo-43948. 

267 skip_osx_framework_user_special_case = ( 

268 user 

269 and is_osx_framework() 

270 and k == "headers" 

271 and old_v.parent.parent == new_v.parent 

272 and old_v.parent.name.startswith("python") 

273 ) 

274 if skip_osx_framework_user_special_case: 

275 continue 

276 

277 # On Red Hat and derived Linux distributions, distutils is patched to 

278 # use "lib64" instead of "lib" for platlib. 

279 if k == "platlib" and _looks_like_red_hat_lib(): 

280 continue 

281 

282 # sysconfig's posix_user scheme sets platlib against 

283 # sys.platlibdir, but distutils's unix_user incorrectly continues 

284 # using the same $usersite for both platlib and purelib. This creates a 

285 # mismatch when sys.platlibdir is not "lib". 

286 skip_bpo_44860 = ( 

287 user 

288 and k == "platlib" 

289 and not WINDOWS 

290 and _PLATLIBDIR != "lib" 

291 and _looks_like_bpo_44860() 

292 ) 

293 if skip_bpo_44860: 

294 continue 

295 

296 # Slackware incorrectly patches posix_user to use lib64 instead of lib, 

297 # but not usersite to match the location. 

298 skip_slackware_user_scheme = ( 

299 user 

300 and k in ("platlib", "purelib") 

301 and not WINDOWS 

302 and _looks_like_slackware_scheme() 

303 ) 

304 if skip_slackware_user_scheme: 

305 continue 

306 

307 # Both Debian and Red Hat patch Python to place the system site under 

308 # /usr/local instead of /usr. Debian also places lib in dist-packages 

309 # instead of site-packages, but the /usr/local check should cover it. 

310 skip_linux_system_special_case = ( 

311 not (user or home or prefix or running_under_virtualenv()) 

312 and old_v.parts[1:3] == ("usr", "local") 

313 and len(new_v.parts) > 1 

314 and new_v.parts[1] == "usr" 

315 and (len(new_v.parts) < 3 or new_v.parts[2] != "local") 

316 and (_looks_like_red_hat_scheme() or _looks_like_debian_scheme()) 

317 ) 

318 if skip_linux_system_special_case: 

319 continue 

320 

321 # MSYS2 MINGW's sysconfig patch does not include the "site-packages" 

322 # part of the path. This is incorrect and will be fixed in MSYS. 

323 skip_msys2_mingw_bug = ( 

324 WINDOWS and k in ("platlib", "purelib") and _looks_like_msys2_mingw_scheme() 

325 ) 

326 if skip_msys2_mingw_bug: 

327 continue 

328 

329 # CPython's POSIX install script invokes pip (via ensurepip) against the 

330 # interpreter located in the source tree, not the install site. This 

331 # triggers special logic in sysconfig that's not present in distutils. 

332 # https://github.com/python/cpython/blob/8c21941ddaf/Lib/sysconfig.py#L178-L194 

333 skip_cpython_build = ( 

334 sysconfig.is_python_build(check_home=True) 

335 and not WINDOWS 

336 and k in ("headers", "include", "platinclude") 

337 ) 

338 if skip_cpython_build: 

339 continue 

340 

341 warning_contexts.append((old_v, new_v, f"scheme.{k}")) 

342 

343 if not warning_contexts: 

344 return old 

345 

346 # Check if this path mismatch is caused by distutils config files. Those 

347 # files will no longer work once we switch to sysconfig, so this raises a 

348 # deprecation message for them. 

349 default_old = _distutils.distutils_scheme( 

350 dist_name, 

351 user, 

352 home, 

353 root, 

354 isolated, 

355 prefix, 

356 ignore_config_files=True, 

357 ) 

358 if any(default_old[k] != getattr(old, k) for k in SCHEME_KEYS): 

359 deprecated( 

360 reason=( 

361 "Configuring installation scheme with distutils config files " 

362 "is deprecated and will no longer work in the near future. If you " 

363 "are using a Homebrew or Linuxbrew Python, please see discussion " 

364 "at https://github.com/Homebrew/homebrew-core/issues/76621" 

365 ), 

366 replacement=None, 

367 gone_in=None, 

368 ) 

369 return old 

370 

371 # Post warnings about this mismatch so user can report them back. 

372 for old_v, new_v, key in warning_contexts: 

373 _warn_mismatched(old_v, new_v, key=key) 

374 _log_context(user=user, home=home, root=root, prefix=prefix) 

375 

376 return old 

377 

378 

379def get_bin_prefix() -> str: 

380 new = _sysconfig.get_bin_prefix() 

381 if _USE_SYSCONFIG: 

382 return new 

383 

384 old = _distutils.get_bin_prefix() 

385 if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="bin_prefix"): 

386 _log_context() 

387 return old 

388 

389 

390def get_bin_user() -> str: 

391 return _sysconfig.get_scheme("", user=True).scripts 

392 

393 

394def _looks_like_deb_system_dist_packages(value: str) -> bool: 

395 """Check if the value is Debian's APT-controlled dist-packages. 

396 

397 Debian's ``distutils.sysconfig.get_python_lib()`` implementation returns the 

398 default package path controlled by APT, but does not patch ``sysconfig`` to 

399 do the same. This is similar to the bug worked around in ``get_scheme()``, 

400 but here the default is ``deb_system`` instead of ``unix_local``. Ultimately 

401 we can't do anything about this Debian bug, and this detection allows us to 

402 skip the warning when needed. 

403 """ 

404 if not _looks_like_debian_scheme(): 

405 return False 

406 if value == "/usr/lib/python3/dist-packages": 

407 return True 

408 return False 

409 

410 

411def get_purelib() -> str: 

412 """Return the default pure-Python lib location.""" 

413 new = _sysconfig.get_purelib() 

414 if _USE_SYSCONFIG: 

415 return new 

416 

417 old = _distutils.get_purelib() 

418 if _looks_like_deb_system_dist_packages(old): 

419 return old 

420 if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="purelib"): 

421 _log_context() 

422 return old 

423 

424 

425def get_platlib() -> str: 

426 """Return the default platform-shared lib location.""" 

427 new = _sysconfig.get_platlib() 

428 if _USE_SYSCONFIG: 

429 return new 

430 

431 from . import _distutils 

432 

433 old = _distutils.get_platlib() 

434 if _looks_like_deb_system_dist_packages(old): 

435 return old 

436 if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="platlib"): 

437 _log_context() 

438 return old