Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pip/_internal/exceptions.py: 41%

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

341 statements  

1"""Exceptions used throughout package. 

2 

3This module MUST NOT try to import from anything within `pip._internal` to 

4operate. This is expected to be importable from any/all files within the 

5subpackage and, thus, should not depend on them. 

6""" 

7 

8from __future__ import annotations 

9 

10import configparser 

11import contextlib 

12import locale 

13import logging 

14import pathlib 

15import re 

16import sys 

17import traceback 

18from collections.abc import Iterable, Iterator 

19from itertools import chain, groupby, repeat 

20from typing import TYPE_CHECKING, Literal 

21 

22from pip._vendor.packaging.requirements import InvalidRequirement 

23from pip._vendor.packaging.version import InvalidVersion 

24from pip._vendor.rich.console import Console, ConsoleOptions, RenderResult 

25from pip._vendor.rich.markup import escape 

26from pip._vendor.rich.text import Text 

27 

28if TYPE_CHECKING: 

29 from hashlib import _Hash 

30 

31 from pip._vendor.requests.models import PreparedRequest, Request, Response 

32 

33 from pip._internal.metadata import BaseDistribution 

34 from pip._internal.models.link import Link 

35 from pip._internal.network.download import _FileDownload 

36 from pip._internal.req.req_install import InstallRequirement 

37 

38logger = logging.getLogger(__name__) 

39 

40 

41# 

42# Scaffolding 

43# 

44def _is_kebab_case(s: str) -> bool: 

45 return re.match(r"^[a-z]+(-[a-z]+)*$", s) is not None 

46 

47 

48def _prefix_with_indent( 

49 s: Text | str, 

50 console: Console, 

51 *, 

52 prefix: str, 

53 indent: str, 

54) -> Text: 

55 if isinstance(s, Text): 

56 text = s 

57 else: 

58 text = console.render_str(s) 

59 

60 return console.render_str(prefix, overflow="ignore") + console.render_str( 

61 f"\n{indent}", overflow="ignore" 

62 ).join(text.split(allow_blank=True)) 

63 

64 

65class PipError(Exception): 

66 """The base pip error.""" 

67 

68 

69class DiagnosticPipError(PipError): 

70 """An error, that presents diagnostic information to the user. 

71 

72 This contains a bunch of logic, to enable pretty presentation of our error 

73 messages. Each error gets a unique reference. Each error can also include 

74 additional context, a hint and/or a note -- which are presented with the 

75 main error message in a consistent style. 

76 

77 This is adapted from the error output styling in `sphinx-theme-builder`. 

78 """ 

79 

80 reference: str 

81 

82 def __init__( 

83 self, 

84 *, 

85 kind: Literal["error", "warning"] = "error", 

86 reference: str | None = None, 

87 message: str | Text, 

88 context: str | Text | None, 

89 hint_stmt: str | Text | None, 

90 note_stmt: str | Text | None = None, 

91 link: str | None = None, 

92 ) -> None: 

93 # Ensure a proper reference is provided. 

94 if reference is None: 

95 assert hasattr(self, "reference"), "error reference not provided!" 

96 reference = self.reference 

97 assert _is_kebab_case(reference), "error reference must be kebab-case!" 

98 

99 self.kind = kind 

100 self.reference = reference 

101 

102 self.message = message 

103 self.context = context 

104 

105 self.note_stmt = note_stmt 

106 self.hint_stmt = hint_stmt 

107 

108 self.link = link 

109 

110 super().__init__(f"<{self.__class__.__name__}: {self.reference}>") 

111 

112 def __repr__(self) -> str: 

113 return ( 

114 f"<{self.__class__.__name__}(" 

115 f"reference={self.reference!r}, " 

116 f"message={self.message!r}, " 

117 f"context={self.context!r}, " 

118 f"note_stmt={self.note_stmt!r}, " 

119 f"hint_stmt={self.hint_stmt!r}" 

120 ")>" 

121 ) 

122 

123 def __rich_console__( 

124 self, 

125 console: Console, 

126 options: ConsoleOptions, 

127 ) -> RenderResult: 

