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

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

385 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.deprecation import build_backend_warnings 

44from pip._internal.utils.retry import retry 

45from pip._internal.utils.virtualenv import running_under_virtualenv 

46 

47__all__ = [ 

48 "rmtree", 

49 "display_path", 

50 "backup_dir", 

51 "ask", 

52 "splitext", 

53 "format_size", 

54 "is_installable_dir", 

55 "normalize_path", 

56 "renames", 

57 "get_prog", 

58 "ensure_dir", 

59 "remove_auth_from_url", 

60 "check_externally_managed", 

61 "looks_like_ci", 

62 "ConfiguredBuildBackendHookCaller", 

63] 

64 

65logger = logging.getLogger(__name__) 

66 

67T = TypeVar("T") 

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

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

70VersionInfo = tuple[int, int, int] 

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

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

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

74 

75FILE_CHUNK_SIZE = 1024 * 1024 

76# These are environment variables present when running under various 

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

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

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

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

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

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

83CI_ENVIRONMENT_VARIABLES = ( 

84 # Azure Pipelines 

85 "BUILD_BUILDID", 

86 # Jenkins 

87 "BUILD_ID", 

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

89 "CI", 

90 # Explicit environment variable. 

91 "PIP_IS_CI", 

92) 

93 

94 

95def get_pip_version() -> str: 

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

97 pip_pkg_dir = os.path.abspath(pip_pkg_dir) 

98 

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

100 

101 

102@cache 

103def running_under_zipapp() -> bool: 

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

105 

106 

107def get_runnable_pip() -> str: 

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

109 

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

111 environment. 

112 """ 

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

114 

115 if running_under_zipapp(): 

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

117 # case, we can use that directly. 

118 return str(source) 

119 

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

121 

122 

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

124 """ 

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

126 three. 

127 

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

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

130 

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

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

133 """ 

134 if len(py_version_info) < 3: 

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

136 elif len(py_version_info) > 3: 

137 py_version_info = py_version_info[:3] 

138 

139 return cast("VersionInfo", py_version_info) 

140 

141 

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

143 """os.path.makedirs without EEXIST.""" 

144 try: 

145 os.makedirs(path) 

146 except OSError as e: 

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

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

149 raise 

150 

151 

152def get_prog() -> str: 

153 try: 

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

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

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

157 else: 

158 return prog 

159 except (AttributeError, TypeError, IndexError): 

160 pass 

161 return "pip" 

162 

163 

164# Retry every half second for up to 3 seconds 

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

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

167 if ignore_errors: 

168 onexc = _onerror_ignore 

169 if onexc is None: 

170 onexc = _onerror_reraise 

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

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

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

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

175 else: 

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

177 

178 

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

180 pass 

181 

182 

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

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

185 

186 

187def rmtree_errorhandler( 

188 func: FunctionType, 

189 path: Path, 

190 exc_info: ExcInfo | BaseException, 

191 *, 

192 onexc: OnExc = _onerror_reraise, 

193) -> None: 

194 """ 

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

196 

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

198 retried. 

199 

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

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

202 """ 

203 try: 

204 st_mode = os.stat(path).st_mode 

205 except OSError: 

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

207 return 

208 

209 if not st_mode & stat.S_IWRITE: 

210 # convert to read/write 

211 try: 

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

213 except OSError: 

214 pass 

215 else: 

216 # use the original function to repeat the operation 

217 try: 

218 func(path) 

219 return 

220 except OSError: 

221 pass 

222 

223 if not isinstance(exc_info, BaseException): 

224 _, exc_info, _ = exc_info 

225 onexc(func, path, exc_info) 

226 

227 

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

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

230 if possible.""" 

231 try: 

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

233 except ValueError: 

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

235 return path 

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

237 

238 

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

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

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

242 n = 1 

243 extension = ext 

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

245 n += 1 

246 extension = ext + str(n) 

247 return dir + extension 

248 

249 

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

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

252 if action in options: 

253 return action 

