1 """Exceptions used throughout package.
3 This module MUST NOT try to import from anything within `pip._internal` to
4 operate. This is expected to be importable from any/all files within the
5 subpackage and, thus, should not depend on them.
15 from itertools
import chain
, groupby
, repeat
16 from typing
import TYPE_CHECKING
, Dict
, Iterator
, List
, Optional
, Union
18 from pip
._vendor
.requests
.models
import Request
, Response
19 from pip
._vendor
.rich
.console
import Console
, ConsoleOptions
, RenderResult
20 from pip
._vendor
.rich
.markup
import escape
21 from pip
._vendor
.rich
.text
import Text
24 from hashlib
import _Hash
25 from typing
import Literal
27 from pip
._internal
.metadata
import BaseDistribution
28 from pip
._internal
.req
.req_install
import InstallRequirement
30 logger
= logging
.getLogger(__name__
)
36 def _is_kebab_case(s
: str) -> bool:
37 return re
.match(r
"^[a-z]+(-[a-z]+)*$", s
) is not None
40 def _prefix_with_indent(
47 if isinstance(s
, Text
):
50 text
= console
.render_str(s
)
52 return console
.render_str(prefix
, overflow
="ignore") + console
.render_str(
53 f
"\n{indent}", overflow
="ignore"
54 ).join(text
.split(allow_blank
=True))
57 class PipError(Exception):
58 """The base pip error."""
61 class DiagnosticPipError(PipError
):
62 """An error, that presents diagnostic information to the user.
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.
69 This is adapted from the error output styling in `sphinx-theme-builder`.
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,
85 # Ensure a proper reference is provided.
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!"
92 self
.reference
= reference
94 self
.message
= message
95 self
.context
= context
97 self
.note_stmt
= note_stmt
98 self
.hint_stmt
= hint_stmt
102 super().__init
__(f
"<{self.__class__.__name__}: {self.reference}>")
104 def __repr__(self
) -> str:
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}"
115 def __rich_console__(
118 options
: ConsoleOptions
,
120 colour
= "red" if self
.kind
== "error" else "yellow"
122 yield f
"[{colour} bold]{self.kind}[/]: [bold]{self.reference}[/]"
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(
131 prefix
=f
"[{colour}]×[/] ",
132 indent
=f
"[{colour}]│[/] ",
134 yield _prefix_with_indent(
137 prefix
=f
"[{colour}]╰─>[/] ",
138 indent
=f
"[{colour}] [/] ",
141 yield _prefix_with_indent(
149 if self
.context
is not None:
153 if self
.note_stmt
is not None or self
.hint_stmt
is not None:
156 if self
.note_stmt
is not None:
157 yield _prefix_with_indent(
160 prefix
="[magenta bold]note[/]: ",
163 if self
.hint_stmt
is not None:
164 yield _prefix_with_indent(
167 prefix
="[cyan bold]hint[/]: ",
171 if self
.link
is not None:
173 yield f
"Link: {self.link}"
179 class ConfigurationError(PipError
):
180 """General exception in configuration"""
183 class InstallationError(PipError
):
184 """General exception during installation"""
187 class UninstallationError(PipError
):
188 """General exception during uninstallation"""
191 class MissingPyProjectBuildRequires(DiagnosticPipError
):
192 """Raised when pyproject.toml has `build-system`, but no `build-system.requires`."""
194 reference
= "missing-pyproject-build-system-requires"
196 def __init__(self
, *, package
: str) -> None:
198 message
=f
"Can not process {escape(package)}",
200 "This package has an invalid pyproject.toml file.\n"
201 "The [build-system] table is missing the mandatory `requires` key."
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."),
208 class InvalidPyProjectBuildRequires(DiagnosticPipError
):
209 """Raised when pyproject.toml an invalid `build-system.requires`."""
211 reference
= "invalid-pyproject-build-system-requires"
213 def __init__(self
, *, package
: str, reason
: str) -> None:
215 message
=f
"Can not process {escape(package)}",
217 "This package has an invalid `build-system.requires` key in "
218 f
"pyproject.toml.\n{reason}"
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."),
225 class NoneMetadataError(PipError
):
226 """Raised when accessing a Distribution's "METADATA" or "PKG-INFO".
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
236 dist
: "BaseDistribution",
240 :param dist: A Distribution object.
241 :param metadata_name: The name of the metadata being accessed
242 (can be "METADATA" or "PKG-INFO").
245 self
.metadata_name
= metadata_name
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(
256 class UserInstallationInvalid(InstallationError
):
257 """A --user install is requested on an environment without user site."""
259 def __str__(self
) -> str:
260 return "User base directory is not specified"
263 class 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"
269 class DistributionNotFound(InstallationError
):
270 """Raised when a distribution cannot be found to satisfy a requirement"""
273 class RequirementsFileParseError(InstallationError
):
274 """Raised when a general error occurs parsing a requirements file line."""
277 class BestVersionAlreadyInstalled(PipError
):
278 """Raised when the most up-to-date version of a package is already
282 class BadCommand(PipError
):
283 """Raised when virtualenv or a command is not found"""
286 class CommandError(PipError
):
287 """Raised when there is an error in command-line arguments"""
290 class PreviousBuildDirError(PipError
):
291 """Raised when there's a previous conflicting build directory"""
294 class NetworkConnectionError(PipError
):
295 """HTTP connection error"""
300 response
: Optional
[Response
] = None,
301 request
: Optional
[Request
] = None,
304 Initialize NetworkConnectionError with `request` and `response`
307 self
.response
= response
308 self
.request
= request
309 self
.error_msg
= error_msg
311 self
.response
is not None
313 and hasattr(response
, "request")
315 self
.request
= self
.response
.request
316 super().__init
__(error_msg
, response
, request
)
318 def __str__(self
) -> str:
319 return str(self
.error_msg
)
322 class InvalidWheelFilename(InstallationError
):
323 """Invalid wheel filename."""
326 class UnsupportedWheel(InstallationError
):
327 """Unsupported wheel."""
330 class InvalidWheel(InstallationError
):
331 """Invalid (e.g. corrupt) wheel."""
333 def __init__(self
, location
: str, name
: str):
334 self
.location
= location
337 def __str__(self
) -> str:
338 return f
"Wheel '{self.name}' located at {self.location} is invalid."
341 class MetadataInconsistent(InstallationError
):
342 """Built metadata contains inconsistent information.
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.
350 self
, ireq
: "InstallRequirement", field
: str, f_val
: str, m_val
: str
357 def __str__(self
) -> str:
359 f
"Requested {self.ireq} has inconsistent {self.field}: "
360 f
"expected {self.f_val!r}, but metadata has {self.m_val!r}"
364 class InstallationSubprocessError(DiagnosticPipError
, InstallationError
):
365 """A subprocess call failed."""
367 reference
= "subprocess-exited-with-error"
372 command_description
: str,
374 output_lines
: Optional
[List
[str]],
376 if output_lines
is None:
377 output_prompt
= Text("See above for output.")
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][/]")
387 f
"[green]{escape(command_description)}[/] did not run successfully.\n"
388 f
"exit code: {exit_code}"
390 context
=output_prompt
,
393 "This error originates from a subprocess, and is likely not a "
398 self
.command_description
= command_description
399 self
.exit_code
= exit_code
401 def __str__(self
) -> str:
402 return f
"{self.command_description} exited with {self.exit_code}"
405 class MetadataGenerationFailed(InstallationSubprocessError
, InstallationError
):
406 reference
= "metadata-generation-failed"
411 package_details
: str,
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.",
420 def __str__(self
) -> str:
421 return "metadata generation failed"
424 class HashErrors(InstallationError
):
425 """Multiple HashError instances rolled into one for reporting"""
427 def __init__(self
) -> None:
428 self
.errors
: List
["HashError"] = []
430 def append(self
, error
: "HashError") -> None:
431 self
.errors
.append(error
)
433 def __str__(self
) -> str:
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
)
440 return "\n".join(lines
)
443 def __bool__(self
) -> bool:
444 return bool(self
.errors
)
447 class HashError(InstallationError
):
449 A failure to verify a package against known-good hashes
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
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.
464 req
: Optional
["InstallRequirement"] = None
468 def body(self
) -> str:
469 """Return a summary of me for display under the heading.
471 This default implementation simply prints a description of the
472 triggering requirement.
474 :param req: The InstallRequirement that provoked this error, with
475 its link already populated by the resolver's _populate_link().
478 return f
" {self._requirement_name()}"
480 def __str__(self
) -> str:
481 return f
"{self.head}\n{self.body()}"
483 def _requirement_name(self
) -> str:
484 """Return a description of the requirement that triggered me.
486 This default implementation returns long description of the req, with
490 return str(self
.req
) if self
.req
else "unknown package"
493 class 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."""
499 "Can't verify hashes for these requirements because we don't "
500 "have a way to hash version control repositories:"
504 class 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."""
510 "Can't verify hashes for these file:// requirements because they "
511 "point to directories:"
515 class HashMissing(HashError
):
516 """A hash was needed for a requirement but is absent."""
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 "
529 def __init__(self
, gotten_hash
: str) -> None:
531 :param gotten_hash: The hash of the (possibly malicious) archive we
534 self
.gotten_hash
= gotten_hash
536 def body(self
) -> str:
537 # Dodge circular import.
538 from pip
._internal
.utils
.hashes
import FAVORITE_HASH
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.
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)
552 return " {} --hash={}:{}".format(
553 package
or "unknown package", FAVORITE_HASH
, self
.gotten_hash
557 class HashUnpinned(HashError
):
558 """A requirement had a hash specified but was not pinned to a specific
563 "In --require-hashes mode, all requirements must have their "
564 "versions pinned with ==. These do not:"
568 class HashMismatch(HashError
):
570 Distribution file hash values don't match.
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.
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."
586 def __init__(self
, allowed
: Dict
[str, List
[str]], gots
: Dict
[str, "_Hash"]) -> None:
588 :param allowed: A dict of algorithm names pointing to lists of allowed
590 :param gots: A dict of algorithm names pointing to hashes we
591 actually got from the files under suspicion
593 self
.allowed
= allowed
596 def body(self
) -> str:
597 return " {}:\n{}".format(self
._requirement
_name
(), self
._hash
_comparison
())
599 def _hash_comparison(self
) -> str:
601 Return a comparison of actual and expected hash values.
605 Expected sha256 abcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcde
606 or 123451234512345123451234512345123451234512345
607 Got bcdefbcdefbcdefbcdefbcdefbcdefbcdefbcdefbcdef
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"))
616 lines
: List
[str] = []
617 for hash_name
, expecteds
in self
.allowed
.items():
618 prefix
= hash_then_or(hash_name
)
620 (" Expected {} {}".format(next(prefix
), e
)) for e
in expecteds
623 " Got {}\n".format(self
.gots
[hash_name
].hexdigest())
625 return "\n".join(lines
)
628 class UnsupportedPythonVersion(InstallationError
):
629 """Unsupported python version according to Requires-Python package
633 class ConfigurationFileCouldNotBeLoaded(ConfigurationError
):
634 """When there are errors while loading a configuration file"""
638 reason
: str = "could not be loaded",
639 fname
: Optional
[str] = None,
640 error
: Optional
[configparser
.Error
] = None,
642 super().__init
__(error
)
647 def __str__(self
) -> str:
648 if self
.fname
is not None:
649 message_part
= f
" in {self.fname}."
651 assert self
.error
is not None
652 message_part
= f
".\n{self.error}\n"
653 return f
"Configuration file {self.reason}{message_part}"
656 _DEFAULT_EXTERNALLY_MANAGED_ERROR
= f
"""\
657 The Python environment under {sys.prefix} is managed externally, and may not be
658 manipulated by the user. Please use specific tooling from the distributor of
659 the Python installation to interact with this environment instead.
663 class ExternallyManagedEnvironment(DiagnosticPipError
):
664 """The current environment is externally managed.
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.
670 :param error: The error message read from ``EXTERNALLY-MANAGED``.
673 reference
= "externally-managed-environment"
675 def __init__(self
, error
: Optional
[str]) -> None:
677 context
= Text(_DEFAULT_EXTERNALLY_MANAGED_ERROR
)
679 context
= Text(error
)
681 message
="This environment is externally managed",
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."
689 hint_stmt
=Text("See PEP 668 for the detailed specification."),
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.
700 category
= locale
.LC_MESSAGES
701 except AttributeError:
702 lang
: Optional
[str] = None
704 lang
, _
= locale
.getlocale(category
)
706 yield f
"Error-{lang}"
707 for sep
in ("-", "_"):
708 before
, found
, _
= lang
.partition(sep
)
711 yield f
"Error-{before}"
717 config
: Union
[pathlib
.Path
, str],
718 ) -> "ExternallyManagedEnvironment":
719 parser
= configparser
.ConfigParser(interpolation
=None)
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
])
728 except (OSError, UnicodeDecodeError, configparser
.ParsingError
):
729 from pip
._internal
.utils
._log
import VERBOSE
731 exc_info
= logger
.isEnabledFor(VERBOSE
)
732 logger
.warning("Failed to read %s", config
, exc_info
=exc_info
)