128 colour = "red" if self.kind == "error" else "yellow" 

129 

130 yield f"[{colour} bold]{self.kind}[/]: [bold]{self.reference}[/]" 

131 yield "" 

132 

133 if not options.ascii_only: 

134 # Present the main message, with relevant context indented. 

135 if self.context is not None: 

136 yield _prefix_with_indent( 

137 self.message, 

138 console, 

139 prefix=f"[{colour}]×[/] ", 

140 indent=f"[{colour}]│[/] ", 

141 ) 

142 yield _prefix_with_indent( 

143 self.context, 

144 console, 

145 prefix=f"[{colour}]╰─>[/] ", 

146 indent=f"[{colour}] [/] ", 

147 ) 

148 else: 

149 yield _prefix_with_indent( 

150 self.message, 

151 console, 

152 prefix="[red]×[/] ", 

153 indent=" ", 

154 ) 

155 else: 

156 yield self.message 

157 if self.context is not None: 

158 yield "" 

159 yield self.context 

160 

161 if self.note_stmt is not None or self.hint_stmt is not None: 

162 yield "" 

163 

164 if self.note_stmt is not None: 

165 yield _prefix_with_indent( 

166 self.note_stmt, 

167 console, 

168 prefix="[magenta bold]note[/]: ", 

169 indent=" ", 

170 ) 

171 if self.hint_stmt is not None: 

172 yield _prefix_with_indent( 

173 self.hint_stmt, 

174 console, 

175 prefix="[cyan bold]hint[/]: ", 

176 indent=" ", 

177 ) 

178 

179 if self.link is not None: 

180 yield "" 

181 yield f"Link: {self.link}" 

182 

183 

184# 

185# Actual Errors 

186# 

187class ConfigurationError(PipError): 

188 """General exception in configuration""" 

189 

190 

191class InstallationError(PipError): 

192 """General exception during installation""" 

193 

194 

195class FailedToPrepareCandidate(InstallationError): 

196 """Raised when we fail to prepare a candidate (i.e. fetch and generate metadata). 

197 

198 This is intentionally not a diagnostic error, since the output will be presented 

199 above this error, when this occurs. This should instead present information to the 

200 user. 

201 """ 

202 

203 def __init__( 

204 self, *, package_name: str, requirement_chain: str, failed_step: str 

205 ) -> None: 

206 super().__init__(f"Failed to build '{package_name}' when {failed_step.lower()}") 

207 self.package_name = package_name 

208 self.requirement_chain = requirement_chain 

209 self.failed_step = failed_step 

210 

211 

212class MissingPyProjectBuildRequires(DiagnosticPipError): 

213 """Raised when pyproject.toml has `build-system`, but no `build-system.requires`.""" 

214 

215 reference = "missing-pyproject-build-system-requires" 

216 

217 def __init__(self, *, package: str) -> None: 

218 super().__init__( 

219 message=f"Can not process {escape(package)}", 

220 context=Text( 

221 "This package has an invalid pyproject.toml file.\n" 

222 "The [build-system] table is missing the mandatory `requires` key." 

223 ), 

224 note_stmt="This is an issue with the package mentioned above, not pip.", 

225 hint_stmt=Text("See PEP 518 for the detailed specification."), 

226 ) 

227 

228 

229class InvalidPyProjectBuildRequires(DiagnosticPipError): 

230 """Raised when pyproject.toml an invalid `build-system.requires`.""" 

231 

232 reference = "invalid-pyproject-build-system-requires" 

233 

234 def __init__(self, *, package: str, reason: str) -> None: 

235 super().__init__( 

236 message=f"Can not process {escape(package)}", 

237 context=Text( 

238 "This package has an invalid `build-system.requires` key in " 

239 f"pyproject.toml.\n{reason}" 

240 ), 

241 note_stmt="This is an issue with the package mentioned above, not pip.", 

242 hint_stmt=Text("See PEP 518 for the detailed specification."), 

243 ) 

244 

245 

246class NoneMetadataError(PipError): 

