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

164 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_USE_SYSCONFIG_DEFAULT = sys.version_info >= (3, 10) 

45 

46 

47def _should_use_sysconfig() -> bool: 

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

49 

50 By default, pip uses sysconfig on Python 3.10+. 

51 But Python distributors can override this decision by setting: 

52 sysconfig._PIP_USE_SYSCONFIG = True / False 

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

54 

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

56 run. 

57 """ 

58 return bool(getattr(sysconfig, "_PIP_USE_SYSCONFIG", _USE_SYSCONFIG_DEFAULT)) 

59 

60 

61_USE_SYSCONFIG = _should_use_sysconfig() 

62 

63if not _USE_SYSCONFIG: 

64 # Import distutils lazily to avoid deprecation warnings, 

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

66 # a pip reinstall. 

67 from . import _distutils 

68 

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

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

71if _USE_SYSCONFIG_DEFAULT and not _USE_SYSCONFIG: 

72 _MISMATCH_LEVEL = logging.WARNING 

73else: 

74 _MISMATCH_LEVEL = logging.DEBUG 

75 

76 

77def _looks_like_bpo_44860() -> bool: 

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

79 

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

81 """ 

82 from distutils.command.install import INSTALL_SCHEMES 

83 

84 try: 

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

86 except KeyError: 

87 return False 

88 return unix_user_platlib == "$usersite" 

89 

90 

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

92 platlib = scheme["platlib"] 

93 if "/$platlibdir/" in platlib: 

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

95 if "/lib64/" not in platlib: 

96 return False 

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

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

99 

100 

101@functools.cache 

102def _looks_like_red_hat_lib() -> bool: 

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

104 

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

106 """ 

107 from distutils.command.install import INSTALL_SCHEMES 

108 

109 return all( 

110 k in INSTALL_SCHEMES 

111 and _looks_like_red_hat_patched_platlib_purelib(INSTALL_SCHEMES[k]) 

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

113 ) 

114 

115 

116@functools.cache 

117def _looks_like_debian_scheme() -> bool: 

118 """Debian adds two additional schemes.""" 

119 from distutils.command.install import INSTALL_SCHEMES 

120 

121 return "deb_system" in INSTALL_SCHEMES and "unix_local" in INSTALL_SCHEMES 

122 

123 

124@functools.cache 

125def _looks_like_red_hat_scheme() -> bool: 

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

127 

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

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

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

131 object without any configuration to detect this. 

132 """ 

133 from distutils.command.install import install 

134 from distutils.dist import Distribution 

135 

136 cmd = install(Distribution()) 

137 cmd.finalize_options() 

138 return ( 

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

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

141 ) 

142 

143 

144@functools.cache 

145def _looks_like_slackware_scheme() -> bool: 

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

147 

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

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

150 """ 

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

152 return False 

153 try: 

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

155 except KeyError: # User-site not available. 

156 return False 

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

158 

159 

160@functools.cache 

161def _looks_like_msys2_mingw_scheme() -> bool: 

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

163 

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

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

166 See msys2/MINGW-packages#9319. 

167 

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

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

170 """ 

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

172 return all( 

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

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

175 ) 

176 

177 

178@functools.cache 

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

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

181 message = ( 

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

183 "\ndistutils: %s" 

184 "\nsysconfig: %s" 

185 ) 

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

187 

188 

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

190 if old == new: 

191 return False 

192 _warn_mismatched(old, new, key=key) 

193 return True 

194 

195 

196@functools.cache 

197def _log_context( 

198 *, 

199 user: bool = False, 

200 home: str | None = None, 

201 root: str | None = None, 

202 prefix: str | None = None, 

203) -> None: 

204 parts = [ 

205 "Additional context:", 

206 "user = %r", 

207 "home = %r", 

208 "root = %r", 

209 "prefix = %r", 

210 ] 

211 

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

213 

214 

215def get_scheme( 

216 dist_name: str, 

217 user: bool = False, 

218 home: str | None = None, 

219 root: str | None = None, 

220 isolated: bool = False, 

221 prefix: str | None = None, 

222) -> Scheme: 

223 new = _sysconfig.get_scheme( 

224 dist_name, 

225 user=user, 

226 home=home, 

227 root=root, 

228 isolated=isolated, 

229 prefix=prefix, 

230 ) 

