Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pip/_internal/utils/misc.py: 36%

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

387 statements  

1from __future__ import annotations 

2 

3import errno 

4import getpass 

5import hashlib 

6import logging 

7import os 

8import pathlib 

9import posixpath 

10import shutil 

11import stat 

12import sys 

13import sysconfig 

14import urllib.parse 

15from collections.abc import Callable, Generator, Iterable, Iterator, Mapping, Sequence 

16from dataclasses import dataclass 

17from functools import cache, partial, wraps 

18from io import StringIO 

19from itertools import filterfalse, tee, zip_longest 

20from pathlib import Path 

21from types import FunctionType, TracebackType 

22from typing import ( 

23 Any, 

24 BinaryIO, 

25 NoReturn, 

26 TextIO, 

27 TypeVar, 

28 cast, 

29) 

30 

31from pip._vendor.packaging.requirements import Requirement 

32from pip._vendor.pyproject_hooks import BackendUnavailable, BuildBackendHookCaller 

33 

34from pip import __file__ as pip_location 

35from pip import __version__ 

36from pip._internal.exceptions import ( 

37 BackendUnavailableError, 

38 CommandError, 

39 ExternallyManagedEnvironment, 

40) 

41from pip._internal.locations import get_major_minor_version 

42from pip._internal.utils.compat import WINDOWS 

43from pip._internal.utils.retry import retry 

44from pip._internal.utils.virtualenv import running_under_virtualenv 

45 

46__all__ = [ 

47 "rmtree", 

48 "display_path", 

49 "backup_dir", 

50 "ask", 

51 "splitext", 

52 "format_size", 

53 "is_installable_dir", 

54 "normalize_path", 

55 "renames", 

56 "get_prog", 

57 "ensure_dir", 

58 "remove_auth_from_url", 

59 "check_externally_managed", 

60 "looks_like_ci", 

61 "ConfiguredBuildBackendHookCaller", 

62] 

63 

64logger = logging.getLogger(__name__) 

65 

66T = TypeVar("T") 

67F = TypeVar("F", bound=Callable[..., Any]) 

68ExcInfo = tuple[type[BaseException], BaseException, TracebackType] 

69VersionInfo = tuple[int, int, int] 

70NetlocTuple = tuple[str, tuple[str | None, str | None]] 

71OnExc = Callable[[FunctionType, Path, BaseException], Any] 

72OnErr = Callable[[FunctionType, Path, ExcInfo], Any] 

73 

74FILE_CHUNK_SIZE = 1024 * 1024 

75# These are environment variables present when running under various 

76# CI systems. For each variable, some CI systems that use the variable 

77# are indicated. The collection was chosen so that for each of a number 

78# of popular systems, at least one of the environment variables is used. 

79# This list is used to provide some indication of and lower bound for 

80# CI traffic to PyPI. Thus, it is okay if the list is not comprehensive. 

81# For more background, see: https://github.com/pypa/pip/issues/5499 

82CI_ENVIRONMENT_VARIABLES = ( 

83 # Azure Pipelines 

84 "BUILD_BUILDID", 

85 # Jenkins 

86 "BUILD_ID", 

87 # AppVeyor, CircleCI, Codeship, Gitlab CI, Shippable, Travis CI 

88 "CI", 

89 # Explicit environment variable. 

90 "PIP_IS_CI", 

91) 

92 

93 

94def get_pip_version() -> str: 

95 pip_pkg_dir = os.path.join(os.path.dirname(__file__), "..", "..") 

96 pip_pkg_dir = os.path.abspath(pip_pkg_dir) 

97 

98 return f"pip {__version__} from {pip_pkg_dir} (python {get_major_minor_version()})" 

99 

100 

101@cache 

102def running_under_zipapp() -> bool: 

103 return not pathlib.Path(pip_location).resolve().parent.is_dir() 

104 

105 

106def get_runnable_pip() -> str: 

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

