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

254 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:48 +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, 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 from typing import Literal 

26 

27 from pip._internal.metadata import BaseDistribution 

28 from pip._internal.req.req_install import InstallRequirement 

29 

30logger = logging.getLogger(__name__) 

31 

32 

33# 

34# Scaffolding 

35# 

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

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

38 

39 

40def _prefix_with_indent( 

41 s: Union[Text, str], 

42 console: Console, 

43 *, 

44 prefix: str, 

45 indent: str, 

46) -> Text: 

47 if isinstance(s, Text): 

48 text = s 

49 else: 

50 text = console.render_str(s) 

51 

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

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

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

55 

56 

57class PipError(Exception): 

58 """The base pip error.""" 

59 

60 

61class DiagnosticPipError(PipError): 

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

63 

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

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

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

67 main error message in a consistent style. 

68 

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

70 """ 

71 

72 reference: str 

73 

74 def __init__( 

75 self, 

76 *, 

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

78 reference: Optional[str] = None, 

79 message: Union[str, Text], 

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

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

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

83 link: Optional[str] = None, 

84 ) -> None: 

85 # Ensure a proper reference is provided. 

86 if reference is None: 

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

88 reference = self.reference 

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

90 

91 self.kind = kind 

92 self.reference = reference 

93 

94 self.message = message 

95 self.context = context 

96 

97 self.note_stmt = note_stmt 

98 self.hint_stmt = hint_stmt 

99 

100 self.link = link 

101 

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

103 

104 def __repr__(self) -> str: 

105 return ( 

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

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

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

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

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

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

112 ")>" 

113 ) 

114 

115 def __rich_console__( 

116 self, 

117 console: Console, 

118 options: ConsoleOptions, 

119 ) -> RenderResult: 

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

121 

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

123 yield "" 

124 

125 if not options.ascii_only: 

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

127 if self.context is not None: 

128 yield _prefix_with_indent( 

129 self.message, 

130 console, 

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

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

133 ) 

134 yield _prefix_with_indent( 

135 self.context, 

136 console, 

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

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

139 ) 

140 else: 

141 yield _prefix_with_indent( 

142 self.message, 

143 console, 

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

145 indent=" ", 

146 ) 

147 else: 

148 yield self.message 

149 if self.context is not None: 

150 yield "" 

151 yield self.context 

152 

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

154 yield "" 

155 

156 if self.note_stmt is not None: 

157 yield _prefix_with_indent( 

158 self.note_stmt, 

159 console, 

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

161 indent=" ", 

162 ) 

163 if self.hint_stmt is not None: 

164 yield _prefix_with_indent( 

165 self.hint_stmt, 

166 console, 

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

168 indent=" ", 

169 ) 

170 

171 if self.link is not None: 

172 yield "" 

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

174 

175 

176# 

177# Actual Errors 

178# 

179class ConfigurationError(PipError): 

180 """General exception in configuration""" 

181 

182 

183class InstallationError(PipError): 

184 """General exception during installation""" 

185 

186 

187class UninstallationError(PipError): 

188 """General exception during uninstallation""" 

189 

190 

191class MissingPyProjectBuildRequires(DiagnosticPipError): 

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

193 

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

195 

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

197 super().__init__( 

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

199 context=Text( 

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

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

202 ), 

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

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

205 ) 

206 

207 

208class InvalidPyProjectBuildRequires(DiagnosticPipError): 

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

210 

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

212 

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

214 super().__init__( 

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

216 context=Text( 

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

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

219 ), 

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

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

222 ) 

223 

224 

225class NoneMetadataError(PipError): 

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

227 

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

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

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

231 errors. 

232 """ 

233 

234 def __init__( 

235 self, 

236 dist: "BaseDistribution", 

237 metadata_name: str, 

238 ) -> None: 

239 """ 

240 :param dist: A Distribution object. 

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

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

243 """ 

244 self.dist = dist 

245 self.metadata_name = metadata_name 

246 

247 def __str__(self) -> str: 

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

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

250 return "None {} metadata found for distribution: {}".format( 

251 self.metadata_name, 

252 self.dist, 

253 ) 

254 

255 

256class UserInstallationInvalid(InstallationError): 

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

258 

259 def __str__(self) -> str: 

260 return "User base directory is not specified" 

261 

262 

263class InvalidSchemeCombination(InstallationError): 

264 def __str__(self) -> str: 

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

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

267 

268 

269class DistributionNotFound(InstallationError): 

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

271 

272 

273class RequirementsFileParseError(InstallationError): 

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

275 

276 

277class BestVersionAlreadyInstalled(PipError): 

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