231 if _USE_SYSCONFIG: 

232 return new 

233 

234 old = _distutils.get_scheme( 

235 dist_name, 

236 user=user, 

237 home=home, 

238 root=root, 

239 isolated=isolated, 

240 prefix=prefix, 

241 ) 

242 

243 warning_contexts = [] 

244 for k in SCHEME_KEYS: 

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

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

247 

248 if old_v == new_v: 

249 continue 

250 

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

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

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

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

255 skip_pypy_special_case = ( 

256 sys.implementation.name == "pypy" 

257 and home is not None 

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

259 and old_v.parent == new_v.parent 

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

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

262 ) 

263 if skip_pypy_special_case: 

264 continue 

265 

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

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

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

269 skip_osx_framework_user_special_case = ( 

270 user 

271 and is_osx_framework() 

272 and k == "headers" 

273 and old_v.parent.parent == new_v.parent 

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

275 ) 

276 if skip_osx_framework_user_special_case: 

277 continue 

278 

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

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

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

282 continue 

283 

284 # On Python 3.9+, sysconfig's posix_user scheme sets platlib against 

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

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

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

288 skip_bpo_44860 = ( 

289 user 

290 and k == "platlib" 

291 and not WINDOWS 

292 and _PLATLIBDIR != "lib" 

293 and _looks_like_bpo_44860() 

294 ) 

295 if skip_bpo_44860: 

296 continue 

297 

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

299 # but not usersite to match the location. 

300 skip_slackware_user_scheme = ( 

301 user 

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

303 and not WINDOWS 

304 and _looks_like_slackware_scheme() 

305 ) 

306 if skip_slackware_user_scheme: 

307 continue 

308 

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

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

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

312 skip_linux_system_special_case = ( 

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

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

315 and len(new_v.parts) > 1 

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

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

318 and (_looks_like_red_hat_scheme() or _looks_like_debian_scheme()) 

319 ) 

320 if skip_linux_system_special_case: 

321 continue 

322 

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

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

325 skip_msys2_mingw_bug = ( 

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

327 ) 

328 if skip_msys2_mingw_bug: 

329 continue 

330 

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

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

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

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

335 skip_cpython_build = ( 

336 sysconfig.is_python_build(check_home=True) 

337 and not WINDOWS 

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

339 ) 

340 if skip_cpython_build: 

341 continue 

342 

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

344 

345 if not warning_contexts: 

346 return old 

347 

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

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

350 # deprecation message for them. 

351 default_old = _distutils.distutils_scheme( 

352 dist_name, 

353 user, 

354 home, 

355 root, 

356 isolated, 

357 prefix, 

358 ignore_config_files=True, 

359 ) 

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

361 deprecated( 

362 reason=( 

363 "Configuring installation scheme with distutils config files " 

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

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

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

367 ), 

368 replacement=None, 

369 gone_in=None, 

370 ) 

371 return old 

372 

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

374 for old_v, new_v, key in warning_contexts: 

375 _warn_mismatched(old_v, new_v, key=key) 

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

377 

378 return old 

379 

380 

381def get_bin_prefix() -> str: 

382 new = _sysconfig.get_bin_prefix() 

383 if _USE_SYSCONFIG: 

384 return new 

385 

386 old = _distutils.get_bin_prefix() 

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

388 _log_context() 

389 return old 

390 

391 

392def get_bin_user() -> str: 

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

394 

395 

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

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

398 

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

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

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

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

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

404 skip the warning when needed. 

405 """ 

406 if not _looks_like_debian_scheme(): 

407 return False 

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

409 return True 

410 return False 

411 

412 

413def get_purelib() -> str: 

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

415 new = _sysconfig.get_purelib() 

416 if _USE_SYSCONFIG: 

417 return new 

418 

419 old = _distutils.get_purelib() 

420 if _looks_like_deb_system_dist_packages(old): 

421 return old 

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

423 _log_context() 

424 return old 

425 

426 

427def get_platlib() -> str: 

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

429 new = _sysconfig.get_platlib() 

430 if _USE_SYSCONFIG: 

431 return new 

432 

433 from . import _distutils 

434 

435 old = _distutils.get_platlib() 

436 if _looks_like_deb_system_dist_packages(old): 

437 return old 

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

439 _log_context() 

440 return old