247 """Raised when accessing a Distribution's "METADATA" or "PKG-INFO". 

248 

249 This signifies an inconsistency, when the Distribution claims to have 

250 the metadata file (if not, raise ``FileNotFoundError`` instead), but is 

251 not actually able to produce its content. This may be due to permission 

252 errors. 

253 """ 

254 

255 def __init__( 

256 self, 

257 dist: BaseDistribution, 

258 metadata_name: str, 

259 ) -> None: 

260 """ 

261 :param dist: A Distribution object. 

262 :param metadata_name: The name of the metadata being accessed 

263 (can be "METADATA" or "PKG-INFO"). 

264 """ 

265 self.dist = dist 

266 self.metadata_name = metadata_name 

267 

268 def __str__(self) -> str: 

269 # Use `dist` in the error message because its stringification 

270 # includes more information, like the version and location. 

271 return f"None {self.metadata_name} metadata found for distribution: {self.dist}" 

272 

273 

274class UserInstallationInvalid(InstallationError): 

275 """A --user install is requested on an environment without user site.""" 

276 

277 def __str__(self) -> str: 

278 return "User base directory is not specified" 

279 

280 

281class InvalidSchemeCombination(InstallationError): 

282 def __str__(self) -> str: 

283 before = ", ".join(str(a) for a in self.args[:-1]) 

284 return f"Cannot set {before} and {self.args[-1]} together" 

285 

286 

287class DistributionNotFound(InstallationError): 

288 """Raised when a distribution cannot be found to satisfy a requirement""" 

289 

290 

291class RequirementsFileParseError(InstallationError): 

292 """Raised when a general error occurs parsing a requirements file line.""" 

293 

294 

295class BestVersionAlreadyInstalled(PipError): 

296 """Raised when the most up-to-date version of a package is already 

297 installed.""" 

298 

299 

300class BadCommand(PipError): 

301 """Raised when virtualenv or a command is not found""" 

302 

303 

304class CommandError(PipError): 

305 """Raised when there is an error in command-line arguments""" 

306 

307 

308class PreviousBuildDirError(PipError): 

309 """Raised when there's a previous conflicting build directory""" 

310 

311 

312class NetworkConnectionError(PipError): 

313 """HTTP connection error""" 

314 

315 def __init__( 

316 self, 

317 error_msg: str, 

318 response: Response | None = None, 

319 request: Request | PreparedRequest | None = None, 

320 ) -> None: 

321 """ 

322 Initialize NetworkConnectionError with `request` and `response` 

323 objects. 

324 """ 

325 self.response = response 

326 self.request = request 

327 self.error_msg = error_msg 

328 if ( 

329 self.response is not None 

330 and not self.request 

331 and hasattr(response, "request") 

332 ): 

333 self.request = self.response.request 

334 super().__init__(error_msg, response, request) 

335 

336 def __str__(self) -> str: 

337 return str(self.error_msg) 

338 

339 

340class InvalidWheelFilename(InstallationError): 

341 """Invalid wheel filename.""" 

342 

343 

344class UnsupportedWheel(InstallationError): 

345 """Unsupported wheel.""" 

346 

347 

348class InvalidWheel(InstallationError): 

349 """Invalid (e.g. corrupt) wheel.""" 

350 

351 def __init__(self, location: str, name: str): 

352 self.location = location 

353 self.name = name 

354 

355 def __str__(self) -> str: 

356 return f"Wheel '{self.name}' located at {self.location} is invalid." 

357 

358 

359class MetadataInconsistent(InstallationError): 

360 """Built metadata contains inconsistent information. 

361 

362 This is raised when the metadata contains values (e.g. name and version) 

363 that do not match the information previously obtained from sdist filename, 

364 user-supplied ``#egg=`` value, or an install requirement name. 

365 """ 

366 

367 def __init__( 

368 self, ireq: InstallRequirement, field: str, f_val: str, m_val: str 

369 ) -> None: 

370 self.ireq = ireq 

371 self.field = field 

372 self.f_val = f_val 

373 self.m_val = m_val 

374 

375 def __str__(self) -> str: 

376 return ( 

377 f"Requested {self.ireq} has inconsistent {self.field}: " 

378 f"expected {self.f_val!r}, but metadata has {self.m_val!r}" 

379 ) 