108 

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

110 environment. 

111 """ 

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

113 

114 if running_under_zipapp(): 

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

116 # case, we can use that directly. 

117 return str(source) 

118 

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

120 

121 

122def normalize_version_info(py_version_info: tuple[int, ...]) -> tuple[int, int, int]: 

123 """ 

124 Convert a tuple of ints representing a Python version to one of length 

125 three. 

126 

127 :param py_version_info: a tuple of ints representing a Python version, 

128 or None to specify no version. The tuple can have any length. 

129 

130 :return: a tuple of length three if `py_version_info` is non-None. 

131 Otherwise, return `py_version_info` unchanged (i.e. None). 

132 """ 

133 if len(py_version_info) < 3: 

134 py_version_info += (3 - len(py_version_info)) * (0,) 

135 elif len(py_version_info) > 3: 

136 py_version_info = py_version_info[:3] 

137 

138 return cast("VersionInfo", py_version_info) 

139 

140 

141def ensure_dir(path: str) -> None: 

142 """os.path.makedirs without EEXIST.""" 

143 try: 

144 os.makedirs(path) 

145 except OSError as e: 

146 # Windows can raise spurious ENOTEMPTY errors. See #6426. 

147 if e.errno != errno.EEXIST and e.errno != errno.ENOTEMPTY: 

148 raise 

149 

150 

151def get_prog() -> str: 

152 try: 

153 prog = os.path.basename(sys.argv[0]) 

154 if prog in ("__main__.py", "-c"): 

155 return f"{sys.executable} -m pip" 

156 else: 

157 return prog 

158 except (AttributeError, TypeError, IndexError): 

159 pass 

160 return "pip" 

161 

162 

163# Retry every half second for up to 3 seconds 

164@retry(stop_after_delay=3, wait=0.5) 

165def rmtree(dir: str, ignore_errors: bool = False, onexc: OnExc | None = None) -> None: 

166 if ignore_errors: 

167 onexc = _onerror_ignore 

168 if onexc is None: 

169 onexc = _onerror_reraise 

170 handler: OnErr = partial(rmtree_errorhandler, onexc=onexc) 

171 if sys.version_info >= (3, 12): 

172 # See https://docs.python.org/3.12/whatsnew/3.12.html#shutil. 

173 shutil.rmtree(dir, onexc=handler) # type: ignore 

174 else: 

175 shutil.rmtree(dir, onerror=handler) # type: ignore 

176 

177 

178def _onerror_ignore(*_args: Any) -> None: 

179 pass 

180 

181 

182def _onerror_reraise(*_args: Any) -> None: 

183 raise # noqa: PLE0704 - Bare exception used to reraise existing exception 

184 

185 

186def rmtree_errorhandler( 

187 func: FunctionType, 

188 path: Path, 

189 exc_info: ExcInfo | BaseException, 

190 *, 

191 onexc: OnExc = _onerror_reraise, 

192) -> None: 

193 """ 

194 `rmtree` error handler to 'force' a file remove (i.e. like `rm -f`). 

195 

196 * If a file is readonly then it's write flag is set and operation is 

197 retried. 

198 

199 * `onerror` is the original callback from `rmtree(... onerror=onerror)` 

200 that is chained at the end if the "rm -f" still fails. 

201 """ 

202 try: 

203 st_mode = os.stat(path).st_mode 

204 except OSError: 

205 # it's equivalent to os.path.exists 

206 return 

207 

208 if not st_mode & stat.S_IWRITE: 

209 # convert to read/write 

210 try: 

211 os.chmod(path, st_mode | stat.S_IWRITE) 

212 except OSError: 

213 pass 

214 else: 

215 # use the original function to repeat the operation 

216 try: 

217 func(path) 

218 return 

219 except OSError: 

220 pass 

221 

222 if not isinstance(exc_info, BaseException): 

223 _, exc_info, _ = exc_info 

224 onexc(func, path, exc_info) 

225 

226 

227def display_path(path: str) -> str: 

228 """Gives the display value for a given path, making it relative to cwd 

