]>
Commit | Line | Data |
---|---|---|
53e6db90 DC |
1 | """Exceptions used throughout package. |
2 | ||
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. | |
6 | """ | |
7 | ||
8 | import configparser | |
9 | import contextlib | |
10 | import locale | |
11 | import logging | |
12 | import pathlib | |
13 | import re | |
14 | import sys | |
15 | from itertools import chain, groupby, repeat | |
16 | from typing import TYPE_CHECKING, Dict, Iterator, List, Optional, Union | |
17 | ||
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 | |
22 | ||
23 | if TYPE_CHECKING: | |
24 | from hashlib import _Hash | |
25 | from typing import Literal | |
26 | ||
27 | from pip._internal.metadata import BaseDistribution | |
28 | from pip._internal.req.req_install import InstallRequirement | |
29 | ||
30 | logger = logging.getLogger(__name__) | |
31 | ||
32 | ||
33 | # | |
34 | # Scaffolding | |
35 | # | |
36 | def _is_kebab_case(s: str) -> bool: | |
37 | return re.match(r"^[a-z]+(-[a-z]+)*$", s) is not None | |
38 | ||
39 | ||
40 | def _prefix_with_indent( | |
41 | s: Union[Text, str], | |
42 | console: Console, | |
43 | *, | |
44 | prefix: str, | |
45 | indent: str, | |
46 | ) -> Text: | |
47 | if isinstance(s, Text): | |
48 | text = s | |
49 | else: | |
50 | text = console.render_str(s) | |
51 | ||
52 | return console.render_str(prefix, overflow="ignore") + console.render_str( | |
53 | f"\n{indent}", overflow="ignore" | |
54 | ).join(text.split(allow_blank=True)) | |
55 | ||
56 | ||
57 | class PipError(Exception): | |
58 | """The base pip error.""" | |
59 | ||
60 | ||
61 | class DiagnosticPipError(PipError): | |
62 | """An error, that presents diagnostic information to the user. | |
63 | ||
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. | |
68 | ||
69 | This is adapted from the error output styling in `sphinx-theme-builder`. | |
70 | """ | |
71 | ||
72 | reference: str | |
73 | ||
74 | def __init__( | |
75 | self, | |
76 | *, | |
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, | |
84 | ) -> None: | |
85 | # Ensure a proper reference is provided. | |
86 | if reference is None: | |
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!" | |
90 | ||
91 | self.kind = kind | |
92 | self.reference = reference | |
93 | ||
94 | self.message = message | |
95 | self.context = context | |
96 | ||
97 | self.note_stmt = note_stmt | |
98 | self.hint_stmt = hint_stmt | |
99 | ||
100 | self.link = link | |
101 | ||
102 | super().__init__(f"<{self.__class__.__name__}: {self.reference}>") | |
103 | ||
104 | def __repr__(self) -> str: | |
105 | return ( | |
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}" | |
112 | ")>" | |
113 | ) | |
114 | ||
115 | def __rich_console__( | |
116 | self, | |
117 | console: Console, | |
118 | options: ConsoleOptions, | |
119 | ) -> RenderResult: | |
120 | colour = "red" if self.kind == "error" else "yellow" | |
121 | ||
122 | yield f"[{colour} bold]{self.kind}[/]: [bold]{self.reference}[/]" | |
123 | yield "" | |
124 | ||
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( | |
129 | self.message, | |
130 | console, | |
131 | prefix=f"[{colour}]×[/] ", | |
132 | indent=f"[{colour}]│[/] ", | |
133 | ) | |
134 | yield _prefix_with_indent( | |
135 | self.context, | |
136 | console, | |
137 | prefix=f"[{colour}]╰─>[/] ", | |
138 | indent=f"[{colour}] [/] ", | |
139 | ) | |
140 | else: | |
141 | yield _prefix_with_indent( | |
142 | self.message, | |
143 | console, | |
144 | prefix="[red]×[/] ", | |
145 | indent=" ", | |
146 | ) | |
147 | else: | |
148 | yield self.message | |
149 | if self.context is not None: | |
150 | yield "" | |
151 | yield self.context | |
152 | ||
153 | if self.note_stmt is not None or self.hint_stmt is not None: | |
154 | yield "" | |
155 | ||
156 | if self.note_stmt is not None: | |
157 | yield _prefix_with_indent( | |
158 | self.note_stmt, | |
159 | console, | |
160 | prefix="[magenta bold]note[/]: ", | |
161 | indent=" ", | |
162 | ) | |
163 | if self.hint_stmt is not None: | |
164 | yield _prefix_with_indent( | |
165 | self.hint_stmt, | |
166 | console, | |
167 | prefix="[cyan bold]hint[/]: ", | |
168 | indent=" ", | |
169 | ) | |
170 | ||
171 | if self.link is not None: | |
172 | yield "" | |
173 | yield f"Link: {self.link}" | |
174 | ||
175 | ||
176 | # | |
177 | # Actual Errors | |
178 | # | |
179 | class ConfigurationError(PipError): | |
180 | """General exception in configuration""" | |
181 | ||
182 | ||
183 | class InstallationError(PipError): | |
184 | """General exception during installation""" | |
185 | ||
186 | ||
187 | class UninstallationError(PipError): | |
188 | """General exception during uninstallation""" | |
189 | ||
190 | ||
191 | class MissingPyProjectBuildRequires(DiagnosticPipError): | |
192 | """Raised when pyproject.toml has `build-system`, but no `build-system.requires`.""" | |
193 | ||
194 | reference = "missing-pyproject-build-system-requires" | |
195 | ||
196 | def __init__(self, *, package: str) -> None: | |
197 | super().__init__( | |
198 | message=f"Can not process {escape(package)}", | |
199 | context=Text( | |
200 | "This package has an invalid pyproject.toml file.\n" | |
201 | "The [build-system] table is missing the mandatory `requires` key." | |
202 | ), | |
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."), | |
205 | ) | |
206 | ||
207 | ||
208 | class InvalidPyProjectBuildRequires(DiagnosticPipError): | |
209 | """Raised when pyproject.toml an invalid `build-system.requires`.""" | |
210 | ||
211 | reference = "invalid-pyproject-build-system-requires" | |
212 | ||
213 | def __init__(self, *, package: str, reason: str) -> None: | |
214 | super().__init__( | |
215 | message=f"Can not process {escape(package)}", | |
216 | context=Text( | |
217 | "This package has an invalid `build-system.requires` key in " | |
218 | f"pyproject.toml.\n{reason}" | |
219 | ), | |
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."), | |
222 | ) | |
223 | ||
224 | ||
225 | class NoneMetadataError(PipError): | |
226 | """Raised when accessing a Distribution's "METADATA" or "PKG-INFO". | |
227 | ||
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 | |
231 | errors. | |
232 | """ | |
233 | ||
234 | def __init__( | |
235 | self, | |
236 | dist: "BaseDistribution", | |
237 | metadata_name: str, | |
238 | ) -> None: | |
239 | """ | |
240 | :param dist: A Distribution object. | |
241 | :param metadata_name: The name of the metadata being accessed | |
242 | (can be "METADATA" or "PKG-INFO"). | |
243 | """ | |
244 | self.dist = dist | |
245 | self.metadata_name = metadata_name | |
246 | ||
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( | |
251 | self.metadata_name, | |
252 | self.dist, | |
253 | ) | |
254 | ||
255 | ||
256 | class UserInstallationInvalid(InstallationError): | |
257 | """A --user install is requested on an environment without user site.""" | |
258 | ||
259 | def __str__(self) -> str: | |
260 | return "User base directory is not specified" | |
261 | ||
262 | ||
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" | |
267 | ||
268 | ||
269 | class DistributionNotFound(InstallationError): | |
270 | """Raised when a distribution cannot be found to satisfy a requirement""" | |
271 | ||
272 | ||
273 | class RequirementsFileParseError(InstallationError): | |
274 | """Raised when a general error occurs parsing a requirements file line.""" | |
275 | ||
276 | ||
277 | class BestVersionAlreadyInstalled(PipError): | |
278 | """Raised when the most up-to-date version of a package is already | |
279 | installed.""" | |
280 | ||
281 | ||
282 | class BadCommand(PipError): | |
283 | """Raised when virtualenv or a command is not found""" | |
284 | ||
285 | ||
286 | class CommandError(PipError): | |
287 | """Raised when there is an error in command-line arguments""" | |
288 | ||
289 | ||
290 | class PreviousBuildDirError(PipError): | |
291 | """Raised when there's a previous conflicting build directory""" | |
292 | ||
293 | ||
294 | class NetworkConnectionError(PipError): | |
295 | """HTTP connection error""" | |
296 | ||
297 | def __init__( | |
298 | self, | |
299 | error_msg: str, | |
300 | response: Optional[Response] = None, | |
301 | request: Optional[Request] = None, | |
302 | ) -> None: | |
303 | """ | |
304 | Initialize NetworkConnectionError with `request` and `response` | |
305 | objects. | |
306 | """ | |
307 | self.response = response | |
308 | self.request = request | |
309 | self.error_msg = error_msg | |
310 | if ( | |
311 | self.response is not None | |
312 | and not self.request | |
313 | and hasattr(response, "request") | |
314 | ): | |
315 | self.request = self.response.request | |
316 | super().__init__(error_msg, response, request) | |
317 | ||
318 | def __str__(self) -> str: | |
319 | return str(self.error_msg) | |
320 | ||
321 | ||
322 | class InvalidWheelFilename(InstallationError): | |
323 | """Invalid wheel filename.""" | |
324 | ||
325 | ||
326 | class UnsupportedWheel(InstallationError): | |
327 | """Unsupported wheel.""" | |
328 | ||
329 | ||
330 | class InvalidWheel(InstallationError): | |
331 | """Invalid (e.g. corrupt) wheel.""" | |
332 | ||
333 | def __init__(self, location: str, name: str): | |
334 | self.location = location | |
335 | self.name = name | |
336 | ||
337 | def __str__(self) -> str: | |
338 | return f"Wheel '{self.name}' located at {self.location} is invalid." | |
339 | ||
340 | ||
341 | class MetadataInconsistent(InstallationError): | |
342 | """Built metadata contains inconsistent information. | |
343 | ||
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. | |
347 | """ | |
348 | ||
349 | def __init__( | |
350 | self, ireq: "InstallRequirement", field: str, f_val: str, m_val: str | |
351 | ) -> None: | |
352 | self.ireq = ireq | |
353 | self.field = field | |
354 | self.f_val = f_val | |
355 | self.m_val = m_val | |
356 | ||
357 | def __str__(self) -> str: | |
358 | return ( | |
359 | f"Requested {self.ireq} has inconsistent {self.field}: " | |
360 | f"expected {self.f_val!r}, but metadata has {self.m_val!r}" | |
361 | ) | |
362 | ||
363 | ||
364 | class LegacyInstallFailure(DiagnosticPipError): | |
365 | """Error occurred while executing `setup.py install`""" | |
366 | ||
367 | reference = "legacy-install-failure" | |
368 | ||
369 | def __init__(self, package_details: str) -> None: | |
370 | super().__init__( | |
371 | message="Encountered error while trying to install package.", | |
372 | context=package_details, | |
373 | hint_stmt="See above for output from the failure.", | |
374 | note_stmt="This is an issue with the package mentioned above, not pip.", | |
375 | ) | |
376 | ||
377 | ||
378 | class InstallationSubprocessError(DiagnosticPipError, InstallationError): | |
379 | """A subprocess call failed.""" | |
380 | ||
381 | reference = "subprocess-exited-with-error" | |
382 | ||
383 | def __init__( | |
384 | self, | |
385 | *, | |
386 | command_description: str, | |
387 | exit_code: int, | |
388 | output_lines: Optional[List[str]], | |
389 | ) -> None: | |
390 | if output_lines is None: | |
391 | output_prompt = Text("See above for output.") | |
392 | else: | |
393 | output_prompt = ( | |
394 | Text.from_markup(f"[red][{len(output_lines)} lines of output][/]\n") | |
395 | + Text("".join(output_lines)) | |
396 | + Text.from_markup(R"[red]\[end of output][/]") | |
397 | ) | |
398 | ||
399 | super().__init__( | |
400 | message=( | |
401 | f"[green]{escape(command_description)}[/] did not run successfully.\n" | |
402 | f"exit code: {exit_code}" | |
403 | ), | |
404 | context=output_prompt, | |
405 | hint_stmt=None, | |
406 | note_stmt=( | |
407 | "This error originates from a subprocess, and is likely not a " | |
408 | "problem with pip." | |
409 | ), | |
410 | ) | |
411 | ||
412 | self.command_description = command_description | |
413 | self.exit_code = exit_code | |
414 | ||
415 | def __str__(self) -> str: | |
416 | return f"{self.command_description} exited with {self.exit_code}" | |
417 | ||
418 | ||
419 | class MetadataGenerationFailed(InstallationSubprocessError, InstallationError): | |
420 | reference = "metadata-generation-failed" | |
421 | ||
422 | def __init__( | |
423 | self, | |
424 | *, | |
425 | package_details: str, | |
426 | ) -> None: | |
427 | super(InstallationSubprocessError, self).__init__( | |
428 | message="Encountered error while generating package metadata.", | |
429 | context=escape(package_details), | |
430 | hint_stmt="See above for details.", | |
431 | note_stmt="This is an issue with the package mentioned above, not pip.", | |
432 | ) | |
433 | ||
434 | def __str__(self) -> str: | |
435 | return "metadata generation failed" | |
436 | ||
437 | ||
438 | class HashErrors(InstallationError): | |
439 | """Multiple HashError instances rolled into one for reporting""" | |
440 | ||
441 | def __init__(self) -> None: | |
442 | self.errors: List["HashError"] = [] | |
443 | ||
444 | def append(self, error: "HashError") -> None: | |
445 | self.errors.append(error) | |
446 | ||
447 | def __str__(self) -> str: | |
448 | lines = [] | |
449 | self.errors.sort(key=lambda e: e.order) | |
450 | for cls, errors_of_cls in groupby(self.errors, lambda e: e.__class__): | |
451 | lines.append(cls.head) | |
452 | lines.extend(e.body() for e in errors_of_cls) | |
453 | if lines: | |
454 | return "\n".join(lines) | |
455 | return "" | |
456 | ||
457 | def __bool__(self) -> bool: | |
458 | return bool(self.errors) | |
459 | ||
460 | ||
461 | class HashError(InstallationError): | |
462 | """ | |
463 | A failure to verify a package against known-good hashes | |
464 | ||
465 | :cvar order: An int sorting hash exception classes by difficulty of | |
466 | recovery (lower being harder), so the user doesn't bother fretting | |
467 | about unpinned packages when he has deeper issues, like VCS | |
468 | dependencies, to deal with. Also keeps error reports in a | |
469 | deterministic order. | |
470 | :cvar head: A section heading for display above potentially many | |
471 | exceptions of this kind | |
472 | :ivar req: The InstallRequirement that triggered this error. This is | |
473 | pasted on after the exception is instantiated, because it's not | |
474 | typically available earlier. | |
475 | ||
476 | """ | |
477 | ||
478 | req: Optional["InstallRequirement"] = None | |
479 | head = "" | |
480 | order: int = -1 | |
481 | ||
482 | def body(self) -> str: | |
483 | """Return a summary of me for display under the heading. | |
484 | ||
485 | This default implementation simply prints a description of the | |
486 | triggering requirement. | |
487 | ||
488 | :param req: The InstallRequirement that provoked this error, with | |
489 | its link already populated by the resolver's _populate_link(). | |
490 | ||
491 | """ | |
492 | return f" {self._requirement_name()}" | |
493 | ||
494 | def __str__(self) -> str: | |
495 | return f"{self.head}\n{self.body()}" | |
496 | ||
497 | def _requirement_name(self) -> str: | |
498 | """Return a description of the requirement that triggered me. | |
499 | ||
500 | This default implementation returns long description of the req, with | |
501 | line numbers | |
502 | ||
503 | """ | |
504 | return str(self.req) if self.req else "unknown package" | |
505 | ||
506 | ||
507 | class VcsHashUnsupported(HashError): | |
508 | """A hash was provided for a version-control-system-based requirement, but | |
509 | we don't have a method for hashing those.""" | |
510 | ||
511 | order = 0 | |
512 | head = ( | |
513 | "Can't verify hashes for these requirements because we don't " | |
514 | "have a way to hash version control repositories:" | |
515 | ) | |
516 | ||
517 | ||
518 | class DirectoryUrlHashUnsupported(HashError): | |
519 | """A hash was provided for a version-control-system-based requirement, but | |
520 | we don't have a method for hashing those.""" | |
521 | ||
522 | order = 1 | |
523 | head = ( | |
524 | "Can't verify hashes for these file:// requirements because they " | |
525 | "point to directories:" | |
526 | ) | |
527 | ||
528 | ||
529 | class HashMissing(HashError): | |
530 | """A hash was needed for a requirement but is absent.""" | |
531 | ||
532 | order = 2 | |
533 | head = ( | |
534 | "Hashes are required in --require-hashes mode, but they are " | |
535 | "missing from some requirements. Here is a list of those " | |
536 | "requirements along with the hashes their downloaded archives " | |
537 | "actually had. Add lines like these to your requirements files to " | |
538 | "prevent tampering. (If you did not enable --require-hashes " | |
539 | "manually, note that it turns on automatically when any package " | |
540 | "has a hash.)" | |
541 | ) | |
542 | ||
543 | def __init__(self, gotten_hash: str) -> None: | |
544 | """ | |
545 | :param gotten_hash: The hash of the (possibly malicious) archive we | |
546 | just downloaded | |
547 | """ | |
548 | self.gotten_hash = gotten_hash | |
549 | ||
550 | def body(self) -> str: | |
551 | # Dodge circular import. | |
552 | from pip._internal.utils.hashes import FAVORITE_HASH | |
553 | ||
554 | package = None | |
555 | if self.req: | |
556 | # In the case of URL-based requirements, display the original URL | |
557 | # seen in the requirements file rather than the package name, | |
558 | # so the output can be directly copied into the requirements file. | |
559 | package = ( | |
560 | self.req.original_link | |
561 | if self.req.original_link | |
562 | # In case someone feeds something downright stupid | |
563 | # to InstallRequirement's constructor. | |
564 | else getattr(self.req, "req", None) | |
565 | ) | |
566 | return " {} --hash={}:{}".format( | |
567 | package or "unknown package", FAVORITE_HASH, self.gotten_hash | |
568 | ) | |
569 | ||
570 | ||
571 | class HashUnpinned(HashError): | |
572 | """A requirement had a hash specified but was not pinned to a specific | |
573 | version.""" | |
574 | ||
575 | order = 3 | |
576 | head = ( | |
577 | "In --require-hashes mode, all requirements must have their " | |
578 | "versions pinned with ==. These do not:" | |
579 | ) | |
580 | ||
581 | ||
582 | class HashMismatch(HashError): | |
583 | """ | |
584 | Distribution file hash values don't match. | |
585 | ||
586 | :ivar package_name: The name of the package that triggered the hash | |
587 | mismatch. Feel free to write to this after the exception is raise to | |
588 | improve its error message. | |
589 | ||
590 | """ | |
591 | ||
592 | order = 4 | |
593 | head = ( | |
594 | "THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS " | |
595 | "FILE. If you have updated the package versions, please update " | |
596 | "the hashes. Otherwise, examine the package contents carefully; " | |
597 | "someone may have tampered with them." | |
598 | ) | |
599 | ||
600 | def __init__(self, allowed: Dict[str, List[str]], gots: Dict[str, "_Hash"]) -> None: | |
601 | """ | |
602 | :param allowed: A dict of algorithm names pointing to lists of allowed | |
603 | hex digests | |
604 | :param gots: A dict of algorithm names pointing to hashes we | |
605 | actually got from the files under suspicion | |
606 | """ | |
607 | self.allowed = allowed | |
608 | self.gots = gots | |
609 | ||
610 | def body(self) -> str: | |
611 | return " {}:\n{}".format(self._requirement_name(), self._hash_comparison()) | |
612 | ||
613 | def _hash_comparison(self) -> str: | |
614 | """ | |
615 | Return a comparison of actual and expected hash values. | |
616 | ||
617 | Example:: | |
618 | ||
619 | Expected sha256 abcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcde | |
620 | or 123451234512345123451234512345123451234512345 | |
621 | Got bcdefbcdefbcdefbcdefbcdefbcdefbcdefbcdefbcdef | |
622 | ||
623 | """ | |
624 | ||
625 | def hash_then_or(hash_name: str) -> "chain[str]": | |
626 | # For now, all the decent hashes have 6-char names, so we can get | |
627 | # away with hard-coding space literals. | |
628 | return chain([hash_name], repeat(" or")) | |
629 | ||
630 | lines: List[str] = [] | |
631 | for hash_name, expecteds in self.allowed.items(): | |
632 | prefix = hash_then_or(hash_name) | |
633 | lines.extend( | |
634 | (" Expected {} {}".format(next(prefix), e)) for e in expecteds | |
635 | ) | |
636 | lines.append( | |
637 | " Got {}\n".format(self.gots[hash_name].hexdigest()) | |
638 | ) | |
639 | return "\n".join(lines) | |
640 | ||
641 | ||
642 | class UnsupportedPythonVersion(InstallationError): | |
643 | """Unsupported python version according to Requires-Python package | |
644 | metadata.""" | |
645 | ||
646 | ||
647 | class ConfigurationFileCouldNotBeLoaded(ConfigurationError): | |
648 | """When there are errors while loading a configuration file""" | |
649 | ||
650 | def __init__( | |
651 | self, | |
652 | reason: str = "could not be loaded", | |
653 | fname: Optional[str] = None, | |
654 | error: Optional[configparser.Error] = None, | |
655 | ) -> None: | |
656 | super().__init__(error) | |
657 | self.reason = reason | |
658 | self.fname = fname | |
659 | self.error = error | |
660 | ||
661 | def __str__(self) -> str: | |
662 | if self.fname is not None: | |
663 | message_part = f" in {self.fname}." | |
664 | else: | |
665 | assert self.error is not None | |
666 | message_part = f".\n{self.error}\n" | |
667 | return f"Configuration file {self.reason}{message_part}" | |
668 | ||
669 | ||
670 | _DEFAULT_EXTERNALLY_MANAGED_ERROR = f"""\ | |
671 | The Python environment under {sys.prefix} is managed externally, and may not be | |
672 | manipulated by the user. Please use specific tooling from the distributor of | |
673 | the Python installation to interact with this environment instead. | |
674 | """ | |
675 | ||
676 | ||
677 | class ExternallyManagedEnvironment(DiagnosticPipError): | |
678 | """The current environment is externally managed. | |
679 | ||
680 | This is raised when the current environment is externally managed, as | |
681 | defined by `PEP 668`_. The ``EXTERNALLY-MANAGED`` configuration is checked | |
682 | and displayed when the error is bubbled up to the user. | |
683 | ||
684 | :param error: The error message read from ``EXTERNALLY-MANAGED``. | |
685 | """ | |
686 | ||
687 | reference = "externally-managed-environment" | |
688 | ||
689 | def __init__(self, error: Optional[str]) -> None: | |
690 | if error is None: | |
691 | context = Text(_DEFAULT_EXTERNALLY_MANAGED_ERROR) | |
692 | else: | |
693 | context = Text(error) | |
694 | super().__init__( | |
695 | message="This environment is externally managed", | |
696 | context=context, | |
697 | note_stmt=( | |
698 | "If you believe this is a mistake, please contact your " | |
699 | "Python installation or OS distribution provider. " | |
700 | "You can override this, at the risk of breaking your Python " | |
701 | "installation or OS, by passing --break-system-packages." | |
702 | ), | |
703 | hint_stmt=Text("See PEP 668 for the detailed specification."), | |
704 | ) | |
705 | ||
706 | @staticmethod | |
707 | def _iter_externally_managed_error_keys() -> Iterator[str]: | |
708 | # LC_MESSAGES is in POSIX, but not the C standard. The most common | |
709 | # platform that does not implement this category is Windows, where | |
710 | # using other categories for console message localization is equally | |
711 | # unreliable, so we fall back to the locale-less vendor message. This | |
712 | # can always be re-evaluated when a vendor proposes a new alternative. | |
713 | try: | |
714 | category = locale.LC_MESSAGES | |
715 | except AttributeError: | |
716 | lang: Optional[str] = None | |
717 | else: | |
718 | lang, _ = locale.getlocale(category) | |
719 | if lang is not None: | |
720 | yield f"Error-{lang}" | |
721 | for sep in ("-", "_"): | |
722 | before, found, _ = lang.partition(sep) | |
723 | if not found: | |
724 | continue | |
725 | yield f"Error-{before}" | |
726 | yield "Error" | |
727 | ||
728 | @classmethod | |
729 | def from_config( | |
730 | cls, | |
731 | config: Union[pathlib.Path, str], | |
732 | ) -> "ExternallyManagedEnvironment": | |
733 | parser = configparser.ConfigParser(interpolation=None) | |
734 | try: | |
735 | parser.read(config, encoding="utf-8") | |
736 | section = parser["externally-managed"] | |
737 | for key in cls._iter_externally_managed_error_keys(): | |
738 | with contextlib.suppress(KeyError): | |
739 | return cls(section[key]) | |
740 | except KeyError: | |
741 | pass | |
742 | except (OSError, UnicodeDecodeError, configparser.ParsingError): | |
743 | from pip._internal.utils._log import VERBOSE | |
744 | ||
745 | exc_info = logger.isEnabledFor(VERBOSE) | |
746 | logger.warning("Failed to read %s", config, exc_info=exc_info) | |
747 | return cls(None) |