380 

381 

382class MetadataInvalid(InstallationError): 

383 """Metadata is invalid.""" 

384 

385 def __init__(self, ireq: InstallRequirement, error: str) -> None: 

386 self.ireq = ireq 

387 self.error = error 

388 

389 def __str__(self) -> str: 

390 return f"Requested {self.ireq} has invalid metadata: {self.error}" 

391 

392 

393class InstallationSubprocessError(DiagnosticPipError, InstallationError): 

394 """A subprocess call failed.""" 

395 

396 reference = "subprocess-exited-with-error" 

397 

398 def __init__( 

399 self, 

400 *, 

401 command_description: str, 

402 exit_code: int, 

403 output_lines: list[str] | None, 

404 ) -> None: 

405 if output_lines is None: 

406 output_prompt = Text("No available output.") 

407 else: 

408 output_prompt = ( 

409 Text.from_markup(f"[red][{len(output_lines)} lines of output][/]\n") 

410 + Text("".join(output_lines)) 

411 + Text.from_markup(R"[red]\[end of output][/]") 

412 ) 

413 

414 super().__init__( 

415 message=( 

416 f"[green]{escape(command_description)}[/] did not run successfully.\n" 

417 f"exit code: {exit_code}" 

418 ), 

419 context=output_prompt, 

420 hint_stmt=None, 

421 note_stmt=( 

422 "This error originates from a subprocess, and is likely not a " 

423 "problem with pip." 

424 ), 

425 ) 

426 

427 self.command_description = command_description 

428 self.exit_code = exit_code 

429 

430 def __str__(self) -> str: 

431 return f"{self.command_description} exited with {self.exit_code}" 

432 

433 

434class MetadataGenerationFailed(DiagnosticPipError, InstallationError): 

435 reference = "metadata-generation-failed" 

436 

437 def __init__( 

438 self, 

439 *, 

440 package_details: str, 

441 ) -> None: 

442 super().__init__( 

443 message="Encountered error while generating package metadata.", 

444 context=escape(package_details), 

445 hint_stmt="See above for details.", 

446 note_stmt="This is an issue with the package mentioned above, not pip.", 

447 ) 

448 

449 def __str__(self) -> str: 

450 return "metadata generation failed" 

451 

452 

453class HashErrors(InstallationError): 

454 """Multiple HashError instances rolled into one for reporting""" 

455 

456 def __init__(self) -> None: 

457 self.errors: list[HashError] = [] 

458 

459 def append(self, error: HashError) -> None: 

460 self.errors.append(error) 

461 

462 def __str__(self) -> str: 

463 lines = [] 

464 self.errors.sort(key=lambda e: e.order) 

465 for cls, errors_of_cls in groupby(self.errors, lambda e: e.__class__): 

466 lines.append(cls.head) 

467 lines.extend(e.body() for e in errors_of_cls) 

468 if lines: 

469 return "\n".join(lines) 

470 return "" 

471 

472 def __bool__(self) -> bool: 

473 return bool(self.errors) 

474 

475 

476class HashError(InstallationError): 

477 """ 

478 A failure to verify a package against known-good hashes 

479 

480 :cvar order: An int sorting hash exception classes by difficulty of 

481 recovery (lower being harder), so the user doesn't bother fretting 

482 about unpinned packages when he has deeper issues, like VCS 

483 dependencies, to deal with. Also keeps error reports in a 

484 deterministic order. 

485 :cvar head: A section heading for display above potentially many 

486 exceptions of this kind 

487 :ivar req: The InstallRequirement that triggered this error. This is 

488 pasted on after the exception is instantiated, because it's not 

489 typically available earlier. 

490 

491 """ 

492 

493 req: InstallRequirement | None = None 

494 head = "" 

495 order: int = -1 

496 

497 def body(self) -> str: 

498 """Return a summary of me for display under the heading. 

499 

500 This default implementation simply prints a description of the 

501 triggering requirement. 

502 

503 :param req: The InstallRequirement that provoked this error, with 

504 its link already populated by the resolver's _populate_link(). 

505 

506 """ 