254 return ask(message, options) 

255 

256 

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

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

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

260 raise Exception( 

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

262 ) 

263 

264 

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

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

267 while 1: 

268 _check_no_input(message) 

269 response = input(message) 

270 response = response.strip().lower() 

271 if response not in options: 

272 print( 

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

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

275 ) 

276 else: 

277 return response 

278 

279 

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

281 """Ask for input interactively.""" 

282 _check_no_input(message) 

283 return input(message) 

284 

285 

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

287 """Ask for a password interactively.""" 

288 _check_no_input(message) 

289 return getpass.getpass(message) 

290 

291 

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

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

294 

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

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

297 'val' is anything else. 

298 """ 

299 val = val.lower() 

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

301 return 1 

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

303 return 0 

304 else: 

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

306 

307 

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

309 if bytes > 1000 * 1000: 

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

311 elif bytes > 10 * 1000: 

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

313 elif bytes > 1000: 

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

315 else: 

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

317 

318 

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

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

321 

322 For example:: 

323 

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

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

326 """ 

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

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

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

330 return table, sizes 

331 

332 

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

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

335 

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

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

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

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

340 """ 

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

342 return False 

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

344 return True 

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

346 return True 

347 return False 

348 

349 

350def read_chunks( 

351 file: BinaryIO, size: int = FILE_CHUNK_SIZE 

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

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

354 while True: 

355 chunk = file.read(size) 

356 if not chunk: 

357 break 

358 yield chunk 

359 

360 

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

362 """ 

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

364 

365 """ 

366 path = os.path.expanduser(path) 

367 if resolve_symlinks: 

368 path = os.path.realpath(path) 

369 else: 

370 path = os.path.abspath(path) 

371 return os.path.normcase(path) 

372 

373 

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

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

376 base, ext = posixpath.splitext(path) 

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

378 ext = base[-4:] + ext 

379 base = base[:-4] 

380 return base, ext 

381 

382 

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

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

385 # Implementation borrowed from os.renames(). 

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

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

388 os.makedirs(head) 

389 

390 shutil.move(old, new) 

391 

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

393 if head and tail: 

394 try: 

395 os.removedirs(head) 

396 except OSError: 

397 pass 

398 

399 

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

401 """ 

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

403 

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

405 

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

407 with normalize_path. 

408 """ 

409 if not running_under_virtualenv(): 

410 return True 

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

412 

413 

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

415 logger.info(msg, *args) 

416 

417 

418class StreamWrapper(StringIO): 

419 orig_stream: TextIO 

420 

421 @classmethod 

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

423 ret = cls() 

424 ret.orig_stream = orig_stream 

425 return ret 

426 

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

428 # type ignore is because TextIOBase.encoding is writeable 

429 @property 

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

431 return self.orig_stream.encoding 

432 

433 

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

435 """ 

436 Build a netloc from a host-port pair 

437 """ 

438 if port is None: 

439 return host 

440 if ":" in host: 

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

442 host = f"[{host}]" 

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

444 

445 

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

447 """ 

448 Build a full URL from a netloc. 

449 """ 

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

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

452 netloc = f"[{netloc}]" 

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

454 

455 

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

457 """ 

458 Return the host-port pair from a netloc. 

459 """ 

460 url = build_url_from_netloc(netloc) 

461 parsed = urllib.parse.urlparse(url) 

462 return parsed.hostname, parsed.port 

463 

464 

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

466 """ 

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

468 

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

470 """ 

471 if "@" not in netloc: 

472 return netloc, (None, None) 

473 

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

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

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

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

478 pw: str | None = None 

479 if ":" in auth: 

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

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

482 # using the password attribute of the return value) 

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

484 else: 

485 user, pw = auth, None 

486 

487 user = urllib.parse.unquote(user) 

488 if pw is not None: 

489 pw = urllib.parse.unquote(pw) 

490 

491 return netloc, (user, pw) 

492 

493 

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

495 """ 

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

497 

