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
« prev ^ index » next coverage.py v7.4.3, created at 2024-02-26 06:33 +0000
1"""Exceptions used throughout package.
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"""
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
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
23if TYPE_CHECKING:
24 from hashlib import _Hash
26 from pip._internal.metadata import BaseDistribution
27 from pip._internal.req.req_install import InstallRequirement
29logger = logging.getLogger(__name__)
32#
33# Scaffolding
34#
35def _is_kebab_case(s: str) -> bool:
36 return re.match(r"^[a-z]+(-[a-z]+)*$", s) is not None
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)
51 return console.render_str(prefix, overflow="ignore") + console.render_str(
52 f"\n{indent}", overflow="ignore"
53 ).join(text.split(allow_blank=True))
56class PipError(Exception):
57 """The base pip error."""
60class DiagnosticPipError(PipError):
61 """An error, that presents diagnostic information to the user.
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.
68 This is adapted from the error output styling in `sphinx-theme-builder`.
69 """
71 reference: str
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!"
90 self.kind = kind
91 self.reference = reference
93 self.message = message
94 self.context = context
96 self.note_stmt = note_stmt
97 self.hint_stmt = hint_stmt
99 self.link = link
101 super().__init__(f"<{self.__class__.__name__}: {self.reference}>")
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 )
114 def __rich_console__(
115 self,
116 console: Console,
117 options: ConsoleOptions,
118 ) -> RenderResult:
119 colour = "red" if self.kind == "error" else "yellow"
121 yield f"[{colour} bold]{self.kind}[/]: [bold]{self.reference}[/]"
122 yield ""
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
152 if self.note_stmt is not None or self.hint_stmt is not None:
153 yield ""
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 )
170 if self.link is not None:
171 yield ""
172 yield f"Link: {self.link}"
175#
176# Actual Errors
177#
178class ConfigurationError(PipError):
179 """General exception in configuration"""
182class InstallationError(PipError):
183 """General exception during installation"""
186class UninstallationError(PipError):
187 """General exception during uninstallation"""
190class MissingPyProjectBuildRequires(DiagnosticPipError):
191 """Raised when pyproject.toml has `build-system`, but no `build-system.requires`."""
193 reference = "missing-pyproject-build-system-requires"
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 )
207class InvalidPyProjectBuildRequires(DiagnosticPipError):
208 """Raised when pyproject.toml an invalid `build-system.requires`."""
210 reference = "invalid-pyproject-build-system-requires"
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 )
224class NoneMetadataError(PipError):
225 """Raised when accessing a Distribution's "METADATA" or "PKG-INFO".
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 """
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
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}"
252class UserInstallationInvalid(InstallationError):
253 """A --user install is requested on an environment without user site."""
255 def __str__(self) -> str:
256 return "User base directory is not specified"
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"
265class DistributionNotFound(InstallationError):
266 """Raised when a distribution cannot be found to satisfy a requirement"""
269class RequirementsFileParseError(InstallationError):
270 """Raised when a general error occurs parsing a requirements file line."""
273class BestVersionAlreadyInstalled(PipError):
274 """Raised when the most up-to-date version of a package is already
275 installed."""
278class BadCommand(PipError):
279 """Raised when virtualenv or a command is not found"""
282class CommandError(PipError):
283 """Raised when there is an error in command-line arguments"""
286class PreviousBuildDirError(PipError):
287 """Raised when there's a previous conflicting build directory"""
290class NetworkConnectionError(PipError):
291 """HTTP connection error"""
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)
314 def __str__(self) -> str:
315 return str(self.error_msg)
318class InvalidWheelFilename(InstallationError):
319 """Invalid wheel filename."""
322class UnsupportedWheel(InstallationError):
323 """Unsupported wheel."""
326class InvalidWheel(InstallationError):
327 """Invalid (e.g. corrupt) wheel."""
329 def __init__(self, location: str, name: str):
330 self.location = location
331 self.name = name
333 def __str__(self) -> str:
334 return f"Wheel '{self.name}' located at {self.location} is invalid."
337class MetadataInconsistent(InstallationError):
338 """Built metadata contains inconsistent information.
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 """
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
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 )
360class InstallationSubprocessError(DiagnosticPipError, InstallationError):
361 """A subprocess call failed."""
363 reference = "subprocess-exited-with-error"
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 )
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 )
394 self.command_description = command_description
395 self.exit_code = exit_code
397 def __str__(self) -> str:
398 return f"{self.command_description} exited with {self.exit_code}"
401class MetadataGenerationFailed(InstallationSubprocessError, InstallationError):
402 reference = "metadata-generation-failed"
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 )
416 def __str__(self) -> str:
417 return "metadata generation failed"
420class HashErrors(InstallationError):
421 """Multiple HashError instances rolled into one for reporting"""
423 def __init__(self) -> None:
424 self.errors: List["HashError"] = []
426 def append(self, error: "HashError") -> None:
427 self.errors.append(error)
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 ""
439 def __bool__(self) -> bool:
440 return bool(self.errors)
443class HashError(InstallationError):
444 """
445 A failure to verify a package against known-good hashes
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.
458 """
460 req: Optional["InstallRequirement"] = None
461 head = ""
462 order: int = -1
464 def body(self) -> str:
465 """Return a summary of me for display under the heading.
467 This default implementation simply prints a description of the
468 triggering requirement.
470 :param req: The InstallRequirement that provoked this error, with
471 its link already populated by the resolver's _populate_link().
473 """
474 return f" {self._requirement_name()}"
476 def __str__(self) -> str:
477 return f"{self.head}\n{self.body()}"
479 def _requirement_name(self) -> str:
480 """Return a description of the requirement that triggered me.
482 This default implementation returns long description of the req, with
483 line numbers
485 """
486 return str(self.req) if self.req else "unknown package"
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."""
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 )
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."""
504 order = 1
505 head = (
506 "Can't verify hashes for these file:// requirements because they "
507 "point to directories:"
508 )
511class HashMissing(HashError):
512 """A hash was needed for a requirement but is absent."""
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 )
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
532 def body(self) -> str:
533 # Dodge circular import.
534 from pip._internal.utils.hashes import FAVORITE_HASH
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 )
553class HashUnpinned(HashError):
554 """A requirement had a hash specified but was not pinned to a specific
555 version."""
557 order = 3
558 head = (
559 "In --require-hashes mode, all requirements must have their "
560 "versions pinned with ==. These do not:"
561 )
564class HashMismatch(HashError):
565 """
566 Distribution file hash values don't match.
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.
572 """
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 )
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
592 def body(self) -> str:
593 return f" {self._requirement_name()}:\n{self._hash_comparison()}"
595 def _hash_comparison(self) -> str:
596 """
597 Return a comparison of actual and expected hash values.
599 Example::
601 Expected sha256 abcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcde
602 or 123451234512345123451234512345123451234512345
603 Got bcdefbcdefbcdefbcdefbcdefbcdefbcdefbcdefbcdef
605 """
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"))
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)
622class UnsupportedPythonVersion(InstallationError):
623 """Unsupported python version according to Requires-Python package
624 metadata."""
627class ConfigurationFileCouldNotBeLoaded(ConfigurationError):
628 """When there are errors while loading a configuration file"""
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
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}"
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"""
657class ExternallyManagedEnvironment(DiagnosticPipError):
658 """The current environment is externally managed.
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.
664 :param error: The error message read from ``EXTERNALLY-MANAGED``.
665 """
667 reference = "externally-managed-environment"
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 )
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"
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
725 exc_info = logger.isEnabledFor(VERBOSE)
726 logger.warning("Failed to read %s", config, exc_info=exc_info)
727 return cls(None)