229 if possible.""" 

230 try: 

231 relative = Path(path).relative_to(Path.cwd()) 

232 except ValueError: 

233 # If the path isn't relative to the CWD, leave it alone 

234 return path 

235 return os.path.join(".", relative) 

236 

237 

238def backup_dir(dir: str, ext: str = ".bak") -> str: 

239 """Figure out the name of a directory to back up the given dir to 

240 (adding .bak, .bak2, etc)""" 

241 n = 1 

242 extension = ext 

243 while os.path.exists(dir + extension): 

244 n += 1 

245 extension = ext + str(n) 

246 return dir + extension 

247 

248 

249def ask_path_exists(message: str, options: Iterable[str]) -> str: 

250 for action in os.environ.get("PIP_EXISTS_ACTION", "").split(): 

251 if action in options: 

252 return action 

253 return ask(message, options) 

254 

255 

256def _check_no_input(message: str) -> None: 

257 """Raise an error if no input is allowed.""" 

258 if os.environ.get("PIP_NO_INPUT"): 

259 raise Exception( 

260 f"No input was expected ($PIP_NO_INPUT set); question: {message}" 

261 ) 

262 

263 

264def ask(message: str, options: Iterable[str]) -> str: 

265 """Ask the message interactively, with the given possible responses""" 

266 while 1: 

267 _check_no_input(message) 

268 response = input(message) 

269 response = response.strip().lower() 

270 if response not in options: 

271 print( 

272 "Your response ({!r}) was not one of the expected responses: " 

273 "{}".format(response, ", ".join(options)) 

274 ) 

275 else: 

276 return response 

277 

278 

279def ask_input(message: str) -> str: 

280 """Ask for input interactively.""" 

281 _check_no_input(message) 

282 return input(message) 

283 

284 

285def ask_password(message: str) -> str: 

286 """Ask for a password interactively.""" 

287 _check_no_input(message) 

288 return getpass.getpass(message) 

289 

290 

291def strtobool(val: str) -> int: 

292 """Convert a string representation of truth to true (1) or false (0). 

293 

294 True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values 

295 are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if 

296 'val' is anything else. 

297 """ 

298 val = val.lower() 

299 if val in ("y", "yes", "t", "true", "on", "1"): 

300 return 1 

301 elif val in ("n", "no", "f", "false", "off", "0"): 

302 return 0 

303 else: 

304 raise ValueError(f"invalid truth value {val!r}") 

305 

306 

307def format_size(bytes: float) -> str: 

308 if bytes > 1000 * 1000: 

309 return f"{bytes / 1000.0 / 1000:.1f} MB" 

310 elif bytes > 10 * 1000: 

311 return f"{int(bytes / 1000)} kB" 

312 elif bytes > 1000: 

313 return f"{bytes / 1000.0:.1f} kB" 

314 else: 

315 return f"{int(bytes)} bytes" 

316 

317 

318def tabulate(rows: Iterable[Iterable[Any]]) -> tuple[list[str], list[int]]: 

319 """Return a list of formatted rows and a list of column sizes. 

320 

321 For example:: 

322 

323 >>> tabulate([['foobar', 2000], [0xdeadbeef]]) 

324 (['foobar 2000', '3735928559'], [10, 4]) 

325 """ 

326 rows = [tuple(map(str, row)) for row in rows] 

327 sizes = [max(map(len, col)) for col in zip_longest(*rows, fillvalue="")] 

328 table = [" ".join(map(str.ljust, row, sizes)).rstrip() for row in rows] 

329 return table, sizes 

330 

331 

332def is_installable_dir(path: str) -> bool: 

333 """Is path is a directory containing pyproject.toml or setup.py? 

334 