498 For example: 

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

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

501 """ 

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

503 if user is None: 

504 return netloc 

505 if password is None: 

506 user = "****" 

507 password = "" 

508 else: 

509 user = urllib.parse.quote(user) 

510 password = ":****" 

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

512 

513 

514def _transform_url( 

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

516) -> tuple[str, NetlocTuple]: 

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

518 

519 transform_netloc is a function taking the netloc and returning a 

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

521 entire tuple is returned. 

522 

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

524 original tuple returned by transform_netloc as item 1. 

525 """ 

526 purl = urllib.parse.urlsplit(url) 

527 netloc_tuple = transform_netloc(purl.netloc) 

528 # stripped url 

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

530 surl = urllib.parse.urlunsplit(url_pieces) 

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

532 

533 

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

535 return split_auth_from_netloc(netloc) 

536 

537 

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

539 return (redact_netloc(netloc),) 

540 

541 

542def split_auth_netloc_from_url( 

543 url: str, 

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

545 """ 

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

547 

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

549 """ 

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

551 return url_without_auth, netloc, auth 

552 

553 

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

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

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

557 # and are not recognized in the url. 

558 return _transform_url(url, _get_netloc)[0] 

559 

560 

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

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

563 return _transform_url(url, _redact_netloc)[0] 

564 

565 

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

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

568 if not req.url: 

569 return str(req) 

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

571 

572 

573@dataclass(frozen=True) 

574class HiddenText: 

575 secret: str 

576 redacted: str 

577 

578 def __repr__(self) -> str: 

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

580 

581 def __str__(self) -> str: 

582 return self.redacted 

583 

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

585 # Equality is particularly useful for testing. 

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

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

588 # just the raw, original string. 

589 return self.secret == other.secret 

590 return NotImplemented 

591 

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

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

594 # equal have the same hash value. 

595 __hash__ = None # type: ignore[assignment] 

596 

597 

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

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

600 

601 

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

603 redacted = redact_auth_from_url(url) 

604 return HiddenText(url, redacted=redacted) 

605 

606 

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

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

609 

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

611 python -m pip ... 

612 """ 

613 pip_names = [ 

614 "pip", 

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

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

617 ] 

618 

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

620 should_show_use_python_msg = ( 

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

622 ) 

623 

624 if should_show_use_python_msg: 

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

626 raise CommandError( 

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

628 " ".join(new_command) 

629 ) 

630 ) 

631 

632 

633def check_externally_managed() -> None: 

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

635 

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

637 is considered externally managed, and an ExternallyManagedEnvironment is 

638 raised. 

639 """ 

640 if running_under_virtualenv(): 

641 return 

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

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

644 return 

645 raise ExternallyManagedEnvironment.from_config(marker) 

646 

647 

648def is_console_interactive() -> bool: 

649 """Is this console interactive?""" 

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

651 

652 

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

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

655 

656 h = hashlib.sha256() 

657 length = 0 

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

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

660 length += len(block) 

661 h.update(block) 

662 return h, length 

663 

664 

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

666 """ 

667 Return paired elements. 

668 

669 For example: 

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

671 """ 

672 iterable = iter(iterable) 

673 return zip_longest(iterable, iterable) 

674 

675 

676def partition( 

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

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

679 """ 

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

681 like 

682 

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

684 """ 

685 t1, t2 = tee(iterable) 

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

687 

688 

689def _build_backend_hook(func: F) -> F: 

690 """Apply warning policy and translate failures for a public backend hook.""" 

691 

692 @wraps(func) 

693 def wrapper( 

694 self: ConfiguredBuildBackendHookCaller, 

695 *args: Any, 

696 **kwargs: Any, 

697 ) -> Any: 

698 with build_backend_warnings(self.backend_warning_level): 

699 try: 

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

701 except BackendUnavailable as error: 

702 self._raise_backend_unavailable(func.__name__, error) 

703 

704 return cast(F, wrapper) 

705 

706 

707class ConfiguredBuildBackendHookCaller(BuildBackendHookCaller): 

708 def __init__( 

709 self, 

710 config_holder: Any, 

711 source_dir: str, 

712 build_backend: str, 

713 backend_path: str | None = None, 

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

715 python_executable: str | None = None, 

716 *, 

717 backend_warning_level: int = logging.DEBUG, 

718 ): 

719 super().__init__( 

720 source_dir, build_backend, backend_path, runner, python_executable 

721 ) 

722 self.config_holder = config_holder 

723 self.backend_warning_level = backend_warning_level 

724 

725 def _raise_backend_unavailable( 

726 self, hook_name: str, error: BackendUnavailable 

727 ) -> NoReturn: 

728 backend_error = ( 

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

730 ) 

731 exception = BackendUnavailableError( 

732 hook_name=hook_name, 

733 backend_name=self.build_backend, 

734 backend_error=backend_error, 

735 ) 

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

737 raise exception from error 

738 

739 @_build_backend_hook 

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

741 return feature in self._supported_features() 

742 

743 @_build_backend_hook 

744 def build_wheel( 

745 self, 

746 wheel_directory: str, 

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

748 metadata_directory: str | None = None, 

749 ) -> str: 

750 cs = self.config_holder.config_settings 

751 return super().build_wheel( 

752 wheel_directory, config_settings=cs, metadata_directory=metadata_directory 

753 ) 

754 

755 @_build_backend_hook 

756 def build_sdist( 

757 self, 

758 sdist_directory: str, 

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

760 ) -> str: 

761 cs = self.config_holder.config_settings 

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

763 

764 @_build_backend_hook 

765 def build_editable( 

766 self, 

767 wheel_directory: str, 

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

769 metadata_directory: str | None = None, 

770 ) -> str: 

771 cs = self.config_holder.config_settings 

772 return super().build_editable( 

773 wheel_directory, config_settings=cs, metadata_directory=metadata_directory 

774 ) 

775 

776 @_build_backend_hook 

777 def get_requires_for_build_wheel( 

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

779 ) -> Sequence[str]: 

780 cs = self.config_holder.config_settings 

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

782 

783 @_build_backend_hook 

784 def get_requires_for_build_sdist( 

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

786 ) -> Sequence[str]: 

787 cs = self.config_holder.config_settings 

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

789 

790 @_build_backend_hook 

791 def get_requires_for_build_editable( 

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

793 ) -> Sequence[str]: 

794 cs = self.config_holder.config_settings 

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

796 

797 @_build_backend_hook 

798 def prepare_metadata_for_build_wheel( 

799 self, 

800 metadata_directory: str, 

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

802 _allow_fallback: bool = True, 

803 ) -> str: 

804 cs = self.config_holder.config_settings 

805 return super().prepare_metadata_for_build_wheel( 

806 metadata_directory=metadata_directory, 

807 config_settings=cs, 

808 _allow_fallback=_allow_fallback, 

809 ) 

810 

811 @_build_backend_hook 

812 def prepare_metadata_for_build_editable( 

813 self, 

814 metadata_directory: str, 

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

816 _allow_fallback: bool = True, 

817 ) -> str: 

818 cs = self.config_holder.config_settings 

819 return super().prepare_metadata_for_build_editable( 

820 metadata_directory=metadata_directory, 

821 config_settings=cs, 

822 _allow_fallback=_allow_fallback, 

823 ) 

824 

825 

826def warn_if_run_as_root() -> None: 

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

828 

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

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

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

832 """ 

833 if running_under_virtualenv(): 

834 return 

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

836 return 

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

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

839 # 

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

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

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

843 return 

844 

845 if os.getuid() != 0: 

846 return 

847 

848 logger.warning( 

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

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

851 "rendering your system unusable. " 

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

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

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

855 "want to suppress this warning." 

856 ) 

857 

858 

859def looks_like_ci() -> bool: 

860 """ 

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

862 """ 

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

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

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

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