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