335 If pyproject.toml exists, this is a PEP 517 project. Otherwise we look for 

336 a legacy setuptools layout by identifying setup.py. We don't check for the 

337 setup.cfg because using it without setup.py is only available for PEP 517 

338 projects, which are already covered by the pyproject.toml check. 

339 """ 

340 if not os.path.isdir(path): 

341 return False 

342 if os.path.isfile(os.path.join(path, "pyproject.toml")): 

343 return True 

344 if os.path.isfile(os.path.join(path, "setup.py")): 

345 return True 

346 return False 

347 

348 

349def read_chunks( 

350 file: BinaryIO, size: int = FILE_CHUNK_SIZE 

351) -> Generator[bytes, None, None]: 

352 """Yield pieces of data from a file-like object until EOF.""" 

353 while True: 

354 chunk = file.read(size) 

355 if not chunk: 

356 break 

357 yield chunk 

358 

359 

360def normalize_path(path: str, resolve_symlinks: bool = True) -> str: 

361 """ 

362 Convert a path to its canonical, case-normalized, absolute version. 

363 

364 """ 

365 path = os.path.expanduser(path) 

366 if resolve_symlinks: 

367 path = os.path.realpath(path) 

368 else: 

369 path = os.path.abspath(path) 

370 return os.path.normcase(path) 

371 

372 

373def splitext(path: str) -> tuple[str, str]: 

374 """Like os.path.splitext, but take off .tar too""" 

375 base, ext = posixpath.splitext(path) 

376 if base.lower().endswith(".tar"): 

377 ext = base[-4:] + ext 

378 base = base[:-4] 

379 return base, ext 

380 

381 

382def renames(old: str, new: str) -> None: 

383 """Like os.renames(), but handles renaming across devices.""" 

384 # Implementation borrowed from os.renames(). 

385 head, tail = os.path.split(new) 

386 if head and tail and not os.path.exists(head): 

387 os.makedirs(head) 

388 

389 shutil.move(old, new) 

390 

391 head, tail = os.path.split(old) 

392 if head and tail: 

393 try: 

394 os.removedirs(head) 

395 except OSError: 

396 pass 

397 

398 

399def is_local(path: str) -> bool: 

400 """ 

401 Return True if path is within sys.prefix, if we're running in a virtualenv. 

402 

403 If we're not in a virtualenv, all paths are considered "local." 

404 

405 Caution: this function assumes the head of path has been normalized 

406 with normalize_path. 

407 """ 

408 if not running_under_virtualenv(): 

409 return True 

410 return path.startswith(normalize_path(sys.prefix)) 

411 

412 

413def write_output(msg: Any, *args: Any) -> None: 

414 logger.info(msg, *args) 

415 

416 

417class StreamWrapper(StringIO): 

418 orig_stream: TextIO 

419 

420 @classmethod 

421 def from_stream(cls, orig_stream: TextIO) -> StreamWrapper: 

422 ret = cls() 

423 ret.orig_stream = orig_stream 

424 return ret 

425 

426 # compileall.compile_dir() needs stdout.encoding to print to stdout 

427 # type ignore is because TextIOBase.encoding is writeable 

428 @property 

429 def encoding(self) -> str: # type: ignore 

430 return self.orig_stream.encoding 

431 

432 

433# Simulates an enum 

434def enum(*sequential: Any, **named: Any) -> type[Any]: 

435 enums = dict(zip(sequential, range(len(sequential))), **named) 

436 reverse = {value: key for key, value in enums.items()} 

437 enums["reverse_mapping"] = reverse 

438 return type("Enum", (), enums) 

439 

440 

441def build_netloc(host: str, port: int | None) -> str: 

442 """ 

443 Build a netloc from a host-port pair 

444 """ 

445 if port is None: 

446 return host 

447 if ":" in host: 

448 # Only wrap host with square brackets when it is IPv6 

449 host = f"[{host}]" 

450 return f"{host}:{port}" 

451 

452 

453def build_url_from_netloc(netloc: str, scheme: str = "https") -> str: 

454 """ 