507 return f" {self._requirement_name()}" 

508 

509 def __str__(self) -> str: 

510 return f"{self.head}\n{self.body()}" 

511 

512 def _requirement_name(self) -> str: 

513 """Return a description of the requirement that triggered me. 

514 

515 This default implementation returns long description of the req, with 

516 line numbers 

517 

518 """ 

519 return str(self.req) if self.req else "unknown package" 

520 

521 

522class VcsHashUnsupported(HashError): 

523 """A hash was provided for a version-control-system-based requirement, but 

524 we don't have a method for hashing those.""" 

525 

526 order = 0 

527 head = ( 

528 "Can't verify hashes for these requirements because we don't " 

529 "have a way to hash version control repositories:" 

530 ) 

531 

532 

533class DirectoryUrlHashUnsupported(HashError): 

534 """A hash was provided for a version-control-system-based requirement, but 

535 we don't have a method for hashing those.""" 

536 

537 order = 1 

538 head = ( 

539 "Can't verify hashes for these file:// requirements because they " 

540 "point to directories:" 

541 ) 

542 

543 

544class HashMissing(HashError): 

545 """A hash was needed for a requirement but is absent.""" 

546 

547 order = 2 

548 head = ( 

549 "Hashes are required in --require-hashes mode, but they are " 

550 "missing from some requirements. Here is a list of those " 

551 "requirements along with the hashes their downloaded archives " 

552 "actually had. Add lines like these to your requirements files to " 

553 "prevent tampering. (If you did not enable --require-hashes " 

554 "manually, note that it turns on automatically when any package " 

555 "has a hash.)" 

556 ) 

557 

558 def __init__(self, gotten_hash: str) -> None: 

559 """ 

560 :param gotten_hash: The hash of the (possibly malicious) archive we 

561 just downloaded 

562 """ 

563 self.gotten_hash = gotten_hash 

564 

565 def body(self) -> str: 

566 # Dodge circular import. 

567 from pip._internal.utils.hashes import FAVORITE_HASH 

568 

569 package = None 

570 if self.req: 

571 # In the case of URL-based requirements, display the original URL 

572 # seen in the requirements file rather than the package name, 

573 # so the output can be directly copied into the requirements file. 

574 package = ( 

575 self.req.original_link 

576 if self.req.is_direct 

577 # In case someone feeds something downright stupid 

578 # to InstallRequirement's constructor. 

579 else getattr(self.req, "req", None) 

580 ) 

581 return " {} --hash={}:{}".format( 

582 package or "unknown package", FAVORITE_HASH, self.gotten_hash 

583 ) 

584 

585 

586class HashUnpinned(HashError): 

587 """A requirement had a hash specified but was not pinned to a specific 

588 version.""" 

589 

590 order = 3 

591 head = ( 

592 "In --require-hashes mode, all requirements must have their " 

593 "versions pinned with ==. These do not:" 

594 ) 

595 

596 

597class HashMismatch(HashError): 

598 """ 

599 Distribution file hash values don't match. 

600 

601 :ivar package_name: The name of the package that triggered the hash 

602 mismatch. Feel free to write to this after the exception is raise to 

603 improve its error message. 

604 

605 """ 

606 

607 order = 4 

608 head = ( 

609 "THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS " 

610 "FILE. If you have updated the package versions, please update " 

611 "the hashes. Otherwise, examine the package contents carefully; " 

612 "someone may have tampered with them." 

613 ) 

614 

615 def __init__(self, allowed: dict[str, list[str]], gots: dict[str, _Hash]) -> None: 

616 """ 

617 :param allowed: A dict of algorithm names pointing to lists of allowed 

618 hex digests 

619 :param gots: A dict of algorithm names pointing to hashes we 

620 actually got from the files under suspicion 

621 """ 

622 self.allowed = allowed 

623 self.gots = gots 

624 

625 def body(self) -> str: 

626 return f" {self._requirement_name()}:\n{self._hash_comparison()}" 

627 

628 def _hash_comparison(self) -> str: 