279 installed.""" 

280 

281 

282class BadCommand(PipError): 

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

284 

285 

286class CommandError(PipError): 

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

288 

289 

290class PreviousBuildDirError(PipError): 

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

292 

293 

294class NetworkConnectionError(PipError): 

295 """HTTP connection error""" 

296 

297 def __init__( 

298 self, 

299 error_msg: str, 

300 response: Optional[Response] = None, 

301 request: Optional[Request] = None, 

302 ) -> None: 

303 """ 

304 Initialize NetworkConnectionError with `request` and `response` 

305 objects. 

306 """ 

307 self.response = response 

308 self.request = request 

309 self.error_msg = error_msg 

310 if ( 

311 self.response is not None 

312 and not self.request 

313 and hasattr(response, "request") 

314 ): 

315 self.request = self.response.request 

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

317 

318 def __str__(self) -> str: 

319 return str(self.error_msg) 

320 

321 

322class InvalidWheelFilename(InstallationError): 

323 """Invalid wheel filename.""" 

324 

325 

326class UnsupportedWheel(InstallationError): 

327 """Unsupported wheel.""" 

328 

329 

330class InvalidWheel(InstallationError): 

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

332 

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

334 self.location = location 

335 self.name = name 

336 

337 def __str__(self) -> str: 

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

339 

340 

341class MetadataInconsistent(InstallationError): 

342 """Built metadata contains inconsistent information. 

343 

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

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

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

347 """ 

348 

349 def __init__( 

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

351 ) -> None: 

352 self.ireq = ireq 

353 self.field = field 

354 self.f_val = f_val 

355 self.m_val = m_val 

356 

357 def __str__(self) -> str: 

358 return ( 

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

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

361 ) 

362 

363 

364class InstallationSubprocessError(DiagnosticPipError, InstallationError): 

365 """A subprocess call failed.""" 

366 

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

368 

369 def __init__( 

370 self, 

371 *, 

372 command_description: str, 

373 exit_code: int, 

374 output_lines: Optional[List[str]], 

375 ) -> None: 

376 if output_lines is None: 

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

378 else: 

379 output_prompt = ( 

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

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

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

383 ) 

384 

385 super().__init__( 

386 message=( 

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

388 f"exit code: {exit_code}" 

389 ), 

390 context=output_prompt, 

391 hint_stmt=None, 

392 note_stmt=( 

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

394 "problem with pip." 

395 ), 

396 ) 

397 

398 self.command_description = command_description 

399 self.exit_code = exit_code 

400 

401 def __str__(self) -> str: 

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

403 

404 

405class MetadataGenerationFailed(InstallationSubprocessError, InstallationError): 

406 reference = "metadata-generation-failed" 

407 

408 def __init__( 

409 self, 

410 *, 

411 package_details: str, 

412 ) -> None: 

413 super(InstallationSubprocessError, self).__init__( 

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

415 context=escape(package_details), 

416 hint_stmt="See above for details.", 

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

418 ) 

419 

420 def __str__(self) -> str: 

421 return "metadata generation failed" 

422 

423 

424class HashErrors(InstallationError): 

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

426 

427 def __init__(self) -> None: 

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

429 

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

431 self.errors.append(error) 

432 

433 def __str__(self) -> str: 

434 lines = [] 

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

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

437 lines.append(cls.head) 

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

439 if lines: 

440 return "\n".join(lines) 

441 return "" 

442 

443 def __bool__(self) -> bool: 

444 return bool(self.errors) 

445 

446 

447class HashError(InstallationError): 

448 """ 

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

450 

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

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

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

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

455 deterministic order. 

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

457 exceptions of this kind 

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

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

460 typically available earlier. 

461 

462 """ 

463 

464 req: Optional["InstallRequirement"] = None 

465 head = "" 

466 order: int = -1 

467 

468 def body(self) -> str: 

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

470 

471 This default implementation simply prints a description of the 

472 triggering requirement. 

473 

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

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

476 

477 """ 

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

479 

480 def __str__(self) -> str: 

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

482 

483 def _requirement_name(self) -> str: 

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

485 

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

487 line numbers 

488 

489 """ 

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

491 

492 

493class VcsHashUnsupported(HashError): 

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

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

496 

497 order = 0 

498 head = ( 

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

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

501 ) 

502 

503 

504class DirectoryUrlHashUnsupported(HashError): 

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

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

507 

508 order = 1 

509 head = ( 

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

511 "point to directories:" 

512 ) 

513 

514 

515class HashMissing(HashError): 

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

517 

518 order = 2 

519 head = ( 

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

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

522 "requirements along with the hashes their downloaded archives " 

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

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

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

526 "has a hash.)" 

527 ) 

528 

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

530 """ 

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

