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

253 statements  

« prev     ^ index     » next       coverage.py v7.4.3, created at 2024-02-26 06:33 +0000

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 

8import configparser 

9import contextlib 

10import locale 

11import logging 

12import pathlib 

13import re 

14import sys 

15from itertools import chain, groupby, repeat 

16from typing import TYPE_CHECKING, Dict, Iterator, List, Literal, Optional, Union 

17 

18from pip._vendor.requests.models import Request, Response 

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

20from pip._vendor.rich.markup import escape 

21from pip._vendor.rich.text import Text 

22 

23if TYPE_CHECKING: 

24 from hashlib import _Hash 

25 

26 from pip._internal.metadata import BaseDistribution 

27 from pip._internal.req.req_install import InstallRequirement 

28 

29logger = logging.getLogger(__name__) 

30 

31 

32# 

33# Scaffolding 

34# 

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

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

37 

38 

39def _prefix_with_indent( 

40 s: Union[Text, str], 

41 console: Console, 

42 *, 

43 prefix: str, 

44 indent: str, 

45) -> Text: 

46 if isinstance(s, Text): 

47 text = s 

48 else: 

49 text = console.render_str(s) 

50 

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

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

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

54 

55 

56class PipError(Exception): 

57 """The base pip error.""" 

58 

59 

60class DiagnosticPipError(PipError): 

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

62 

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

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

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

66 main error message in a consistent style. 

67 

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

69 """ 

70 

71 reference: str 

72 

73 def __init__( 

74 self, 

75 *, 

76 kind: 'Literal["error", "warning"]' = "error", 

77 reference: Optional[str] = None, 

78 message: Union[str, Text], 

79 context: Optional[Union[str, Text]], 

80 hint_stmt: Optional[Union[str, Text]], 

81 note_stmt: Optional[Union[str, Text]] = None, 

82 link: Optional[str] = None, 

83 ) -> None: 

84 # Ensure a proper reference is provided. 

85 if reference is None: 

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

87 reference = self.reference 

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

89 

90 self.kind = kind 

91 self.reference = reference 

92 

93 self.message = message 

94 self.context = context 

95 

96 self.note_stmt = note_stmt 

97 self.hint_stmt = hint_stmt 

98 

99 self.link = link 

100 

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

102 

103 def __repr__(self) -> str: 

104 return ( 

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

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

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

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

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

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

111 ")>" 

112 ) 

113 

114 def __rich_console__( 

115 self, 

116 console: Console, 

117 options: ConsoleOptions, 

118 ) -> RenderResult: 

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

120 

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

122 yield "" 

123 

124 if not options.ascii_only: 

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

126 if self.context is not None: 

127 yield _prefix_with_indent( 

128 self.message, 

129 console, 

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

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

132 ) 

133 yield _prefix_with_indent( 

134 self.context, 

135 console, 

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

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

138 ) 

139 else: 

140 yield _prefix_with_indent( 

141 self.message, 

142 console, 

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

144 indent=" ", 

145 ) 

146 else: 

147 yield self.message 

148 if self.context is not None: 

149 yield "" 

150 yield self.context 

151 

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

153 yield "" 

154 

155 if self.note_stmt is not None: 

156 yield _prefix_with_indent( 

157 self.note_stmt, 

158 console, 

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

160 indent=" ", 

161 ) 

162 if self.hint_stmt is not None: 

163 yield _prefix_with_indent( 

164 self.hint_stmt, 

165 console, 

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

167 indent=" ", 

168 ) 

169 

170 if self.link is not None: 

171 yield "" 

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

173 

174 

175# 

176# Actual Errors 

177# 

178class ConfigurationError(PipError): 

179 """General exception in configuration""" 

180 

181 

182class InstallationError(PipError): 

183 """General exception during installation""" 

184 

185 

186class UninstallationError(PipError): 

187 """General exception during uninstallation""" 

188 

189 

190class MissingPyProjectBuildRequires(DiagnosticPipError): 

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

192 

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

194 

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

196 super().__init__( 

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

198 context=Text( 

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

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

201 ), 

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

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

204 ) 

205 

206 

207class InvalidPyProjectBuildRequires(DiagnosticPipError): 

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

209 

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

211 

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

213 super().__init__( 

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

215 context=Text( 

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

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

218 ), 

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

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

221 ) 

222 

223 

224class NoneMetadataError(PipError): 

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

226 

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

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

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

230 errors. 

231 """ 

232 

233 def __init__( 

234 self, 

235 dist: "BaseDistribution", 

236 metadata_name: str, 

237 ) -> None: 

238 """ 

239 :param dist: A Distribution object. 

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

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

242 """ 

243 self.dist = dist 

244 self.metadata_name = metadata_name 

245 

246 def __str__(self) -> str: 

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

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

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

250 

251 

252class UserInstallationInvalid(InstallationError): 

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

254 

