1"""Package of exceptions used in the pip codebase.
2
3This package used to be a single module, so many exceptions were
4grandfathered into this module. In the future, more groups of exceptions
5should be moved to their own submodules as needed.
6
7This package MUST NOT try to import from anything within `pip._internal` to
8operate. This is expected to be importable from any/all files within the
9subpackage and, thus, should not depend on them.
10"""
11
12# NOTE: the pip._internal import restriction may change later if there is an
13# exception group that does really need internal code to function (e.g.
14# possibly network). In that case, said group should NOT be re-exported here.
15
16from __future__ import annotations
17
18import configparser
19import contextlib
20import locale
21import logging
22import pathlib
23import sys
24from collections.abc import Iterator
25from typing import TYPE_CHECKING
26
27from pip._vendor.packaging.requirements import InvalidRequirement
28from pip._vendor.packaging.version import InvalidVersion
29from pip._vendor.rich.markup import escape
30from pip._vendor.rich.text import Text
31
32from pip._internal.exceptions._base import (
33 DiagnosticPipError,
34 InstallationError,
35 PipError,
36)
37from pip._internal.exceptions.build import (
38 BackendUnavailableError,
39 BuildDependencyInstallError,
40 InstallationSubprocessError,
41 VenvCreationError,
42 VenvImportError,
43)
44from pip._internal.exceptions.hashes import (
45 DirectoryUrlHashUnsupported,
46 HashError,
47 HashErrors,
48 HashMismatch,
49 HashMissing,
50 HashUnpinned,
51 VcsHashUnsupported,
52)
53from pip._internal.exceptions.network import (
54 ConnectionFailedError,
55 ConnectionTimeoutError,
56 IncompleteDownloadError,
57 NetworkConnectionError,
58 ProxyConnectionError,
59 SSLMissingError,
60 SSLVerificationError,
61)
62from pip._internal.exceptions.pyproject import (
63 InvalidPyProjectBuildRequires,
64 MissingPyProjectBuildRequires,
65)
66from pip._internal.exceptions.uninstall import (
67 LegacyDistutilsInstall,
68 UninstallMissingRecord,
69)
70from pip._internal.exceptions.wheel import (
71 InvalidWheel,
72 InvalidWheelFilename,
73 UnsupportedWheel,
74)
75
76if TYPE_CHECKING:
77 from pip._internal.metadata import BaseDistribution
78 from pip._internal.models.link import Link
79 from pip._internal.req.req_install import InstallRequirement
80
81
82__all__ = [
83 # base
84 "DiagnosticPipError",
85 "InstallationError",
86 "PipError",
87 # build
88 "BackendUnavailableError",
89 "BuildDependencyInstallError",
90 "InstallationSubprocessError",
91 "VenvCreationError",
92 "VenvImportError",
93 # hashes
94 "DirectoryUrlHashUnsupported",
95 "HashError",
96 "HashErrors",
97 "HashMismatch",
98 "HashMissing",
99 "HashUnpinned",
100 "VcsHashUnsupported",
101 # network
102 "ConnectionFailedError",
103 "ConnectionTimeoutError",
104 "IncompleteDownloadError",
105 "NetworkConnectionError",
106 "ProxyConnectionError",
107 "SSLMissingError",
108 "SSLVerificationError",
109 # pyproject
110 "InvalidPyProjectBuildRequires",
111 "MissingPyProjectBuildRequires",
112 # uninstall
113 "LegacyDistutilsInstall",
114 "UninstallMissingRecord",
115 # wheel
116 "InvalidWheel",
117 "InvalidWheelFilename",
118 "UnsupportedWheel",
119]
120
121logger = logging.getLogger(__name__)
122
123
124class ConfigurationError(PipError):
125 """General exception in configuration"""
126
127
128class FailedToPrepareCandidate(InstallationError):
129 """Raised when we fail to prepare a candidate (i.e. fetch and generate metadata).
130
131 This is intentionally not a diagnostic error, since the output will be presented
132 above this error, when this occurs. This should instead present information to the
133 user.
134 """
135
136 def __init__(
137 self, *, package_name: str, requirement_chain: str, failed_step: str
138 ) -> None:
139 super().__init__(f"Failed to build '{package_name}' when {failed_step.lower()}")
140 self.package_name = package_name
141 self.requirement_chain = requirement_chain
142 self.failed_step = failed_step
143
144
145class NoneMetadataError(PipError):
146 """Raised when accessing a Distribution's "METADATA" or "PKG-INFO".
147
148 This signifies an inconsistency, when the Distribution claims to have
149 the metadata file (if not, raise ``FileNotFoundError`` instead), but is
150 not actually able to produce its content. This may be due to permission
151 errors.
152 """
153
154 def __init__(
155 self,
156 dist: BaseDistribution,
157 metadata_name: str,
158 ) -> None:
159 """
160 :param dist: A Distribution object.
161 :param metadata_name: The name of the metadata being accessed
162 (can be "METADATA" or "PKG-INFO").
163 """
164 self.dist = dist
165 self.metadata_name = metadata_name
166
167 def __str__(self) -> str:
168 # Use `dist` in the error message because its stringification
169 # includes more information, like the version and location.
170 return f"None {self.metadata_name} metadata found for distribution: {self.dist}"
171
172
173class UserInstallationInvalid(InstallationError):
174 """A --user install is requested on an environment without user site."""
175
176 def __str__(self) -> str:
177 return "User base directory is not specified"
178
179
180class InvalidSchemeCombination(InstallationError):
181 def __str__(self) -> str:
182 before = ", ".join(str(a) for a in self.args[:-1])
183 return f"Cannot set {before} and {self.args[-1]} together"
184
185
186class DistributionNotFound(InstallationError):
187 """Raised when a distribution cannot be found to satisfy a requirement"""
188
189
190class RequirementsFileParseError(InstallationError):
191 """Raised when a general error occurs parsing a requirements file line."""
192
193
194class BestVersionAlreadyInstalled(PipError):
195 """Raised when the most up-to-date version of a package is already
196 installed."""
197
198
199class BadCommand(PipError):
200 """Raised when virtualenv or a command is not found"""
201
202
203class CommandError(PipError):
204 """Raised when there is an error in command-line arguments"""
205
206
207class PreviousBuildDirError(PipError):
208 """Raised when there's a previous conflicting build directory"""
209
210
211class MetadataInconsistent(InstallationError):
212 """Built metadata contains inconsistent information.
213
214 This is raised when the metadata contains values (e.g. name and version)
215 that do not match the information previously obtained from sdist filename,
216 user-supplied ``#egg=`` value, or an install requirement name.
217 """
218
219 def __init__(
220 self, ireq: InstallRequirement, field: str, f_val: str, m_val: str
221 ) -> None:
222 self.ireq = ireq
223 self.field = field
224 self.f_val = f_val
225 self.m_val = m_val
226
227 def __str__(self) -> str:
228 return (
229 f"Requested {self.ireq} has inconsistent {self.field}: "
230 f"expected {self.f_val!r}, but metadata has {self.m_val!r}"
231 )
232
233
234class SidecarMetadataInconsistent(MetadataInconsistent):
235 """The wheel's METADATA disagrees with its PEP 658 ``.metadata`` file.
236
237 Raised after the wheel has been downloaded and hash-verified, when a
238 resolver-affecting field in the wheel's embedded ``METADATA`` does not
239 match the value taken from the remote ``.metadata`` sidecar that drove
240 resolution. ``f_val`` is the sidecar value, ``m_val`` is the wheel value.
241 """
242
243 def __str__(self) -> str:
244 return (
245 f"Requested {self.ireq} has inconsistent {self.field} between "
246 f"its PEP 658 .metadata file and the wheel's METADATA: "
247 f"sidecar has {self.f_val!r}, wheel has {self.m_val!r}"
248 )
249
250
251class MetadataInvalid(InstallationError):
252 """Metadata is invalid."""
253
254 def __init__(self, ireq: InstallRequirement, error: str) -> None:
255 self.ireq = ireq
256 self.error = error
257
258 def __str__(self) -> str:
259 return f"Requested {self.ireq} has invalid metadata: {self.error}"
260
261
262class MetadataGenerationFailed(DiagnosticPipError, InstallationError):
263 reference = "metadata-generation-failed"
264
265 def __init__(
266 self,
267 *,
268 package_details: str,
269 ) -> None:
270 super().__init__(
271 message="Encountered error while generating package metadata.",
272 context=escape(package_details),
273 hint_stmt="See above for details.",
274 note_stmt="This is an issue with the package mentioned above, not pip.",
275 )
276
277 def __str__(self) -> str:
278 return "metadata generation failed"
279
280
281class UnsupportedPythonVersion(InstallationError):
282 """Unsupported python version according to Requires-Python package
283 metadata."""
284
285
286class ConfigurationFileCouldNotBeLoaded(ConfigurationError):
287 """When there are errors while loading a configuration file"""
288
289 def __init__(
290 self,
291 reason: str = "could not be loaded",
292 fname: str | None = None,
293 error: configparser.Error | None = None,
294 ) -> None:
295 super().__init__(error)
296 self.reason = reason
297 self.fname = fname
298 self.error = error
299
300 def __str__(self) -> str:
301 if self.fname is not None:
302 message_part = f" in {self.fname}."
303 else:
304 assert self.error is not None
305 message_part = f".\n{self.error}\n"
306 return f"Configuration file {self.reason}{message_part}"
307
308
309_DEFAULT_EXTERNALLY_MANAGED_ERROR = f"""\
310The Python environment under {sys.prefix} is managed externally, and may not be
311manipulated by the user. Please use specific tooling from the distributor of
312the Python installation to interact with this environment instead.
313"""
314
315
316class ExternallyManagedEnvironment(DiagnosticPipError):
317 """The current environment is externally managed.
318
319 This is raised when the current environment is externally managed, as
320 defined by `PEP 668`_. The ``EXTERNALLY-MANAGED`` configuration is checked
321 and displayed when the error is bubbled up to the user.
322
323 :param error: The error message read from ``EXTERNALLY-MANAGED``.
324 """
325
326 reference = "externally-managed-environment"
327
328 def __init__(self, error: str | None) -> None:
329 if error is None:
330 context = Text(_DEFAULT_EXTERNALLY_MANAGED_ERROR)
331 else:
332 context = Text(error)
333 super().__init__(
334 message="This environment is externally managed",
335 context=context,
336 note_stmt=(
337 "If you believe this is a mistake, please contact your "
338 "Python installation or OS distribution provider. "
339 "You can override this, at the risk of breaking your Python "
340 "installation or OS, by passing --break-system-packages."
341 ),
342 hint_stmt=Text("See PEP 668 for the detailed specification."),
343 )
344
345 @staticmethod
346 def _iter_externally_managed_error_keys() -> Iterator[str]:
347 # LC_MESSAGES is in POSIX, but not the C standard. The most common
348 # platform that does not implement this category is Windows, where
349 # using other categories for console message localization is equally
350 # unreliable, so we fall back to the locale-less vendor message. This
351 # can always be re-evaluated when a vendor proposes a new alternative.
352 try:
353 category = locale.LC_MESSAGES
354 except AttributeError:
355 lang: str | None = None
356 else:
357 lang, _ = locale.getlocale(category)
358 if lang is not None:
359 yield f"Error-{lang}"
360 for sep in ("-", "_"):
361 before, found, _ = lang.partition(sep)
362 if not found:
363 continue
364 yield f"Error-{before}"
365 yield "Error"
366
367 @classmethod
368 def from_config(
369 cls,
370 config: pathlib.Path | str,
371 ) -> ExternallyManagedEnvironment:
372 parser = configparser.ConfigParser(interpolation=None)
373 try:
374 parser.read(config, encoding="utf-8")
375 section = parser["externally-managed"]
376 for key in cls._iter_externally_managed_error_keys():
377 with contextlib.suppress(KeyError):
378 return cls(section[key])
379 except KeyError:
380 pass
381 except (OSError, UnicodeDecodeError, configparser.ParsingError):
382 from pip._internal.utils._log import VERBOSE
383
384 exc_info = logger.isEnabledFor(VERBOSE)
385 logger.warning("Failed to read %s", config, exc_info=exc_info)
386 return cls(None)
387
388
389class InvalidInstalledPackage(DiagnosticPipError):
390 reference = "invalid-installed-package"
391
392 def __init__(
393 self,
394 *,
395 dist: BaseDistribution,
396 invalid_exc: InvalidRequirement | InvalidVersion,
397 ) -> None:
398 installed_location = dist.installed_location
399
400 if isinstance(invalid_exc, InvalidRequirement):
401 invalid_type = "requirement"
402 else:
403 invalid_type = "version"
404
405 super().__init__(
406 message=Text(
407 f"Cannot process installed package {dist} "
408 + (f"in {installed_location!r} " if installed_location else "")
409 + f"because it has an invalid {invalid_type}:\n{invalid_exc.args[0]}"
410 ),
411 context=(
412 "Starting with pip 24.1, packages with invalid "
413 f"{invalid_type}s can not be processed."
414 ),
415 hint_stmt="To proceed this package must be uninstalled.",
416 )
417
418
419class ResolutionTooDeepError(DiagnosticPipError):
420 """Raised when the dependency resolver exceeds the maximum recursion depth."""
421
422 reference = "resolution-too-deep"
423
424 def __init__(self) -> None:
425 super().__init__(
426 message="Dependency resolution exceeded maximum depth",
427 context=(
428 "Pip cannot resolve the current dependencies as the dependency graph "
429 "is too complex for pip to solve efficiently."
430 ),
431 hint_stmt=(
432 "Try adding lower bounds to constrain your dependencies, "
433 "for example: 'package>=2.0.0' instead of just 'package'. "
434 ),
435 link="https://pip.pypa.io/en/stable/topics/dependency-resolution/#handling-resolution-too-deep-errors",
436 )
437
438
439class InstallWheelBuildError(DiagnosticPipError):
440 reference = "failed-wheel-build-for-install"
441
442 def __init__(self, failed: list[InstallRequirement]) -> None:
443 super().__init__(
444 message=(
445 "Failed to build installable wheels for some "
446 "pyproject.toml based projects"
447 ),
448 context=", ".join(r.name for r in failed), # type: ignore
449 hint_stmt=None,
450 )
451
452
453class InvalidEggFragment(DiagnosticPipError):
454 reference = "invalid-egg-fragment"
455
456 def __init__(self, link: Link, fragment: str) -> None:
457 hint = ""
458 if ">" in fragment or "=" in fragment or "<" in fragment:
459 hint = (
460 "Version specifiers are silently ignored for URL references. "
461 "Remove them. "
462 )
463 if "[" in fragment and "]" in fragment:
464 hint += "Try using the Direct URL requirement syntax: 'name[extra] @ URL'"
465
466 if not hint:
467 hint = "Egg fragments can only be a valid project name."
468
469 super().__init__(
470 message=f"The '{escape(fragment)}' egg fragment is invalid",
471 context=f"from '{escape(str(link))}'",
472 hint_stmt=escape(hint),
473 )