629 """ 

630 Return a comparison of actual and expected hash values. 

631 

632 Example:: 

633 

634 Expected sha256 abcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcde 

635 or 123451234512345123451234512345123451234512345 

636 Got bcdefbcdefbcdefbcdefbcdefbcdefbcdefbcdefbcdef 

637 

638 """ 

639 

640 def hash_then_or(hash_name: str) -> chain[str]: 

641 # For now, all the decent hashes have 6-char names, so we can get 

642 # away with hard-coding space literals. 

643 return chain([hash_name], repeat(" or")) 

644 

645 lines: list[str] = [] 

646 for hash_name, expecteds in self.allowed.items(): 

647 prefix = hash_then_or(hash_name) 

648 lines.extend((f" Expected {next(prefix)} {e}") for e in expecteds) 

649 lines.append( 

650 f" Got {self.gots[hash_name].hexdigest()}\n" 

651 ) 

652 return "\n".join(lines) 

653 

654 

655class UnsupportedPythonVersion(InstallationError): 

656 """Unsupported python version according to Requires-Python package 

657 metadata.""" 

658 

659 

660class ConfigurationFileCouldNotBeLoaded(ConfigurationError): 

661 """When there are errors while loading a configuration file""" 

662 

663 def __init__( 

664 self, 

665 reason: str = "could not be loaded", 

666 fname: str | None = None, 

667 error: configparser.Error | None = None, 

668 ) -> None: 

669 super().__init__(error) 

670 self.reason = reason 

671 self.fname = fname 

672 self.error = error 

673 

674 def __str__(self) -> str: 

675 if self.fname is not None: 

676 message_part = f" in {self.fname}." 

677 else: 

678 assert self.error is not None 

679 message_part = f".\n{self.error}\n" 

680 return f"Configuration file {self.reason}{message_part}" 

681 

682 

683_DEFAULT_EXTERNALLY_MANAGED_ERROR = f"""\ 

684The Python environment under {sys.prefix} is managed externally, and may not be 

685manipulated by the user. Please use specific tooling from the distributor of 

686the Python installation to interact with this environment instead. 

687""" 

688 

689 

690class ExternallyManagedEnvironment(DiagnosticPipError): 

691 """The current environment is externally managed. 

692 

693 This is raised when the current environment is externally managed, as 

694 defined by `PEP 668`_. The ``EXTERNALLY-MANAGED`` configuration is checked 

695 and displayed when the error is bubbled up to the user. 

696 

697 :param error: The error message read from ``EXTERNALLY-MANAGED``. 

698 """ 

699 

700 reference = "externally-managed-environment" 

701 

702 def __init__(self, error: str | None) -> None: 

703 if error is None: 

704 context = Text(_DEFAULT_EXTERNALLY_MANAGED_ERROR) 

705 else: 

706 context = Text(error) 

707 super().__init__( 

708 message="This environment is externally managed", 

709 context=context, 

710 note_stmt=( 

711 "If you believe this is a mistake, please contact your " 

712 "Python installation or OS distribution provider. " 

713 "You can override this, at the risk of breaking your Python " 

714 "installation or OS, by passing --break-system-packages." 

715 ), 

716 hint_stmt=Text("See PEP 668 for the detailed specification."), 

717 ) 

718 

719 @staticmethod 

720 def _iter_externally_managed_error_keys() -> Iterator[str]: 

721 # LC_MESSAGES is in POSIX, but not the C standard. The most common 

722 # platform that does not implement this category is Windows, where 

723 # using other categories for console message localization is equally 

724 # unreliable, so we fall back to the locale-less vendor message. This 

725 # can always be re-evaluated when a vendor proposes a new alternative. 

726 try: 

727 category = locale.LC_MESSAGES 

728 except AttributeError: 

729 lang: str | None = None 

730 else: 

731 lang, _ = locale.getlocale(category) 

732 if lang is not None: 

733 yield f"Error-{lang}" 

734 for sep in ("-", "_"): 

735 before, found, _ = lang.partition(sep) 

736 if not found: 

737 continue 

738 yield f"Error-{before}" 

739 yield "Error" 

740 

741 @classmethod 

742 def from_config( 

743 cls, 

744 config: pathlib.Path | str, 

745 ) -> ExternallyManagedEnvironment: 