255 def __str__(self) -> str: 

256 return "User base directory is not specified" 

257 

258 

259class InvalidSchemeCombination(InstallationError): 

260 def __str__(self) -> str: 

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

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

263 

264 

265class DistributionNotFound(InstallationError): 

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

267 

268 

269class RequirementsFileParseError(InstallationError): 

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

271 

272 

273class BestVersionAlreadyInstalled(PipError): 

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

275 installed.""" 

276 

277 

278class BadCommand(PipError): 

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

280 

281 

282class CommandError(PipError): 

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

284 

285 

286class PreviousBuildDirError(PipError): 

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

288 

289 

290class NetworkConnectionError(PipError): 

291 """HTTP connection error""" 

292 

293 def __init__( 

294 self, 

295 error_msg: str, 

296 response: Optional[Response] = None, 

297 request: Optional[Request] = None, 

298 ) -> None: 

299 """ 

300 Initialize NetworkConnectionError with `request` and `response` 

301 objects. 

302 """ 

303 self.response = response 

304 self.request = request 

305 self.error_msg = error_msg 

306 if ( 

307 self.response is not None 

308 and not self.request 

309 and hasattr(response, "request") 

310 ): 

311 self.request = self.response.request 

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

313 

314 def __str__(self) -> str: 

315 return str(self.error_msg) 

316 

317 

318class InvalidWheelFilename(InstallationError): 

319 """Invalid wheel filename.""" 

320 

321 

322class UnsupportedWheel(InstallationError): 

323 """Unsupported wheel.""" 

324 

325 

326class InvalidWheel(InstallationError): 

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

328 

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

330 self.location = location 

331 self.name = name 

332 

333 def __str__(self) -> str: 

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

335 

336 

337class MetadataInconsistent(InstallationError): 

338 """Built metadata contains inconsistent information. 

339 

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

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

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

343 """ 

344 

345 def __init__( 

346 self, ireq: "InstallRequirement", field: str, f_val: str, m_val: str 

347 ) -> None: 

348 self.ireq = ireq 

349 self.field = field 

350 self.f_val = f_val 

351 self.m_val = m_val 

352 

353 def __str__(self) -> str: 

354 return ( 

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

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

357 ) 

358 

359 

360class InstallationSubprocessError(DiagnosticPipError, InstallationError): 

361 """A subprocess call failed.""" 

362 

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

364 

365 def __init__( 

366 self, 

367 *, 

368 command_description: str, 

369 exit_code: int, 

370 output_lines: Optional[List[str]], 

371 ) -> None: 

372 if output_lines is None: 

373 output_prompt = Text("See above for output.") 

374 else: 

375 output_prompt = ( 

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

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

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

379 ) 

380 

381 super().__init__( 

382 message=( 

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

384 f"exit code: {exit_code}" 

385 ), 

386 context=output_prompt, 

387 hint_stmt=None, 

388 note_stmt=( 

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

390 "problem with pip." 

391 ), 

392 ) 

393 

394 self.command_description = command_description 

395 self.exit_code = exit_code 

396 

397 def __str__(self) -> str: 

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

399 

400 

401class MetadataGenerationFailed(InstallationSubprocessError, InstallationError): 

402 reference = "metadata-generation-failed" 

403 

404 def __init__( 

405 self, 

406 *, 

407 package_details: str, 

408 ) -> None: 

409 super(InstallationSubprocessError, self).__init__( 

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

411 context=escape(package_details), 

412 hint_stmt="See above for details.", 

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

414 ) 

415 

416 def __str__(self) -> str: 

417 return "metadata generation failed" 

418 

419 

420class HashErrors(InstallationError): 

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

422 

423 def __init__(self) -> None: 

424 self.errors: List["HashError"] = [] 

425 

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

427 self.errors.append(error) 

428 

429 def __str__(self) -> str: 

430 lines = [] 

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

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

433 lines.append(cls.head) 

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

435 if lines: 

436 return "\n".join(lines) 

437 return "" 

438 

439 def __bool__(self) -> bool: 

440 return bool(self.errors) 

441 

442 

443class HashError(InstallationError): 

444 """ 

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

446 

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

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

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

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

451 deterministic order. 

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

453 exceptions of this kind 

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

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

456 typically available earlier. 

457 

458 """ 

459 

460 req: Optional["InstallRequirement"] = None 

461 head = "" 

462 order: int = -1 

463 

464 def body(self) -> str: 

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

466 

467 This default implementation simply prints a description of the 

468 triggering requirement. 

469 

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

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

472 

473 """ 

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

475 

476 def __str__(self) -> str: 

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

478 

479 def _requirement_name(self) -> str: 

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

481 

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

483 line numbers 

484 