455 Build a full URL from a netloc. 

456 """ 

457 if netloc.count(":") >= 2 and "@" not in netloc and "[" not in netloc: 

458 # It must be a bare IPv6 address, so wrap it with brackets. 

459 netloc = f"[{netloc}]" 

460 return f"{scheme}://{netloc}" 

461 

462 

463def parse_netloc(netloc: str) -> tuple[str | None, int | None]: 

464 """ 

465 Return the host-port pair from a netloc. 

466 """ 

467 url = build_url_from_netloc(netloc) 

468 parsed = urllib.parse.urlparse(url) 

469 return parsed.hostname, parsed.port 

470 

471 

472def split_auth_from_netloc(netloc: str) -> NetlocTuple: 

473 """ 

474 Parse out and remove the auth information from a netloc. 

475 

476 Returns: (netloc, (username, password)). 

477 """ 

478 if "@" not in netloc: 

479 return netloc, (None, None) 

480 

481 # Split from the right because that's how urllib.parse.urlsplit() 

482 # behaves if more than one @ is present (which can be checked using 

483 # the password attribute of urlsplit()'s return value). 

484 auth, netloc = netloc.rsplit("@", 1) 

485 pw: str | None = None 

486 if ":" in auth: 

487 # Split from the left because that's how urllib.parse.urlsplit() 

488 # behaves if more than one : is present (which again can be checked 

489 # using the password attribute of the return value) 

490 user, pw = auth.split(":", 1) 

491 else: 

492 user, pw = auth, None 

493 

494 user = urllib.parse.unquote(user) 

495 if pw is not None: 

496 pw = urllib.parse.unquote(pw) 

497 

498 return netloc, (user, pw) 

499 

500 

501def redact_netloc(netloc: str) -> str: 

502 """ 

503 Replace the sensitive data in a netloc with "****", if it exists. 

504 

505 For example: 

506 - "user:pass@example.com" returns "user:****@example.com" 

507 - "accesstoken@example.com" returns "****@example.com" 

508 """ 

509 netloc, (user, password) = split_auth_from_netloc(netloc) 

510 if user is None: 

511 return netloc 

512 if password is None: 

513 user = "****" 

514 password = "" 

515 else: 

516 user = urllib.parse.quote(user) 

517 password = ":****" 

518 return f"{user}{password}@{netloc}" 

519 

520 

521def _transform_url( 

522 url: str, transform_netloc: Callable[[str], tuple[Any, ...]] 

523) -> tuple[str, NetlocTuple]: 

524 """Transform and replace netloc in a url. 

525 

526 transform_netloc is a function taking the netloc and returning a 

527 tuple. The first element of this tuple is the new netloc. The 

528 entire tuple is returned. 

529 

530 Returns a tuple containing the transformed url as item 0 and the 

531 original tuple returned by transform_netloc as item 1. 

532 """ 

533 purl = urllib.parse.urlsplit(url) 

534 netloc_tuple = transform_netloc(purl.netloc) 

535 # stripped url 

536 url_pieces = (purl.scheme, netloc_tuple[0], purl.path, purl.query, purl.fragment) 

537 surl = urllib.parse.urlunsplit(url_pieces) 

538 return surl, cast("NetlocTuple", netloc_tuple) 

539 

540 

541def _get_netloc(netloc: str) -> NetlocTuple: 

542 return split_auth_from_netloc(netloc) 

543 

544 

545def _redact_netloc(netloc: str) -> tuple[str]: 

546 return (redact_netloc(netloc),) 

547 

548 

549def split_auth_netloc_from_url( 

550 url: str, 

551) -> tuple[str, str, tuple[str | None, str | None]]: 

552 """ 

553 Parse a url into separate netloc, auth, and url with no auth. 