746 parser = configparser.ConfigParser(interpolation=None) 

747 try: 

748 parser.read(config, encoding="utf-8") 

749 section = parser["externally-managed"] 

750 for key in cls._iter_externally_managed_error_keys(): 

751 with contextlib.suppress(KeyError): 

752 return cls(section[key]) 

753 except KeyError: 

754 pass 

755 except (OSError, UnicodeDecodeError, configparser.ParsingError): 

756 from pip._internal.utils._log import VERBOSE 

757 

758 exc_info = logger.isEnabledFor(VERBOSE) 

759 logger.warning("Failed to read %s", config, exc_info=exc_info) 

760 return cls(None) 

761 

762 

763class UninstallMissingRecord(DiagnosticPipError): 

764 reference = "uninstall-no-record-file" 

765 

766 def __init__(self, *, distribution: BaseDistribution) -> None: 

767 installer = distribution.installer 

768 if not installer or installer == "pip": 

769 dep = f"{distribution.raw_name}=={distribution.version}" 

770 hint = Text.assemble( 

771 "You might be able to recover from this via: ", 

772 (f"pip install --force-reinstall --no-deps {dep}", "green"), 

773 ) 

774 else: 

775 hint = Text( 

776 f"The package was installed by {installer}. " 

777 "You should check if it can uninstall the package." 

778 ) 

779 

780 super().__init__( 

781 message=Text(f"Cannot uninstall {distribution}"), 

782 context=( 

783 "The package's contents are unknown: " 

784 f"no RECORD file was found for {distribution.raw_name}." 

785 ), 

786 hint_stmt=hint, 

787 ) 

788 

789 

790class LegacyDistutilsInstall(DiagnosticPipError): 

791 reference = "uninstall-distutils-installed-package" 

792 

793 def __init__(self, *, distribution: BaseDistribution) -> None: 

794 super().__init__( 

795 message=Text(f"Cannot uninstall {distribution}"), 

796 context=( 

797 "It is a distutils installed project and thus we cannot accurately " 

798 "determine which files belong to it which would lead to only a partial " 

799 "uninstall." 

800 ), 

801 hint_stmt=None, 

802 ) 

803 

804 

805class InvalidInstalledPackage(DiagnosticPipError): 

806 reference = "invalid-installed-package" 

807 

808 def __init__( 

809 self, 

810 *, 

811 dist: BaseDistribution, 

812 invalid_exc: InvalidRequirement | InvalidVersion, 

813 ) -> None: 

814 installed_location = dist.installed_location 

815 

816 if isinstance(invalid_exc, InvalidRequirement): 

817 invalid_type = "requirement" 

818 else: 

819 invalid_type = "version" 

820 

821 super().__init__( 

822 message=Text( 

823 f"Cannot process installed package {dist} " 

824 + (f"in {installed_location!r} " if installed_location else "") 

825 + f"because it has an invalid {invalid_type}:\n{invalid_exc.args[0]}" 

826 ), 

827 context=( 

828 "Starting with pip 24.1, packages with invalid " 

829 f"{invalid_type}s can not be processed." 

830 ), 

831 hint_stmt="To proceed this package must be uninstalled.", 

832 ) 

833 

834 

835class IncompleteDownloadError(DiagnosticPipError): 

836 """Raised when the downloader receives fewer bytes than advertised 

837 in the Content-Length header.""" 

838 

839 reference = "incomplete-download" 

840 

841 def __init__(self, download: _FileDownload) -> None: 

842 # Dodge circular import. 

843 from pip._internal.utils.misc import format_size 

844 

845 assert download.size is not None 

846 download_status = ( 

847 f"{format_size(download.bytes_received)}/{format_size(download.size)}" 

848 ) 

849 if download.reattempts: 

850 retry_status = f"after {download.reattempts + 1} attempts " 

851 hint = "Use --resume-retries to configure resume attempt limit." 

852 else: 

853 # Download retrying is not enabled. 

854 retry_status = "" 

855 hint = "Consider using --resume-retries to enable download resumption." 

