123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733 |
- """Exceptions used throughout package.
- This module MUST NOT try to import from anything within `pip._internal` to
- operate. This is expected to be importable from any/all files within the
- subpackage and, thus, should not depend on them.
- """
- import configparser
- import contextlib
- import locale
- import logging
- import pathlib
- import re
- import sys
- from itertools import chain, groupby, repeat
- from typing import TYPE_CHECKING, Dict, Iterator, List, Optional, Union
- from pip._vendor.requests.models import Request, Response
- from pip._vendor.rich.console import Console, ConsoleOptions, RenderResult
- from pip._vendor.rich.markup import escape
- from pip._vendor.rich.text import Text
- if TYPE_CHECKING:
- from hashlib import _Hash
- from typing import Literal
- from pip._internal.metadata import BaseDistribution
- from pip._internal.req.req_install import InstallRequirement
- logger = logging.getLogger(__name__)
- #
- # Scaffolding
- #
- def _is_kebab_case(s: str) -> bool:
- return re.match(r"^[a-z]+(-[a-z]+)*$", s) is not None
- def _prefix_with_indent(
- s: Union[Text, str],
- console: Console,
- *,
- prefix: str,
- indent: str,
- ) -> Text:
- if isinstance(s, Text):
- text = s
- else:
- text = console.render_str(s)
- return console.render_str(prefix, overflow="ignore") + console.render_str(
- f"\n{indent}", overflow="ignore"
- ).join(text.split(allow_blank=True))
- class PipError(Exception):
- """The base pip error."""
- class DiagnosticPipError(PipError):
- """An error, that presents diagnostic information to the user.
- This contains a bunch of logic, to enable pretty presentation of our error
- messages. Each error gets a unique reference. Each error can also include
- additional context, a hint and/or a note -- which are presented with the
- main error message in a consistent style.
- This is adapted from the error output styling in `sphinx-theme-builder`.
- """
- reference: str
- def __init__(
- self,
- *,
- kind: 'Literal["error", "warning"]' = "error",
- reference: Optional[str] = None,
- message: Union[str, Text],
- context: Optional[Union[str, Text]],
- hint_stmt: Optional[Union[str, Text]],
- note_stmt: Optional[Union[str, Text]] = None,
- link: Optional[str] = None,
- ) -> None:
- # Ensure a proper reference is provided.
- if reference is None:
- assert hasattr(self, "reference"), "error reference not provided!"
- reference = self.reference
- assert _is_kebab_case(reference), "error reference must be kebab-case!"
- self.kind = kind
- self.reference = reference
- self.message = message
- self.context = context
- self.note_stmt = note_stmt
- self.hint_stmt = hint_stmt
- self.link = link
- super().__init__(f"<{self.__class__.__name__}: {self.reference}>")
- def __repr__(self) -> str:
- return (
- f"<{self.__class__.__name__}("
- f"reference={self.reference!r}, "
- f"message={self.message!r}, "
- f"context={self.context!r}, "
- f"note_stmt={self.note_stmt!r}, "
- f"hint_stmt={self.hint_stmt!r}"
- ")>"
- )
- def __rich_console__(
- self,
- console: Console,
- options: ConsoleOptions,
- ) -> RenderResult:
- colour = "red" if self.kind == "error" else "yellow"
- yield f"[{colour} bold]{self.kind}[/]: [bold]{self.reference}[/]"
- yield ""
- if not options.ascii_only:
- # Present the main message, with relevant context indented.
- if self.context is not None:
- yield _prefix_with_indent(
- self.message,
- console,
- prefix=f"[{colour}]×[/] ",
- indent=f"[{colour}]│[/] ",
- )
- yield _prefix_with_indent(
- self.context,
- console,
- prefix=f"[{colour}]╰─>[/] ",
- indent=f"[{colour}] [/] ",
- )
- else:
- yield _prefix_with_indent(
- self.message,
- console,
- prefix="[red]×[/] ",
- indent=" ",
- )
- else:
- yield self.message
- if self.context is not None:
- yield ""
- yield self.context
- if self.note_stmt is not None or self.hint_stmt is not None:
- yield ""
- if self.note_stmt is not None:
- yield _prefix_with_indent(
- self.note_stmt,
- console,
- prefix="[magenta bold]note[/]: ",
- indent=" ",
- )
- if self.hint_stmt is not None:
- yield _prefix_with_indent(
- self.hint_stmt,
- console,
- prefix="[cyan bold]hint[/]: ",
- indent=" ",
- )
- if self.link is not None:
- yield ""
- yield f"Link: {self.link}"
- #
- # Actual Errors
- #
- class ConfigurationError(PipError):
- """General exception in configuration"""
- class InstallationError(PipError):
- """General exception during installation"""
- class UninstallationError(PipError):
- """General exception during uninstallation"""
- class MissingPyProjectBuildRequires(DiagnosticPipError):
- """Raised when pyproject.toml has `build-system`, but no `build-system.requires`."""
- reference = "missing-pyproject-build-system-requires"
- def __init__(self, *, package: str) -> None:
- super().__init__(
- message=f"Can not process {escape(package)}",
- context=Text(
- "This package has an invalid pyproject.toml file.\n"
- "The [build-system] table is missing the mandatory `requires` key."
- ),
- note_stmt="This is an issue with the package mentioned above, not pip.",
- hint_stmt=Text("See PEP 518 for the detailed specification."),
- )
- class InvalidPyProjectBuildRequires(DiagnosticPipError):
- """Raised when pyproject.toml an invalid `build-system.requires`."""
- reference = "invalid-pyproject-build-system-requires"
- def __init__(self, *, package: str, reason: str) -> None:
- super().__init__(
- message=f"Can not process {escape(package)}",
- context=Text(
- "This package has an invalid `build-system.requires` key in "
- f"pyproject.toml.\n{reason}"
- ),
- note_stmt="This is an issue with the package mentioned above, not pip.",
- hint_stmt=Text("See PEP 518 for the detailed specification."),
- )
- class NoneMetadataError(PipError):
- """Raised when accessing a Distribution's "METADATA" or "PKG-INFO".
- This signifies an inconsistency, when the Distribution claims to have
- the metadata file (if not, raise ``FileNotFoundError`` instead), but is
- not actually able to produce its content. This may be due to permission
- errors.
- """
- def __init__(
- self,
- dist: "BaseDistribution",
- metadata_name: str,
- ) -> None:
- """
- :param dist: A Distribution object.
- :param metadata_name: The name of the metadata being accessed
- (can be "METADATA" or "PKG-INFO").
- """
- self.dist = dist
- self.metadata_name = metadata_name
- def __str__(self) -> str:
- # Use `dist` in the error message because its stringification
- # includes more information, like the version and location.
- return "None {} metadata found for distribution: {}".format(
- self.metadata_name,
- self.dist,
- )
- class UserInstallationInvalid(InstallationError):
- """A --user install is requested on an environment without user site."""
- def __str__(self) -> str:
- return "User base directory is not specified"
- class InvalidSchemeCombination(InstallationError):
- def __str__(self) -> str:
- before = ", ".join(str(a) for a in self.args[:-1])
- return f"Cannot set {before} and {self.args[-1]} together"
- class DistributionNotFound(InstallationError):
- """Raised when a distribution cannot be found to satisfy a requirement"""
- class RequirementsFileParseError(InstallationError):
- """Raised when a general error occurs parsing a requirements file line."""
- class BestVersionAlreadyInstalled(PipError):
- """Raised when the most up-to-date version of a package is already
- installed."""
- class BadCommand(PipError):
- """Raised when virtualenv or a command is not found"""
- class CommandError(PipError):
- """Raised when there is an error in command-line arguments"""
- class PreviousBuildDirError(PipError):
- """Raised when there's a previous conflicting build directory"""
- class NetworkConnectionError(PipError):
- """HTTP connection error"""
- def __init__(
- self,
- error_msg: str,
- response: Optional[Response] = None,
- request: Optional[Request] = None,
- ) -> None:
- """
- Initialize NetworkConnectionError with `request` and `response`
- objects.
- """
- self.response = response
- self.request = request
- self.error_msg = error_msg
- if (
- self.response is not None
- and not self.request
- and hasattr(response, "request")
- ):
- self.request = self.response.request
- super().__init__(error_msg, response, request)
- def __str__(self) -> str:
- return str(self.error_msg)
- class InvalidWheelFilename(InstallationError):
- """Invalid wheel filename."""
- class UnsupportedWheel(InstallationError):
- """Unsupported wheel."""
- class InvalidWheel(InstallationError):
- """Invalid (e.g. corrupt) wheel."""
- def __init__(self, location: str, name: str):
- self.location = location
- self.name = name
- def __str__(self) -> str:
- return f"Wheel '{self.name}' located at {self.location} is invalid."
- class MetadataInconsistent(InstallationError):
- """Built metadata contains inconsistent information.
- This is raised when the metadata contains values (e.g. name and version)
- that do not match the information previously obtained from sdist filename,
- user-supplied ``#egg=`` value, or an install requirement name.
- """
- def __init__(
- self, ireq: "InstallRequirement", field: str, f_val: str, m_val: str
- ) -> None:
- self.ireq = ireq
- self.field = field
- self.f_val = f_val
- self.m_val = m_val
- def __str__(self) -> str:
- return (
- f"Requested {self.ireq} has inconsistent {self.field}: "
- f"expected {self.f_val!r}, but metadata has {self.m_val!r}"
- )
- class InstallationSubprocessError(DiagnosticPipError, InstallationError):
- """A subprocess call failed."""
- reference = "subprocess-exited-with-error"
- def __init__(
- self,
- *,
- command_description: str,
- exit_code: int,
- output_lines: Optional[List[str]],
- ) -> None:
- if output_lines is None:
- output_prompt = Text("See above for output.")
- else:
- output_prompt = (
- Text.from_markup(f"[red][{len(output_lines)} lines of output][/]\n")
- + Text("".join(output_lines))
- + Text.from_markup(R"[red]\[end of output][/]")
- )
- super().__init__(
- message=(
- f"[green]{escape(command_description)}[/] did not run successfully.\n"
- f"exit code: {exit_code}"
- ),
- context=output_prompt,
- hint_stmt=None,
- note_stmt=(
- "This error originates from a subprocess, and is likely not a "
- "problem with pip."
- ),
- )
- self.command_description = command_description
- self.exit_code = exit_code
- def __str__(self) -> str:
- return f"{self.command_description} exited with {self.exit_code}"
- class MetadataGenerationFailed(InstallationSubprocessError, InstallationError):
- reference = "metadata-generation-failed"
- def __init__(
- self,
- *,
- package_details: str,
- ) -> None:
- super(InstallationSubprocessError, self).__init__(
- message="Encountered error while generating package metadata.",
- context=escape(package_details),
- hint_stmt="See above for details.",
- note_stmt="This is an issue with the package mentioned above, not pip.",
- )
- def __str__(self) -> str:
- return "metadata generation failed"
- class HashErrors(InstallationError):
- """Multiple HashError instances rolled into one for reporting"""
- def __init__(self) -> None:
- self.errors: List["HashError"] = []
- def append(self, error: "HashError") -> None:
- self.errors.append(error)
- def __str__(self) -> str:
- lines = []
- self.errors.sort(key=lambda e: e.order)
- for cls, errors_of_cls in groupby(self.errors, lambda e: e.__class__):
- lines.append(cls.head)
- lines.extend(e.body() for e in errors_of_cls)
- if lines:
- return "\n".join(lines)
- return ""
- def __bool__(self) -> bool:
- return bool(self.errors)
- class HashError(InstallationError):
- """
- A failure to verify a package against known-good hashes
- :cvar order: An int sorting hash exception classes by difficulty of
- recovery (lower being harder), so the user doesn't bother fretting
- about unpinned packages when he has deeper issues, like VCS
- dependencies, to deal with. Also keeps error reports in a
- deterministic order.
- :cvar head: A section heading for display above potentially many
- exceptions of this kind
- :ivar req: The InstallRequirement that triggered this error. This is
- pasted on after the exception is instantiated, because it's not
- typically available earlier.
- """
- req: Optional["InstallRequirement"] = None
- head = ""
- order: int = -1
- def body(self) -> str:
- """Return a summary of me for display under the heading.
- This default implementation simply prints a description of the
- triggering requirement.
- :param req: The InstallRequirement that provoked this error, with
- its link already populated by the resolver's _populate_link().
- """
- return f" {self._requirement_name()}"
- def __str__(self) -> str:
- return f"{self.head}\n{self.body()}"
- def _requirement_name(self) -> str:
- """Return a description of the requirement that triggered me.
- This default implementation returns long description of the req, with
- line numbers
- """
- return str(self.req) if self.req else "unknown package"
- class VcsHashUnsupported(HashError):
- """A hash was provided for a version-control-system-based requirement, but
- we don't have a method for hashing those."""
- order = 0
- head = (
- "Can't verify hashes for these requirements because we don't "
- "have a way to hash version control repositories:"
- )
- class DirectoryUrlHashUnsupported(HashError):
- """A hash was provided for a version-control-system-based requirement, but
- we don't have a method for hashing those."""
- order = 1
- head = (
- "Can't verify hashes for these file:// requirements because they "
- "point to directories:"
- )
- class HashMissing(HashError):
- """A hash was needed for a requirement but is absent."""
- order = 2
- head = (
- "Hashes are required in --require-hashes mode, but they are "
- "missing from some requirements. Here is a list of those "
- "requirements along with the hashes their downloaded archives "
- "actually had. Add lines like these to your requirements files to "
- "prevent tampering. (If you did not enable --require-hashes "
- "manually, note that it turns on automatically when any package "
- "has a hash.)"
- )
- def __init__(self, gotten_hash: str) -> None:
- """
- :param gotten_hash: The hash of the (possibly malicious) archive we
- just downloaded
- """
- self.gotten_hash = gotten_hash
- def body(self) -> str:
- # Dodge circular import.
- from pip._internal.utils.hashes import FAVORITE_HASH
- package = None
- if self.req:
- # In the case of URL-based requirements, display the original URL
- # seen in the requirements file rather than the package name,
- # so the output can be directly copied into the requirements file.
- package = (
- self.req.original_link
- if self.req.is_direct
- # In case someone feeds something downright stupid
- # to InstallRequirement's constructor.
- else getattr(self.req, "req", None)
- )
- return " {} --hash={}:{}".format(
- package or "unknown package", FAVORITE_HASH, self.gotten_hash
- )
- class HashUnpinned(HashError):
- """A requirement had a hash specified but was not pinned to a specific
- version."""
- order = 3
- head = (
- "In --require-hashes mode, all requirements must have their "
- "versions pinned with ==. These do not:"
- )
- class HashMismatch(HashError):
- """
- Distribution file hash values don't match.
- :ivar package_name: The name of the package that triggered the hash
- mismatch. Feel free to write to this after the exception is raise to
- improve its error message.
- """
- order = 4
- head = (
- "THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS "
- "FILE. If you have updated the package versions, please update "
- "the hashes. Otherwise, examine the package contents carefully; "
- "someone may have tampered with them."
- )
- def __init__(self, allowed: Dict[str, List[str]], gots: Dict[str, "_Hash"]) -> None:
- """
- :param allowed: A dict of algorithm names pointing to lists of allowed
- hex digests
- :param gots: A dict of algorithm names pointing to hashes we
- actually got from the files under suspicion
- """
- self.allowed = allowed
- self.gots = gots
- def body(self) -> str:
- return " {}:\n{}".format(self._requirement_name(), self._hash_comparison())
- def _hash_comparison(self) -> str:
- """
- Return a comparison of actual and expected hash values.
- Example::
- Expected sha256 abcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcde
- or 123451234512345123451234512345123451234512345
- Got bcdefbcdefbcdefbcdefbcdefbcdefbcdefbcdefbcdef
- """
- def hash_then_or(hash_name: str) -> "chain[str]":
- # For now, all the decent hashes have 6-char names, so we can get
- # away with hard-coding space literals.
- return chain([hash_name], repeat(" or"))
- lines: List[str] = []
- for hash_name, expecteds in self.allowed.items():
- prefix = hash_then_or(hash_name)
- lines.extend(
- (" Expected {} {}".format(next(prefix), e)) for e in expecteds
- )
- lines.append(
- " Got {}\n".format(self.gots[hash_name].hexdigest())
- )
- return "\n".join(lines)
- class UnsupportedPythonVersion(InstallationError):
- """Unsupported python version according to Requires-Python package
- metadata."""
- class ConfigurationFileCouldNotBeLoaded(ConfigurationError):
- """When there are errors while loading a configuration file"""
- def __init__(
- self,
- reason: str = "could not be loaded",
- fname: Optional[str] = None,
- error: Optional[configparser.Error] = None,
- ) -> None:
- super().__init__(error)
- self.reason = reason
- self.fname = fname
- self.error = error
- def __str__(self) -> str:
- if self.fname is not None:
- message_part = f" in {self.fname}."
- else:
- assert self.error is not None
- message_part = f".\n{self.error}\n"
- return f"Configuration file {self.reason}{message_part}"
- _DEFAULT_EXTERNALLY_MANAGED_ERROR = f"""\
- The Python environment under {sys.prefix} is managed externally, and may not be
- manipulated by the user. Please use specific tooling from the distributor of
- the Python installation to interact with this environment instead.
- """
- class ExternallyManagedEnvironment(DiagnosticPipError):
- """The current environment is externally managed.
- This is raised when the current environment is externally managed, as
- defined by `PEP 668`_. The ``EXTERNALLY-MANAGED`` configuration is checked
- and displayed when the error is bubbled up to the user.
- :param error: The error message read from ``EXTERNALLY-MANAGED``.
- """
- reference = "externally-managed-environment"
- def __init__(self, error: Optional[str]) -> None:
- if error is None:
- context = Text(_DEFAULT_EXTERNALLY_MANAGED_ERROR)
- else:
- context = Text(error)
- super().__init__(
- message="This environment is externally managed",
- context=context,
- note_stmt=(
- "If you believe this is a mistake, please contact your "
- "Python installation or OS distribution provider. "
- "You can override this, at the risk of breaking your Python "
- "installation or OS, by passing --break-system-packages."
- ),
- hint_stmt=Text("See PEP 668 for the detailed specification."),
- )
- @staticmethod
- def _iter_externally_managed_error_keys() -> Iterator[str]:
- # LC_MESSAGES is in POSIX, but not the C standard. The most common
- # platform that does not implement this category is Windows, where
- # using other categories for console message localization is equally
- # unreliable, so we fall back to the locale-less vendor message. This
- # can always be re-evaluated when a vendor proposes a new alternative.
- try:
- category = locale.LC_MESSAGES
- except AttributeError:
- lang: Optional[str] = None
- else:
- lang, _ = locale.getlocale(category)
- if lang is not None:
- yield f"Error-{lang}"
- for sep in ("-", "_"):
- before, found, _ = lang.partition(sep)
- if not found:
- continue
- yield f"Error-{before}"
- yield "Error"
- @classmethod
- def from_config(
- cls,
- config: Union[pathlib.Path, str],
- ) -> "ExternallyManagedEnvironment":
- parser = configparser.ConfigParser(interpolation=None)
- try:
- parser.read(config, encoding="utf-8")
- section = parser["externally-managed"]
- for key in cls._iter_externally_managed_error_keys():
- with contextlib.suppress(KeyError):
- return cls(section[key])
- except KeyError:
- pass
- except (OSError, UnicodeDecodeError, configparser.ParsingError):
- from pip._internal.utils._log import VERBOSE
- exc_info = logger.isEnabledFor(VERBOSE)
- logger.warning("Failed to read %s", config, exc_info=exc_info)
- return cls(None)
|