554 

555 Returns: (url_without_auth, netloc, (username, password)) 

556 """ 

557 url_without_auth, (netloc, auth) = _transform_url(url, _get_netloc) 

558 return url_without_auth, netloc, auth 

559 

560 

561def remove_auth_from_url(url: str) -> str: 

562 """Return a copy of url with 'username:password@' removed.""" 

563 # username/pass params are passed to subversion through flags 

564 # and are not recognized in the url. 

565 return _transform_url(url, _get_netloc)[0] 

566 

567 

568def redact_auth_from_url(url: str) -> str: 

569 """Replace the password in a given url with ****.""" 

570 return _transform_url(url, _redact_netloc)[0] 

571 

572 

573def redact_auth_from_requirement(req: Requirement) -> str: 

574 """Replace the password in a given requirement url with ****.""" 

575 if not req.url: 

576 return str(req) 

577 return str(req).replace(req.url, redact_auth_from_url(req.url)) 

578 

579 

580@dataclass(frozen=True) 

581class HiddenText: 

582 secret: str 

583 redacted: str 

584 

585 def __repr__(self) -> str: 

586 return f"<HiddenText {str(self)!r}>" 

587 

588 def __str__(self) -> str: 

589 return self.redacted 

590 

591 def __eq__(self, other: object) -> bool: 

592 # Equality is particularly useful for testing. 

593 if type(self) is type(other): 

594 # The string being used for redaction doesn't also have to match, 

595 # just the raw, original string. 

596 return self.secret == other.secret 

597 return NotImplemented 

598 

599 # Disable hashing, since we have a custom __eq__ and don't need hash-ability 

600 # (yet). The only required property of hashing is that objects which compare 

601 # equal have the same hash value. 

602 __hash__ = None # type: ignore[assignment] 

603 

604 

605def hide_value(value: str) -> HiddenText: 

606 return HiddenText(value, redacted="****") 

607 

608 

609def hide_url(url: str) -> HiddenText: 

610 redacted = redact_auth_from_url(url) 

611 return HiddenText(url, redacted=redacted) 

612 

613 

614def protect_pip_from_modification_on_windows(modifying_pip: bool) -> None: 

615 """Protection of pip.exe from modification on Windows 

616 

617 On Windows, any operation modifying pip should be run as: 

618 python -m pip ... 

619 """ 

620 pip_names = [ 

621 "pip", 

622 f"pip{sys.version_info.major}", 

623 f"pip{sys.version_info.major}.{sys.version_info.minor}", 

624 ] 

625 

626 # See https://github.com/pypa/pip/issues/1299 for more discussion 

627 should_show_use_python_msg = ( 

628 modifying_pip and WINDOWS and os.path.basename(sys.argv[0]) in pip_names 

629 ) 

630 

631 if should_show_use_python_msg: 

632 new_command = [sys.executable, "-m", "pip"] + sys.argv[1:] 

633 raise CommandError( 

634 "To modify pip, please run the following command:\n{}".format( 

635 " ".join(new_command) 

636 ) 

637 ) 

638 

639 

640def check_externally_managed() -> None: 

641 """Check whether the current environment is externally managed. 

642 

643 If the ``EXTERNALLY-MANAGED`` config file is found, the current environment 

644 is considered externally managed, and an ExternallyManagedEnvironment is 

645 raised. 

646 """ 

647 if running_under_virtualenv(): 

648 return 

649 marker = os.path.join(sysconfig.get_path("stdlib"), "EXTERNALLY-MANAGED") 

650 if not os.path.isfile(marker): 

651 return 

652 raise ExternallyManagedEnvironment.from_config(marker) 

653 

654 

655def is_console_interactive() -> bool: 

656 """Is this console interactive?""" 

657 return sys.stdin is not None and sys.stdin.isatty() 

658 

659 

660def hash_file(path: str, blocksize: int = 1 << 20) -> tuple[Any, int]: 

661 """Return (hash, length) for path using hashlib.sha256()""" 

662 

663 h = hashlib.sha256() 

664 length = 0 

665 with open(path, "rb") as f: 

666 for block in read_chunks(f, size=blocksize): 

667 length += len(block) 

668 h.update(block) 

669 return h, length 

670 

671 

672def pairwise(iterable: Iterable[Any]) -> Iterator[tuple[Any, Any]]: 

673 """ 

