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
8from __future__ import annotations
9
10import configparser
11import contextlib
12import locale
13import logging
14import os
15import pathlib
16import re
17import sys
18import traceback
19from collections.abc import Iterable, Iterator
20from itertools import chain, groupby, repeat
21from typing import TYPE_CHECKING, Literal
22
23from pip._vendor.packaging.requirements import InvalidRequirement
24from pip._vendor.packaging.version import InvalidVersion
25from pip._vendor.rich.console import Console, ConsoleOptions, RenderResult
26from pip._vendor.rich.markup import escape
27from pip._vendor.rich.text import Text
28
29if TYPE_CHECKING:
30 from hashlib import _Hash
31
32 from pip._vendor import urllib3
33 from pip._vendor.requests.models import PreparedRequest, Request, Response
34
35 from pip._internal.metadata import BaseDistribution
36 from pip._internal.models.link import Link
37 from pip._internal.network.download import _FileDownload
38 from pip._internal.req.req_install import InstallRequirement
39
40logger = logging.getLogger(__name__)
41
42
43#
44# Scaffolding
45#
46def _is_kebab_case(s: str) -> bool:
47 return re.match(r"^[a-z]+(-[a-z]+)*$", s) is not None
48
49
50def _prefix_with_indent(
51 s: Text | str,
52 console: Console,
53 *,
54 prefix: str,
55 indent: str,
56) -> Text:
57 if isinstance(s, Text):
58 text = s
59 else:
60 text = console.render_str(s)
61
62 return console.render_str(prefix, overflow="ignore") + console.render_str(
63 f"\n{indent}", overflow="ignore"
64 ).join(text.split(allow_blank=True))
65
66
67class PipError(Exception):
68 """The base pip error."""
69
70
71class DiagnosticPipError(PipError):
72 """An error, that presents diagnostic information to the user.
73
74 This contains a bunch of logic, to enable pretty presentation of our error
75 messages. Each error gets a unique reference. Each error can also include
76 additional context, a hint and/or a note -- which are presented with the
77 main error message in a consistent style.
78
79 This is adapted from the error output styling in `sphinx-theme-builder`.
80 """
81
82 reference: str
83
84 def __init__(
85 self,
86 *,
87 kind: Literal["error", "warning"] = "error",
88 reference: str | None = None,
89 message: str | Text,
90 context: str | Text | None,
91 hint_stmt: str | Text | None,
92 note_stmt: str | Text | None = None,
93 link: str | None = None,
94 ) -> None:
95 # Ensure a proper reference is provided.
96 if reference is None:
97 assert hasattr(self, "reference"), "error reference not provided!"
98 reference = self.reference
99 assert _is_kebab_case(reference), "error reference must be kebab-case!"
100
101 self.kind = kind
102 self.reference = reference
103
104 self.message = message
105 self.context = context
106
107 self.note_stmt = note_stmt
108 self.hint_stmt = hint_stmt
109
110 self.link = link
111
112 super().__init__(f"<{self.__class__.__name__}: {self.reference}>")
113
114 def __repr__(self) -> str:
115 return (
116 f"<{self.__class__.__name__}("
117 f"reference={self.reference!r}, "
118 f"message={self.message!r}, "
119 f"context={self.context!r}, "
120 f"note_stmt={self.note_stmt!r}, "
121 f"hint_stmt={self.hint_stmt!r}"
122 ")>"
123 )
124
125 def __rich_console__(
126 self,
127 console: Console,
128 options: ConsoleOptions,
129 ) -> RenderResult:
130 colour = "red" if self.kind == "error" else "yellow"
131
132 yield f"[{colour} bold]{self.kind}[/]: [bold]{self.reference}[/]"
133 yield ""
134
135 if not options.ascii_only:
136 # Present the main message, with relevant context indented.
137 if self.context is not None:
138 yield _prefix_with_indent(
139 self.message,
140 console,
141 prefix=f"[{colour}]×[/] ",
142 indent=f"[{colour}]│[/] ",
143 )
144 yield _prefix_with_indent(
145 self.context,
146 console,
147 prefix=f"[{colour}]╰─>[/] ",
148 indent=f"[{colour}] [/] ",
149 )
150 else:
151 yield _prefix_with_indent(
152 self.message,
153 console,
154 prefix="[red]×[/] ",
155 indent=" ",
156 )
157 else:
158 yield self.message
159 if self.context is not None:
160 yield ""
161 yield self.context
162
163 if self.note_stmt is not None or self.hint_stmt is not None:
164 yield ""
165
166 if self.note_stmt is not None:
167 yield _prefix_with_indent(
168 self.note_stmt,
169 console,
170 prefix="[magenta bold]note[/]: ",
171 indent=" ",
172 )
173 if self.hint_stmt is not None:
174 yield _prefix_with_indent(
175 self.hint_stmt,
176 console,
177 prefix="[cyan bold]hint[/]: ",
178 indent=" ",
179 )
180
181 if self.link is not None:
182 yield ""
183 yield f"Link: {self.link}"
184
185
186#
187# Actual Errors
188#
189class ConfigurationError(PipError):
190 """General exception in configuration"""
191
192
193class InstallationError(PipError):
194 """General exception during installation"""
195
196
197class FailedToPrepareCandidate(InstallationError):
198 """Raised when we fail to prepare a candidate (i.e. fetch and generate metadata).
199
200 This is intentionally not a diagnostic error, since the output will be presented
201 above this error, when this occurs. This should instead present information to the
202 user.
203 """
204
205 def __init__(
206 self, *, package_name: str, requirement_chain: str, failed_step: str
207 ) -> None:
208 super().__init__(f"Failed to build '{package_name}' when {failed_step.lower()}")
209 self.package_name = package_name
210 self.requirement_chain = requirement_chain
211 self.failed_step = failed_step
212
213
214class MissingPyProjectBuildRequires(DiagnosticPipError):
215 """Raised when pyproject.toml has `build-system`, but no `build-system.requires`."""
216
217 reference = "missing-pyproject-build-system-requires"
218
219 def __init__(self, *, package: str) -> None:
220 super().__init__(
221 message=f"Can not process {escape(package)}",
222 context=Text(
223 "This package has an invalid pyproject.toml file.\n"
224 "The [build-system] table is missing the mandatory `requires` key."
225 ),
226 note_stmt="This is an issue with the package mentioned above, not pip.",
227 hint_stmt=Text("See PEP 518 for the detailed specification."),
228 )
229
230
231class InvalidPyProjectBuildRequires(DiagnosticPipError):
232 """Raised when pyproject.toml an invalid `build-system.requires`."""
233
234 reference = "invalid-pyproject-build-system-requires"
235
236 def __init__(self, *, package: str, reason: str) -> None:
237 super().__init__(
238 message=f"Can not process {escape(package)}",
239 context=Text(
240 "This package has an invalid `build-system.requires` key in "
241 f"pyproject.toml.\n{reason}"
242 ),
243 note_stmt="This is an issue with the package mentioned above, not pip.",
244 hint_stmt=Text("See PEP 518 for the detailed specification."),
245 )
246
247
248class NoneMetadataError(PipError):
249 """Raised when accessing a Distribution's "METADATA" or "PKG-INFO".
250
251 This signifies an inconsistency, when the Distribution claims to have
252 the metadata file (if not, raise ``FileNotFoundError`` instead), but is
253 not actually able to produce its content. This may be due to permission
254 errors.
255 """
256
257 def __init__(
258 self,
259 dist: BaseDistribution,
260 metadata_name: str,
261 ) -> None:
262 """
263 :param dist: A Distribution object.
264 :param metadata_name: The name of the metadata being accessed
265 (can be "METADATA" or "PKG-INFO").
266 """
267 self.dist = dist
268 self.metadata_name = metadata_name
269
270 def __str__(self) -> str:
271 # Use `dist` in the error message because its stringification
272 # includes more information, like the version and location.
273 return f"None {self.metadata_name} metadata found for distribution: {self.dist}"
274
275
276class UserInstallationInvalid(InstallationError):
277 """A --user install is requested on an environment without user site."""
278
279 def __str__(self) -> str:
280 return "User base directory is not specified"
281
282
283class InvalidSchemeCombination(InstallationError):
284 def __str__(self) -> str:
285 before = ", ".join(str(a) for a in self.args[:-1])
286 return f"Cannot set {before} and {self.args[-1]} together"
287
288
289class DistributionNotFound(InstallationError):
290 """Raised when a distribution cannot be found to satisfy a requirement"""
291
292
293class RequirementsFileParseError(InstallationError):
294 """Raised when a general error occurs parsing a requirements file line."""
295
296
297class BestVersionAlreadyInstalled(PipError):
298 """Raised when the most up-to-date version of a package is already
299 installed."""
300
301
302class BadCommand(PipError):
303 """Raised when virtualenv or a command is not found"""
304
305
306class CommandError(PipError):
307 """Raised when there is an error in command-line arguments"""
308
309
310class PreviousBuildDirError(PipError):
311 """Raised when there's a previous conflicting build directory"""
312
313
314class NetworkConnectionError(PipError):
315 """HTTP connection error"""
316
317 def __init__(
318 self,
319 error_msg: str,
320 response: Response | None = None,
321 request: Request | PreparedRequest | None = None,
322 ) -> None:
323 """
324 Initialize NetworkConnectionError with `request` and `response`
325 objects.
326 """
327 self.response = response
328 self.request = request
329 self.error_msg = error_msg
330 if (
331 self.response is not None
332 and not self.request
333 and hasattr(response, "request")
334 ):
335 self.request = self.response.request
336 super().__init__(error_msg, response, request)
337
338 def __str__(self) -> str:
339 return str(self.error_msg)
340
341
342class ConnectionFailedError(DiagnosticPipError):
343 reference = "connection-failed"
344
345 def __init__(self, url: str, host: str, error: Exception) -> None:
346 from http.client import RemoteDisconnected
347
348 from pip._vendor.urllib3.exceptions import (
349 NameResolutionError,
350 NewConnectionError,
351 ProtocolError,
352 )
353
354 details = str(error)
355 if isinstance(error, NameResolutionError):
356 parts = details.split("Failed to resolve ", maxsplit=1)
357 if len(parts) == 2:
358 details = "Failed to resolve IP address for " + parts[1]
359 elif isinstance(error, NewConnectionError):
360 parts = details.split("Failed to establish a new connection: ", maxsplit=1)
361 if len(parts) == 2:
362 _, details = parts
363 elif isinstance(error, ProtocolError):
364 try:
365 reason = error.args[1]
366 except IndexError:
367 pass
368 else:
369 if isinstance(reason, (RemoteDisconnected, ConnectionResetError)):
370 details = (
371 "the connection was closed without a reply from the server."
372 )
373
374 super().__init__(
375 message=(
376 f"Failed to connect to [magenta]{escape(host)}[/] while fetching "
377 f"{escape(url)}"
378 ),
379 context=Text(details),
380 hint_stmt=(
381 "Are you connected to the Internet? If so, check whether your system "
382 f"can connect to [magenta]{escape(host)}[/] before trying again. "
383 "There may be a firewall or proxy that's preventing the connection."
384 ),
385 )
386
387
388class ConnectionTimeoutError(DiagnosticPipError):
389 reference = "connection-timeout"
390
391 def __init__(
392 self,
393 url: str,
394 host: str,
395 *,
396 kind: Literal["connect", "read"],
397 timeout: float,
398 ) -> None:
399 context = Text.assemble(
400 (host, "magenta"), f" didn't respond within {timeout} seconds"
401 )
402 if kind == "connect":
403 context.append(" (while establishing a connection)")
404 super().__init__(
405 message=f"Unable to fetch {escape(url)}",
406 context=context,
407 hint_stmt=(
408 "This is probably a temporary issue with the remote server or the "
409 "network connection. If this error persists, check the network "
410 "configuration. There may be a firewall or proxy that's preventing "
411 "the connection."
412 ),
413 )
414
415
416class SSLMissingError(DiagnosticPipError):
417 reference = "ssl-missing"
418
419 def __init__(self, url: str) -> None:
420 super().__init__(
421 message=f"Failed to establish a secure connection for {escape(url)}",
422 context="The 'ssl' module is unavailable but required for HTTPS URLs",
423 hint_stmt=None,
424 )
425
426
427class SSLVerificationError(DiagnosticPipError):
428 reference = "ssl-verification-failed"
429
430 def __init__(self, url: str, host: str, error: urllib3.exceptions.SSLError) -> None:
431 message = (
432 "Failed to establish a secure connection to "
433 f"[magenta]{escape(host)}[/] while fetching {escape(url)}"
434 )
435 hint = "You may need to use --cert or check your proxy/firewall configuration"
436 super().__init__(message=message, context=Text(str(error)), hint_stmt=hint)
437
438
439class ProxyConnectionError(DiagnosticPipError):
440 reference = "proxy-connection-failed"
441
442 def __init__(
443 self, url: str, proxy: str, error: urllib3.exceptions.ProxyError
444 ) -> None:
445 super().__init__(
446 message=(
447 "Failed to connect to proxy "
448 f"[magenta]{escape(proxy)}[/] while fetching {escape(url)}"
449 ),
450 context=Text(str(error)),
451 hint_stmt="This is likely a proxy configuration issue.",
452 )
453
454
455class InvalidWheelFilename(InstallationError):
456 """Invalid wheel filename."""
457
458
459class UnsupportedWheel(InstallationError):
460 """Unsupported wheel."""
461
462
463class InvalidWheel(InstallationError):
464 """Invalid (e.g. corrupt) wheel."""
465
466 def __init__(self, location: str, name: str):
467 self.location = location
468 self.name = name
469
470 def __str__(self) -> str:
471 return f"Wheel '{self.name}' located at {self.location} is invalid."
472
473
474class MetadataInconsistent(InstallationError):
475 """Built metadata contains inconsistent information.
476
477 This is raised when the metadata contains values (e.g. name and version)
478 that do not match the information previously obtained from sdist filename,
479 user-supplied ``#egg=`` value, or an install requirement name.
480 """
481
482 def __init__(
483 self, ireq: InstallRequirement, field: str, f_val: str, m_val: str
484 ) -> None:
485 self.ireq = ireq
486 self.field = field
487 self.f_val = f_val
488 self.m_val = m_val
489
490 def __str__(self) -> str:
491 return (
492 f"Requested {self.ireq} has inconsistent {self.field}: "
493 f"expected {self.f_val!r}, but metadata has {self.m_val!r}"
494 )
495
496
497class SidecarMetadataInconsistent(MetadataInconsistent):
498 """The wheel's METADATA disagrees with its PEP 658 ``.metadata`` file.
499
500 Raised after the wheel has been downloaded and hash-verified, when a
501 resolver-affecting field in the wheel's embedded ``METADATA`` does not
502 match the value taken from the remote ``.metadata`` sidecar that drove
503 resolution. ``f_val`` is the sidecar value, ``m_val`` is the wheel value.
504 """
505
506 def __str__(self) -> str:
507 return (
508 f"Requested {self.ireq} has inconsistent {self.field} between "
509 f"its PEP 658 .metadata file and the wheel's METADATA: "
510 f"sidecar has {self.f_val!r}, wheel has {self.m_val!r}"
511 )
512
513
514class MetadataInvalid(InstallationError):
515 """Metadata is invalid."""
516
517 def __init__(self, ireq: InstallRequirement, error: str) -> None:
518 self.ireq = ireq
519 self.error = error
520
521 def __str__(self) -> str:
522 return f"Requested {self.ireq} has invalid metadata: {self.error}"
523
524
525class InstallationSubprocessError(DiagnosticPipError, InstallationError):
526 """A subprocess call failed."""
527
528 reference = "subprocess-exited-with-error"
529
530 def __init__(
531 self,
532 *,
533 command_description: str,
534 exit_code: int,
535 output_lines: list[str] | None,
536 ) -> None:
537 if output_lines is None:
538 output_prompt = Text("No available output.")
539 else:
540 output_prompt = (
541 Text.from_markup(f"[red][{len(output_lines)} lines of output][/]\n")
542 + Text("".join(output_lines))
543 + Text.from_markup(R"[red]\[end of output][/]")
544 )
545
546 super().__init__(
547 message=(
548 f"[green]{escape(command_description)}[/] did not run successfully.\n"
549 f"exit code: {exit_code}"
550 ),
551 context=output_prompt,
552 hint_stmt=None,
553 note_stmt=(
554 "This error originates from a subprocess, and is likely not a "
555 "problem with pip."
556 ),
557 )
558
559 self.command_description = command_description
560 self.exit_code = exit_code
561
562 def __str__(self) -> str:
563 return f"{self.command_description} exited with {self.exit_code}"
564
565
566class MetadataGenerationFailed(DiagnosticPipError, InstallationError):
567 reference = "metadata-generation-failed"
568
569 def __init__(
570 self,
571 *,
572 package_details: str,
573 ) -> None:
574 super().__init__(
575 message="Encountered error while generating package metadata.",
576 context=escape(package_details),
577 hint_stmt="See above for details.",
578 note_stmt="This is an issue with the package mentioned above, not pip.",
579 )
580
581 def __str__(self) -> str:
582 return "metadata generation failed"
583
584
585class HashErrors(InstallationError):
586 """Multiple HashError instances rolled into one for reporting"""
587
588 def __init__(self) -> None:
589 self.errors: list[HashError] = []
590
591 def append(self, error: HashError) -> None:
592 self.errors.append(error)
593
594 def __str__(self) -> str:
595 lines = []
596 self.errors.sort(key=lambda e: e.order)
597 for cls, errors_of_cls in groupby(self.errors, lambda e: e.__class__):
598 lines.append(cls.head)
599 lines.extend(e.body() for e in errors_of_cls)
600 if lines:
601 return "\n".join(lines)
602 return ""
603
604 def __bool__(self) -> bool:
605 return bool(self.errors)
606
607
608class HashError(InstallationError):
609 """
610 A failure to verify a package against known-good hashes
611
612 :cvar order: An int sorting hash exception classes by difficulty of
613 recovery (lower being harder), so the user doesn't bother fretting
614 about unpinned packages when he has deeper issues, like VCS
615 dependencies, to deal with. Also keeps error reports in a
616 deterministic order.
617 :cvar head: A section heading for display above potentially many
618 exceptions of this kind
619 :ivar req: The InstallRequirement that triggered this error. This is
620 pasted on after the exception is instantiated, because it's not
621 typically available earlier.
622
623 """
624
625 req: InstallRequirement | None = None
626 head = ""
627 order: int = -1
628
629 def body(self) -> str:
630 """Return a summary of me for display under the heading.
631
632 This default implementation simply prints a description of the
633 triggering requirement.
634
635 :param req: The InstallRequirement that provoked this error, with
636 its link already populated by the resolver's _populate_link().
637
638 """
639 return f" {self._requirement_name()}"
640
641 def __str__(self) -> str:
642 return f"{self.head}\n{self.body()}"
643
644 def _requirement_name(self) -> str:
645 """Return a description of the requirement that triggered me.
646
647 This default implementation returns long description of the req, with
648 line numbers
649
650 """
651 return str(self.req) if self.req else "unknown package"
652
653
654class VcsHashUnsupported(HashError):
655 """A hash was provided for a version-control-system-based requirement, but
656 we don't have a method for hashing those."""
657
658 order = 0
659 head = (
660 "Can't verify hashes for these requirements because we don't "
661 "have a way to hash version control repositories:"
662 )
663
664
665class DirectoryUrlHashUnsupported(HashError):
666 """A hash was provided for a version-control-system-based requirement, but
667 we don't have a method for hashing those."""
668
669 order = 1
670 head = (
671 "Can't verify hashes for these file:// requirements because they "
672 "point to directories:"
673 )
674
675
676class HashMissing(HashError):
677 """A hash was needed for a requirement but is absent."""
678
679 order = 2
680 head = (
681 "Hashes are required in --require-hashes mode, but they are "
682 "missing from some requirements. Here is a list of those "
683 "requirements along with the hashes their downloaded archives "
684 "actually had. Add lines like these to your requirements files to "
685 "prevent tampering. (If you did not enable --require-hashes "
686 "manually, note that it turns on automatically when any package "
687 "has a hash.)"
688 )
689
690 def __init__(self, gotten_hash: str) -> None:
691 """
692 :param gotten_hash: The hash of the (possibly malicious) archive we
693 just downloaded
694 """
695 self.gotten_hash = gotten_hash
696
697 def body(self) -> str:
698 # Dodge circular import.
699 from pip._internal.utils.hashes import FAVORITE_HASH
700
701 package = None
702 if self.req:
703 # In the case of URL-based requirements, display the original URL
704 # seen in the requirements file rather than the package name,
705 # so the output can be directly copied into the requirements file.
706 package = (
707 self.req.original_link
708 if self.req.is_direct
709 # In case someone feeds something downright stupid
710 # to InstallRequirement's constructor.
711 else getattr(self.req, "req", None)
712 )
713 return " {} --hash={}:{}".format(
714 package or "unknown package", FAVORITE_HASH, self.gotten_hash
715 )
716
717
718class HashUnpinned(HashError):
719 """A requirement had a hash specified but was not pinned to a specific
720 version."""
721
722 order = 3
723 head = (
724 "In --require-hashes mode, all requirements must have their "
725 "versions pinned with ==. These do not:"
726 )
727
728
729class HashMismatch(HashError):
730 """
731 Distribution file hash values don't match.
732
733 :ivar package_name: The name of the package that triggered the hash
734 mismatch. Feel free to write to this after the exception is raise to
735 improve its error message.
736
737 """
738
739 order = 4
740 head = (
741 "THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS "
742 "FILE. If you have updated the package versions, please update "
743 "the hashes. Otherwise, examine the package contents carefully; "
744 "someone may have tampered with them."
745 )
746
747 def __init__(self, allowed: dict[str, list[str]], gots: dict[str, _Hash]) -> None:
748 """
749 :param allowed: A dict of algorithm names pointing to lists of allowed
750 hex digests
751 :param gots: A dict of algorithm names pointing to hashes we
752 actually got from the files under suspicion
753 """
754 self.allowed = allowed
755 self.gots = gots
756
757 def body(self) -> str:
758 return f" {self._requirement_name()}:\n{self._hash_comparison()}"
759
760 def _hash_comparison(self) -> str:
761 """
762 Return a comparison of actual and expected hash values.
763
764 Example::
765
766 Expected sha256 abcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcde
767 or 123451234512345123451234512345123451234512345
768 Got bcdefbcdefbcdefbcdefbcdefbcdefbcdefbcdefbcdef
769
770 """
771
772 def hash_then_or(hash_name: str) -> chain[str]:
773 # For now, all the decent hashes have 6-char names, so we can get
774 # away with hard-coding space literals.
775 return chain([hash_name], repeat(" or"))
776
777 lines: list[str] = []
778 for hash_name, expecteds in self.allowed.items():
779 prefix = hash_then_or(hash_name)
780 lines.extend((f" Expected {next(prefix)} {e}") for e in expecteds)
781 lines.append(
782 f" Got {self.gots[hash_name].hexdigest()}\n"
783 )
784 return "\n".join(lines)
785
786
787class UnsupportedPythonVersion(InstallationError):
788 """Unsupported python version according to Requires-Python package
789 metadata."""
790
791
792class ConfigurationFileCouldNotBeLoaded(ConfigurationError):
793 """When there are errors while loading a configuration file"""
794
795 def __init__(
796 self,
797 reason: str = "could not be loaded",
798 fname: str | None = None,
799 error: configparser.Error | None = None,
800 ) -> None:
801 super().__init__(error)
802 self.reason = reason
803 self.fname = fname
804 self.error = error
805
806 def __str__(self) -> str:
807 if self.fname is not None:
808 message_part = f" in {self.fname}."
809 else:
810 assert self.error is not None
811 message_part = f".\n{self.error}\n"
812 return f"Configuration file {self.reason}{message_part}"
813
814
815_DEFAULT_EXTERNALLY_MANAGED_ERROR = f"""\
816The Python environment under {sys.prefix} is managed externally, and may not be
817manipulated by the user. Please use specific tooling from the distributor of
818the Python installation to interact with this environment instead.
819"""
820
821
822class ExternallyManagedEnvironment(DiagnosticPipError):
823 """The current environment is externally managed.
824
825 This is raised when the current environment is externally managed, as
826 defined by `PEP 668`_. The ``EXTERNALLY-MANAGED`` configuration is checked
827 and displayed when the error is bubbled up to the user.
828
829 :param error: The error message read from ``EXTERNALLY-MANAGED``.
830 """
831
832 reference = "externally-managed-environment"
833
834 def __init__(self, error: str | None) -> None:
835 if error is None:
836 context = Text(_DEFAULT_EXTERNALLY_MANAGED_ERROR)
837 else:
838 context = Text(error)
839 super().__init__(
840 message="This environment is externally managed",
841 context=context,
842 note_stmt=(
843 "If you believe this is a mistake, please contact your "
844 "Python installation or OS distribution provider. "
845 "You can override this, at the risk of breaking your Python "
846 "installation or OS, by passing --break-system-packages."
847 ),
848 hint_stmt=Text("See PEP 668 for the detailed specification."),
849 )
850
851 @staticmethod
852 def _iter_externally_managed_error_keys() -> Iterator[str]:
853 # LC_MESSAGES is in POSIX, but not the C standard. The most common
854 # platform that does not implement this category is Windows, where
855 # using other categories for console message localization is equally
856 # unreliable, so we fall back to the locale-less vendor message. This
857 # can always be re-evaluated when a vendor proposes a new alternative.
858 try:
859 category = locale.LC_MESSAGES
860 except AttributeError:
861 lang: str | None = None
862 else:
863 lang, _ = locale.getlocale(category)
864 if lang is not None:
865 yield f"Error-{lang}"
866 for sep in ("-", "_"):
867 before, found, _ = lang.partition(sep)
868 if not found:
869 continue
870 yield f"Error-{before}"
871 yield "Error"
872
873 @classmethod
874 def from_config(
875 cls,
876 config: pathlib.Path | str,
877 ) -> ExternallyManagedEnvironment:
878 parser = configparser.ConfigParser(interpolation=None)
879 try:
880 parser.read(config, encoding="utf-8")
881 section = parser["externally-managed"]
882 for key in cls._iter_externally_managed_error_keys():
883 with contextlib.suppress(KeyError):
884 return cls(section[key])
885 except KeyError:
886 pass
887 except (OSError, UnicodeDecodeError, configparser.ParsingError):
888 from pip._internal.utils._log import VERBOSE
889
890 exc_info = logger.isEnabledFor(VERBOSE)
891 logger.warning("Failed to read %s", config, exc_info=exc_info)
892 return cls(None)
893
894
895class UninstallMissingRecord(DiagnosticPipError):
896 reference = "uninstall-no-record-file"
897
898 def __init__(self, *, distribution: BaseDistribution) -> None:
899 installer = distribution.installer
900 if not installer or installer == "pip":
901 dep = f"{distribution.raw_name}=={distribution.version}"
902 hint = Text.assemble(
903 "You might be able to recover from this via: ",
904 (f"pip install --ignore-installed --no-deps {dep}", "green"),
905 )
906 else:
907 hint = Text(
908 f"The package was installed by {installer}. "
909 "You should check if it can uninstall the package."
910 )
911
912 super().__init__(
913 message=Text(f"Cannot uninstall {distribution}"),
914 context=(
915 "The package's contents are unknown: "
916 f"no RECORD file was found for {distribution.raw_name}."
917 ),
918 hint_stmt=hint,
919 )
920
921
922class LegacyDistutilsInstall(DiagnosticPipError):
923 reference = "uninstall-distutils-installed-package"
924
925 def __init__(self, *, distribution: BaseDistribution) -> None:
926 super().__init__(
927 message=Text(f"Cannot uninstall {distribution}"),
928 context=(
929 "It is a distutils installed project and thus we cannot accurately "
930 "determine which files belong to it which would lead to only a partial "
931 "uninstall."
932 ),
933 hint_stmt=None,
934 )
935
936
937class InvalidInstalledPackage(DiagnosticPipError):
938 reference = "invalid-installed-package"
939
940 def __init__(
941 self,
942 *,
943 dist: BaseDistribution,
944 invalid_exc: InvalidRequirement | InvalidVersion,
945 ) -> None:
946 installed_location = dist.installed_location
947
948 if isinstance(invalid_exc, InvalidRequirement):
949 invalid_type = "requirement"
950 else:
951 invalid_type = "version"
952
953 super().__init__(
954 message=Text(
955 f"Cannot process installed package {dist} "
956 + (f"in {installed_location!r} " if installed_location else "")
957 + f"because it has an invalid {invalid_type}:\n{invalid_exc.args[0]}"
958 ),
959 context=(
960 "Starting with pip 24.1, packages with invalid "
961 f"{invalid_type}s can not be processed."
962 ),
963 hint_stmt="To proceed this package must be uninstalled.",
964 )
965
966
967class IncompleteDownloadError(DiagnosticPipError):
968 """Raised when the downloader receives fewer bytes than advertised
969 in the Content-Length header."""
970
971 reference = "incomplete-download"
972
973 def __init__(self, download: _FileDownload) -> None:
974 # Dodge circular import.
975 from pip._internal.utils.misc import format_size
976
977 assert download.size is not None
978 download_status = (
979 f"{format_size(download.bytes_received)}/{format_size(download.size)}"
980 )
981 if download.reattempts:
982 retry_status = f"after {download.reattempts + 1} attempts "
983 hint = "Use --resume-retries to configure resume attempt limit."
984 else:
985 # Download retrying is not enabled.
986 retry_status = ""
987 hint = "Consider using --resume-retries to enable download resumption."
988 message = Text(
989 f"Download failed {retry_status}because not enough bytes "
990 f"were received ({download_status})"
991 )
992
993 super().__init__(
994 message=message,
995 context=f"URL: {download.link.redacted_url}",
996 hint_stmt=hint,
997 note_stmt="This is an issue with network connectivity, not pip.",
998 )
999
1000
1001class ResolutionTooDeepError(DiagnosticPipError):
1002 """Raised when the dependency resolver exceeds the maximum recursion depth."""
1003
1004 reference = "resolution-too-deep"
1005
1006 def __init__(self) -> None:
1007 super().__init__(
1008 message="Dependency resolution exceeded maximum depth",
1009 context=(
1010 "Pip cannot resolve the current dependencies as the dependency graph "
1011 "is too complex for pip to solve efficiently."
1012 ),
1013 hint_stmt=(
1014 "Try adding lower bounds to constrain your dependencies, "
1015 "for example: 'package>=2.0.0' instead of just 'package'. "
1016 ),
1017 link="https://pip.pypa.io/en/stable/topics/dependency-resolution/#handling-resolution-too-deep-errors",
1018 )
1019
1020
1021class InstallWheelBuildError(DiagnosticPipError):
1022 reference = "failed-wheel-build-for-install"
1023
1024 def __init__(self, failed: list[InstallRequirement]) -> None:
1025 super().__init__(
1026 message=(
1027 "Failed to build installable wheels for some "
1028 "pyproject.toml based projects"
1029 ),
1030 context=", ".join(r.name for r in failed), # type: ignore
1031 hint_stmt=None,
1032 )
1033
1034
1035class InvalidEggFragment(DiagnosticPipError):
1036 reference = "invalid-egg-fragment"
1037
1038 def __init__(self, link: Link, fragment: str) -> None:
1039 hint = ""
1040 if ">" in fragment or "=" in fragment or "<" in fragment:
1041 hint = (
1042 "Version specifiers are silently ignored for URL references. "
1043 "Remove them. "
1044 )
1045 if "[" in fragment and "]" in fragment:
1046 hint += "Try using the Direct URL requirement syntax: 'name[extra] @ URL'"
1047
1048 if not hint:
1049 hint = "Egg fragments can only be a valid project name."
1050
1051 super().__init__(
1052 message=f"The '{escape(fragment)}' egg fragment is invalid",
1053 context=f"from '{escape(str(link))}'",
1054 hint_stmt=escape(hint),
1055 )
1056
1057
1058class BuildDependencyInstallError(DiagnosticPipError):
1059 """Raised when build dependencies cannot be installed."""
1060
1061 reference = "failed-build-dependency-install"
1062
1063 def __init__(
1064 self,
1065 req: InstallRequirement | None,
1066 build_reqs: Iterable[str],
1067 *,
1068 cause: Exception,
1069 log_lines: list[str] | None,
1070 ) -> None:
1071 if isinstance(cause, PipError):
1072 note = "This is likely not a problem with pip."
1073 else:
1074 note = (
1075 "pip crashed unexpectedly. Please file an issue on pip's issue "
1076 "tracker: https://github.com/pypa/pip/issues/new"
1077 )
1078
1079 if log_lines is None:
1080 # No logs are available, they must have been printed earlier.
1081 context = Text("See above for more details.")
1082 else:
1083 if isinstance(cause, PipError):
1084 log_lines.append(f"ERROR: {cause}")
1085 else:
1086 # Split rendered error into real lines without trailing newlines.
1087 log_lines.extend(
1088 "".join(traceback.format_exception(cause)).splitlines()
1089 )
1090
1091 context = Text.assemble(
1092 f"Installing {' '.join(build_reqs)}\n",
1093 (f"[{len(log_lines)} lines of output]\n", "red"),
1094 "\n".join(log_lines),
1095 ("\n[end of output]", "red"),
1096 )
1097
1098 message = Text("Cannot install build dependencies", "green")
1099 if req:
1100 message += Text(f" for {req}")
1101 super().__init__(
1102 message=message, context=context, hint_stmt=None, note_stmt=note
1103 )
1104
1105
1106class VenvImportError(DiagnosticPipError):
1107 """Raised when 'venv' can't be imported."""
1108
1109 reference = "venv-import-error"
1110
1111 def __init__(self) -> None:
1112 if sys.platform != "linux":
1113 hint_stmt = None
1114 else:
1115 hint_stmt = (
1116 "If this is an OS-provided Python, it's likely that your OS "
1117 "package maintainers have split Python's standard library across "
1118 "multiple OS packages."
1119 )
1120 super().__init__(
1121 message="Cannot import the 'venv' module of the Python standard library",
1122 context=(
1123 "This is a symptom of a broken/modified Python, which cannot be used "
1124 "with pip."
1125 ),
1126 note_stmt="This is an issue with the Python installation itself, not pip.",
1127 hint_stmt=hint_stmt,
1128 )
1129
1130
1131class VenvCreationError(DiagnosticPipError):
1132 """Raised when a virtual environment can't be created."""
1133
1134 reference = "venv-creation-error"
1135
1136 def __init__(self, context: str) -> None:
1137 if os.name == "nt":
1138 hint = "This may be caused by running antivirus software."
1139 else:
1140 hint = None
1141 super().__init__(
1142 message="Cannot create a virtual environment",
1143 context=Text(context),
1144 hint_stmt=hint,
1145 )