485 """ 

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

487 

488 

489class VcsHashUnsupported(HashError): 

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

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

492 

493 order = 0 

494 head = ( 

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

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

497 ) 

498 

499 

500class DirectoryUrlHashUnsupported(HashError): 

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

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

503 

504 order = 1 

505 head = ( 

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

507 "point to directories:" 

508 ) 

509 

510 

511class HashMissing(HashError): 

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

513 

514 order = 2 

515 head = ( 

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

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

518 "requirements along with the hashes their downloaded archives " 

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

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

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

522 "has a hash.)" 

523 ) 

524 

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

526 """ 

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

528 just downloaded 

529 """ 

530 self.gotten_hash = gotten_hash 

531 

532 def body(self) -> str: 

533 # Dodge circular import. 

534 from pip._internal.utils.hashes import FAVORITE_HASH 

535 

536 package = None 

537 if self.req: 

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

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

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

541 package = ( 

542 self.req.original_link 

543 if self.req.is_direct 

544 # In case someone feeds something downright stupid 

545 # to InstallRequirement's constructor. 

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

547 ) 

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

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

550 ) 

551 

552 

553class HashUnpinned(HashError): 

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

555 version.""" 

556 

557 order = 3 

558 head = ( 

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

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

561 ) 

562 

563 

564class HashMismatch(HashError): 

565 """ 

566 Distribution file hash values don't match. 

567 

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

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

570 improve its error message. 

571 

572 """ 

573 

574 order = 4 

575 head = ( 

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

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

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

579 "someone may have tampered with them." 

580 ) 

581 

582 def __init__(self, allowed: Dict[str, List[str]], gots: Dict[str, "_Hash"]) -> None: 

583 """ 

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

585 hex digests 

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

587 actually got from the files under suspicion 

588 """ 

589 self.allowed = allowed 

590 self.gots = gots 

591 

592 def body(self) -> str: 

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

594 

595 def _hash_comparison(self) -> str: 

596 """ 

597 Return a comparison of actual and expected hash values. 

598 

599 Example:: 

600 

601 Expected sha256 abcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcde 

602 or 123451234512345123451234512345123451234512345 

603 Got bcdefbcdefbcdefbcdefbcdefbcdefbcdefbcdefbcdef 

604 

605 """ 

606 

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

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

609 # away with hard-coding space literals. 

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

611 

612 lines: List[str] = [] 

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

614 prefix = hash_then_or(hash_name) 

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

616 lines.append( 

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

618 ) 

619 return "\n".join(lines) 

620 

621 

622class UnsupportedPythonVersion(InstallationError): 

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

624 metadata.""" 

625 

626 

627class ConfigurationFileCouldNotBeLoaded(ConfigurationError): 

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

629 

630 def __init__( 

631 self, 

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

633 fname: Optional[str] = None, 

634 error: Optional[configparser.Error] = None, 

635 ) -> None: 

636 super().__init__(error) 

637 self.reason = reason 

638 self.fname = fname 

639 self.error = error 

640 

641 def __str__(self) -> str: 

642 if self.fname is not None: 

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

644 else: 

645 assert self.error is not None 

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

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

648 

649 

650_DEFAULT_EXTERNALLY_MANAGED_ERROR = f"""\ 

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

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

653the Python installation to interact with this environment instead. 

654""" 

655 

656 

657class ExternallyManagedEnvironment(DiagnosticPipError): 

658 """The current environment is externally managed. 

659 

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

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

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

663 

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

665 """ 

666 

667 reference = "externally-managed-environment" 

668 

669 def __init__(self, error: Optional[str]) -> None: 

670 if error is None: 

671 context = Text(_DEFAULT_EXTERNALLY_MANAGED_ERROR) 

672 else: 

673 context = Text(error) 

674 super().__init__( 

675 message="This environment is externally managed", 

676 context=context, 

677 note_stmt=( 

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

679 "Python installation or OS distribution provider. " 

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

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

682 ), 

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

684 ) 

685 

686 @staticmethod 

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

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

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

690 # using other categories for console message localization is equally 

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

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

693 try: 

694 category = locale.LC_MESSAGES 

695 except AttributeError: 

696 lang: Optional[str] = None 

697 else: 

698 lang, _ = locale.getlocale(category) 

699 if lang is not None: 

700 yield f"Error-{lang}" 

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

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

703 if not found: 

704 continue 

705 yield f"Error-{before}" 

706 yield "Error" 

707 

708 @classmethod 

709 def from_config( 

710 cls, 

711 config: Union[pathlib.Path, str], 

712 ) -> "ExternallyManagedEnvironment": 

713 parser = configparser.ConfigParser(interpolation=None) 

714 try: 

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

716 section = parser["externally-managed"] 

717 for key in cls._iter_externally_managed_error_keys(): 

718 with contextlib.suppress(KeyError): 

719 return cls(section[key]) 

720 except KeyError: 

721 pass 

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

723 from pip._internal.utils._log import VERBOSE 

724 

725 exc_info = logger.isEnabledFor(VERBOSE) 

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

727 return cls(None)