674 Return paired elements. 

675 

676 For example: 

677 s -> (s0, s1), (s2, s3), (s4, s5), ... 

678 """ 

679 iterable = iter(iterable) 

680 return zip_longest(iterable, iterable) 

681 

682 

683def partition( 

684 pred: Callable[[T], bool], iterable: Iterable[T] 

685) -> tuple[Iterable[T], Iterable[T]]: 

686 """ 

687 Use a predicate to partition entries into false entries and true entries, 

688 like 

689 

690 partition(is_odd, range(10)) --> 0 2 4 6 8 and 1 3 5 7 9 

691 """ 

692 t1, t2 = tee(iterable) 

693 return filterfalse(pred, t1), filter(pred, t2) 

694 

695 

696def _handle_backend_unavailable(func: F) -> F: 

697 @wraps(func) 

698 def wrapper( 

699 self: ConfiguredBuildBackendHookCaller, 

700 *args: Any, 

701 **kwargs: Any, 

702 ) -> Any: 

703 try: 

704 return func(self, *args, **kwargs) 

705 except BackendUnavailable as error: 

706 self._raise_backend_unavailable(func.__name__, error) 

707 

708 return cast(F, wrapper) 

709 

710 

711class ConfiguredBuildBackendHookCaller(BuildBackendHookCaller): 

712 def __init__( 

713 self, 

714 config_holder: Any, 

715 source_dir: str, 

716 build_backend: str, 

717 backend_path: str | None = None, 

718 runner: Callable[..., None] | None = None, 

719 python_executable: str | None = None, 

720 ): 

721 super().__init__( 

722 source_dir, build_backend, backend_path, runner, python_executable 

723 ) 

724 self.config_holder = config_holder 

725 

726 def _raise_backend_unavailable( 

727 self, hook_name: str, error: BackendUnavailable 

728 ) -> NoReturn: 

729 backend_error = ( 

730 error.traceback.strip().splitlines()[-1] if error.traceback else str(error) 

731 ) 

732 exception = BackendUnavailableError( 

733 hook_name=hook_name, 

734 backend_name=self.build_backend, 

735 backend_error=backend_error, 

736 ) 

737 logger.error("%s", exception, extra={"rich": True}) 

738 raise exception from error 

739 

740 @_handle_backend_unavailable 

741 def supports_feature(self, feature: str) -> bool: 

742 return feature in self._supported_features() 

743 

744 @_handle_backend_unavailable 

745 def build_wheel( 

746 self, 

747 wheel_directory: str, 

748 config_settings: Mapping[str, Any] | None = None, 

749 metadata_directory: str | None = None, 

750 ) -> str: 

751 cs = self.config_holder.config_settings 

752 return super().build_wheel( 

753 wheel_directory, config_settings=cs, metadata_directory=metadata_directory 

754 ) 

755 

756 @_handle_backend_unavailable 

757 def build_sdist( 

758 self, 

759 sdist_directory: str, 

760 config_settings: Mapping[str, Any] | None = None, 

761 ) -> str: 

762 cs = self.config_holder.config_settings 

763 return super().build_sdist(sdist_directory, config_settings=cs) 

764 

765 @_handle_backend_unavailable 

766 def build_editable( 

767 self, 

768 wheel_directory: str, 

769 config_settings: Mapping[str, Any] | None = None, 

770 metadata_directory: str | None = None, 

771 ) -> str: 

772 cs = self.config_holder.config_settings 

773 return super().build_editable( 

774 wheel_directory, config_settings=cs, metadata_directory=metadata_directory 

775 ) 

776 

777 @_handle_backend_unavailable 

778 def get_requires_for_build_wheel( 

779 self, config_settings: Mapping[str, Any] | None = None 

780 ) -> Sequence[str]: 

781 cs = self.config_holder.config_settings 

782 return super().get_requires_for_build_wheel(config_settings=cs) 

783 

784 @_handle_backend_unavailable 

785 def get_requires_for_build_sdist( 

786 self, config_settings: Mapping[str, Any] | None = None 

787 ) -> Sequence[str]: 

788 cs = self.config_holder.config_settings 

789 return super().get_requires_for_build_sdist(config_settings=cs) 

790 

791 @_handle_backend_unavailable 

792 def get_requires_for_build_editable( 

793 self, config_settings: Mapping[str, Any] | None = None 

794 ) -> Sequence[str]: 

795 cs = self.config_holder.config_settings 

796 return super().get_requires_for_build_editable(config_settings=cs) 

797 

798 @_handle_backend_unavailable 

799 def prepare_metadata_for_build_wheel( 

800 self, 

801 metadata_directory: str, 

802 config_settings: Mapping[str, Any] | None = None, 

803 _allow_fallback: bool = True, 

804 ) -> str: 

805 cs = self.config_holder.config_settings 

806 return super().prepare_metadata_for_build_wheel( 

807 metadata_directory=metadata_directory, 

808 config_settings=cs, 

809 _allow_fallback=_allow_fallback, 

810 ) 

811 

812 @_handle_backend_unavailable 

813 def prepare_metadata_for_build_editable( 

814 self, 

815 metadata_directory: str, 

816 config_settings: Mapping[str, Any] | None = None, 

817 _allow_fallback: bool = True, 

818 ) -> str | None: 

819 cs = self.config_holder.config_settings 

820 return super().prepare_metadata_for_build_editable( 

821 metadata_directory=metadata_directory, 

822 config_settings=cs, 

823 _allow_fallback=_allow_fallback, 

824 ) 

825 

826 

827def warn_if_run_as_root() -> None: 

828 """Output a warning for sudo users on Unix. 

