385 lines
12 KiB
Python
385 lines
12 KiB
Python
"""Exceptions used throughout package"""
|
|
|
|
from itertools import chain, groupby, repeat
|
|
|
|
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
|
|
|
|
if MYPY_CHECK_RUNNING:
|
|
import configparser
|
|
from hashlib import _Hash
|
|
from typing import Dict, List, Optional
|
|
|
|
from pip._vendor.pkg_resources import Distribution
|
|
from pip._vendor.requests.models import Request, Response
|
|
|
|
from pip._internal.req.req_install import InstallRequirement
|
|
|
|
|
|
class PipError(Exception):
|
|
"""Base pip exception"""
|
|
|
|
|
|
class ConfigurationError(PipError):
|
|
"""General exception in configuration"""
|
|
|
|
|
|
class InstallationError(PipError):
|
|
"""General exception during installation"""
|
|
|
|
|
|
class UninstallationError(PipError):
|
|
"""General exception during uninstallation"""
|
|
|
|
|
|
class NoneMetadataError(PipError):
|
|
"""
|
|
Raised when accessing "METADATA" or "PKG-INFO" metadata for a
|
|
pip._vendor.pkg_resources.Distribution object and
|
|
`dist.has_metadata('METADATA')` returns True but
|
|
`dist.get_metadata('METADATA')` returns None (and similarly for
|
|
"PKG-INFO").
|
|
"""
|
|
|
|
def __init__(self, dist, metadata_name):
|
|
# type: (Distribution, 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):
|
|
# type: () -> 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 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, response=None, request=None):
|
|
# type: (str, Response, Request) -> 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):
|
|
# type: () -> str
|
|
return str(self.error_msg)
|
|
|
|
|
|
class InvalidWheelFilename(InstallationError):
|
|
"""Invalid wheel filename."""
|
|
|
|
|
|
class UnsupportedWheel(InstallationError):
|
|
"""Unsupported wheel."""
|
|
|
|
|
|
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
|
|
or user-supplied ``#egg=`` value.
|
|
"""
|
|
def __init__(self, ireq, field, f_val, m_val):
|
|
# type: (InstallRequirement, str, str, str) -> None
|
|
self.ireq = ireq
|
|
self.field = field
|
|
self.f_val = f_val
|
|
self.m_val = m_val
|
|
|
|
def __str__(self):
|
|
# type: () -> str
|
|
template = (
|
|
"Requested {} has inconsistent {}: "
|
|
"filename has {!r}, but metadata has {!r}"
|
|
)
|
|
return template.format(self.ireq, self.field, self.f_val, self.m_val)
|
|
|
|
|
|
class InstallationSubprocessError(InstallationError):
|
|
"""A subprocess call failed during installation."""
|
|
def __init__(self, returncode, description):
|
|
# type: (int, str) -> None
|
|
self.returncode = returncode
|
|
self.description = description
|
|
|
|
def __str__(self):
|
|
# type: () -> str
|
|
return (
|
|
"Command errored out with exit status {}: {} "
|
|
"Check the logs for full command output."
|
|
).format(self.returncode, self.description)
|
|
|
|
|
|
class HashErrors(InstallationError):
|
|
"""Multiple HashError instances rolled into one for reporting"""
|
|
|
|
def __init__(self):
|
|
# type: () -> None
|
|
self.errors = [] # type: List[HashError]
|
|
|
|
def append(self, error):
|
|
# type: (HashError) -> None
|
|
self.errors.append(error)
|
|
|
|
def __str__(self):
|
|
# type: () -> 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 __nonzero__(self):
|
|
# type: () -> bool
|
|
return bool(self.errors)
|
|
|
|
def __bool__(self):
|
|
# type: () -> bool
|
|
return self.__nonzero__()
|
|
|
|
|
|
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 = None # type: Optional[InstallRequirement]
|
|
head = ''
|
|
order = -1 # type: int
|
|
|
|
def body(self):
|
|
# type: () -> 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):
|
|
# type: () -> str
|
|
return f'{self.head}\n{self.body()}'
|
|
|
|
def _requirement_name(self):
|
|
# type: () -> 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):
|
|
# type: (str) -> None
|
|
"""
|
|
:param gotten_hash: The hash of the (possibly malicious) archive we
|
|
just downloaded
|
|
"""
|
|
self.gotten_hash = gotten_hash
|
|
|
|
def body(self):
|
|
# type: () -> 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.original_link
|
|
# 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, gots):
|
|
# type: (Dict[str, List[str]], 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):
|
|
# type: () -> str
|
|
return ' {}:\n{}'.format(self._requirement_name(),
|
|
self._hash_comparison())
|
|
|
|
def _hash_comparison(self):
|
|
# type: () -> str
|
|
"""
|
|
Return a comparison of actual and expected hash values.
|
|
|
|
Example::
|
|
|
|
Expected sha256 abcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcde
|
|
or 123451234512345123451234512345123451234512345
|
|
Got bcdefbcdefbcdefbcdefbcdefbcdefbcdefbcdefbcdef
|
|
|
|
"""
|
|
def hash_then_or(hash_name):
|
|
# type: (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 = [] # type: 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="could not be loaded", fname=None, error=None):
|
|
# type: (str, Optional[str], Optional[configparser.Error]) -> None
|
|
super().__init__(error)
|
|
self.reason = reason
|
|
self.fname = fname
|
|
self.error = error
|
|
|
|
def __str__(self):
|
|
# type: () -> 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}"
|