532 just downloaded 

533 """ 

534 self.gotten_hash = gotten_hash 

535 

536 def body(self) -> str: 

537 # Dodge circular import. 

538 from pip._internal.utils.hashes import FAVORITE_HASH 

539 

540 package = None 

541 if self.req: 

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

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

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

545 package = ( 

546 self.req.original_link 

547 if self.req.is_direct 

548 # In case someone feeds something downright stupid 

549 # to InstallRequirement's constructor. 

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

551 ) 

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

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

554 ) 

555 

556 

557class HashUnpinned(HashError): 

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

559 version.""" 

560 

561 order = 3 

562 head = ( 

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

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

565 ) 

566 

567 

568class HashMismatch(HashError): 

569 """ 

570 Distribution file hash values don't match. 

571 

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

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

574 improve its error message. 

575 

576 """ 

577 

578 order = 4 

579 head = ( 

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

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

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

583 "someone may have tampered with them." 

584 ) 

585 

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

587 """ 

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

589 hex digests 

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

591 actually got from the files under suspicion 

592 """ 

593 self.allowed = allowed 

594 self.gots = gots 

595 

596 def body(self) -> str: 

597 return " {}:\n{}".format(self._requirement_name(), self._hash_comparison()) 

598 

599 def _hash_comparison(self) -> str: 

600 """ 

601 Return a comparison of actual and expected hash values. 

602 

603 Example:: 

604 

605 Expected sha256 abcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcde 

606 or 123451234512345123451234512345123451234512345 

607 Got bcdefbcdefbcdefbcdefbcdefbcdefbcdefbcdefbcdef 

608 

609 """ 

610 

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

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

613 # away with hard-coding space literals. 

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

615 

616 lines: List[str] = [] 

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

618 prefix = hash_then_or(hash_name) 

619 lines.extend( 

620 (" Expected {} {}".format(next(prefix), e)) for e in expecteds 

621 ) 

622 lines.append( 

623 " Got {}\n".format(self.gots[hash_name].hexdigest()) 

624 ) 

625 return "\n".join(lines) 

626 

627 

628class UnsupportedPythonVersion(InstallationError): 

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

630 metadata.""" 

631 

632 

633class ConfigurationFileCouldNotBeLoaded(ConfigurationError): 

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

635 

636 def __init__( 

637 self, 

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

639 fname: Optional[str] = None, 

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

641 ) -> None: 

642 super().__init__(error) 

643 self.reason = reason 

644 self.fname = fname 

645 self.error = error 

646 

647 def __str__(self) -> str: 

648 if self.fname is not None: 

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

650 else: 

651 assert self.error is not None 

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

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

654 

655 

656_DEFAULT_EXTERNALLY_MANAGED_ERROR = f"""\ 

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

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

659the Python installation to interact with this environment instead. 

660""" 

661 

662 

663class ExternallyManagedEnvironment(DiagnosticPipError): 

664 """The current environment is externally managed. 

665 

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

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

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

669 

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

671 """ 

672 

673 reference = "externally-managed-environment" 

674 

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

676 if error is None: 

677 context = Text(_DEFAULT_EXTERNALLY_MANAGED_ERROR) 

678 else: 

679 context = Text(error) 

680 super().__init__( 

681 message="This environment is externally managed", 

682 context=context, 

683 note_stmt=( 

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

685 "Python installation or OS distribution provider. " 

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

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

688 ), 

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

690 ) 

691 

692 @staticmethod 

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

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

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

696 # using other categories for console message localization is equally 

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

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

699 try: 

700 category = locale.LC_MESSAGES 

701 except AttributeError: 

702 lang: Optional[str] = None 

703 else: 

704 lang, _ = locale.getlocale(category) 

705 if lang is not None: 

706 yield f"Error-{lang}" 

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

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

709 if not found: 

710 continue 

711 yield f"Error-{before}" 

712 yield "Error" 

713 

714 @classmethod 

715 def from_config( 

716 cls, 

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

718 ) -> "ExternallyManagedEnvironment": 

719 parser = configparser.ConfigParser(interpolation=None) 

720 try: 

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

722 section = parser["externally-managed"] 

723 for key in cls._iter_externally_managed_error_keys(): 

724 with contextlib.suppress(KeyError): 

725 return cls(section[key]) 

726 except KeyError: 

727 pass 

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

729 from pip._internal.utils._log import VERBOSE 

730 

731 exc_info = logger.isEnabledFor(VERBOSE) 

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

733 return cls(None)