829 

830 In a virtual environment, sudo pip still writes to virtualenv. 

831 On Windows, users may run pip as Administrator without issues. 

832 This warning only applies to Unix root users outside of virtualenv. 

833 """ 

834 if running_under_virtualenv(): 

835 return 

836 if not hasattr(os, "getuid"): 

837 return 

838 # On Windows, there are no "system managed" Python packages. Installing as 

839 # Administrator via pip is the correct way of updating system environments. 

840 # 

841 # We choose sys.platform over utils.compat.WINDOWS here to enable Mypy platform 

842 # checks: https://mypy.readthedocs.io/en/stable/common_issues.html 

843 if sys.platform == "win32" or sys.platform == "cygwin": 

844 return 

845 

846 if os.getuid() != 0: 

847 return 

848 

849 logger.warning( 

850 "Running pip as the 'root' user can result in broken permissions and " 

851 "conflicting behaviour with the system package manager, possibly " 

852 "rendering your system unusable. " 

853 "It is recommended to use a virtual environment instead: " 

854 "https://pip.pypa.io/warnings/venv. " 

855 "Use the --root-user-action option if you know what you are doing and " 

856 "want to suppress this warning." 

857 ) 

858 

859 

860def looks_like_ci() -> bool: 

861 """ 

862 Return whether it looks like pip is running under CI. 

863 """ 

864 # We don't use the method of checking for a tty (e.g. using isatty()) 

865 # because some CI systems mimic a tty (e.g. Travis CI). Thus that 

866 # method doesn't provide definitive information in either direction. 

867 return any(name in os.environ for name in CI_ENVIRONMENT_VARIABLES)