856 message = Text( 

857 f"Download failed {retry_status}because not enough bytes " 

858 f"were received ({download_status})" 

859 ) 

860 

861 super().__init__( 

862 message=message, 

863 context=f"URL: {download.link.redacted_url}", 

864 hint_stmt=hint, 

865 note_stmt="This is an issue with network connectivity, not pip.", 

866 ) 

867 

868 

869class ResolutionTooDeepError(DiagnosticPipError): 

870 """Raised when the dependency resolver exceeds the maximum recursion depth.""" 

871 

872 reference = "resolution-too-deep" 

873 

874 def __init__(self) -> None: 

875 super().__init__( 

876 message="Dependency resolution exceeded maximum depth", 

877 context=( 

878 "Pip cannot resolve the current dependencies as the dependency graph " 

879 "is too complex for pip to solve efficiently." 

880 ), 

881 hint_stmt=( 

882 "Try adding lower bounds to constrain your dependencies, " 

883 "for example: 'package>=2.0.0' instead of just 'package'. " 

884 ), 

885 link="https://pip.pypa.io/en/stable/topics/dependency-resolution/#handling-resolution-too-deep-errors", 

886 ) 

887 

888 

889class InstallWheelBuildError(DiagnosticPipError): 

890 reference = "failed-wheel-build-for-install" 

891 

892 def __init__(self, failed: list[InstallRequirement]) -> None: 

893 super().__init__( 

894 message=( 

895 "Failed to build installable wheels for some " 

896 "pyproject.toml based projects" 

897 ), 

898 context=", ".join(r.name for r in failed), # type: ignore 

899 hint_stmt=None, 

900 ) 

901 

902 

903class InvalidEggFragment(DiagnosticPipError): 

904 reference = "invalid-egg-fragment" 

905 

906 def __init__(self, link: Link, fragment: str) -> None: 

907 hint = "" 

908 if ">" in fragment or "=" in fragment or "<" in fragment: 

909 hint = ( 

910 "Version specifiers are silently ignored for URL references. " 

911 "Remove them. " 

912 ) 

913 if "[" in fragment and "]" in fragment: 

914 hint += "Try using the Direct URL requirement syntax: 'name[extra] @ URL'" 

915 

916 if not hint: 

917 hint = "Egg fragments can only be a valid project name." 

918 

919 super().__init__( 

920 message=f"The '{escape(fragment)}' egg fragment is invalid", 

921 context=f"from '{escape(str(link))}'", 

922 hint_stmt=escape(hint), 

923 ) 

924 

925 

926class BuildDependencyInstallError(DiagnosticPipError): 

927 """Raised when build dependencies cannot be installed.""" 

928 

929 reference = "failed-build-dependency-install" 

930 

931 def __init__( 

932 self, 

933 req: InstallRequirement | None, 

934 build_reqs: Iterable[str], 

935 *, 

936 cause: Exception, 

937 log_lines: list[str] | None, 

938 ) -> None: 

939 if isinstance(cause, PipError): 

940 note = "This is likely not a problem with pip." 

941 else: 

942 note = ( 

943 "pip crashed unexpectedly. Please file an issue on pip's issue " 

944 "tracker: https://github.com/pypa/pip/issues/new" 

945 ) 

946 

947 if log_lines is None: 

948 # No logs are available, they must have been printed earlier. 

949 context = Text("See above for more details.") 

950 else: 

951 if isinstance(cause, PipError): 

952 log_lines.append(f"ERROR: {cause}") 

953 else: 

954 # Split rendered error into real lines without trailing newlines. 

955 log_lines.extend( 

956 "".join(traceback.format_exception(cause)).splitlines() 

957 ) 

958 

959 context = Text.assemble( 

960 f"Installing {' '.join(build_reqs)}\n", 

961 (f"[{len(log_lines)} lines of output]\n", "red"), 

962 "\n".join(log_lines), 

963 ("\n[end of output]", "red"), 

964 ) 

965 

966 message = Text("Cannot install build dependencies", "green") 

967 if req: 

968 message += Text(f" for {req}") 

969 super().__init__( 

970 message=message, context=context, hint_stmt=None, note_stmt=note 

971 )