|
1 | 1 | # The following comment should be removed at some point in the future.
|
2 | 2 | # mypy: strict-optional=False
|
3 | 3 |
|
| 4 | +import configparser |
4 | 5 | import contextlib
|
5 | 6 | import errno
|
6 | 7 | import getpass
|
7 | 8 | import hashlib
|
8 | 9 | import io
|
| 10 | +import locale |
9 | 11 | import logging
|
10 | 12 | import os
|
11 | 13 | import posixpath
|
12 | 14 | import shutil
|
13 | 15 | import stat
|
14 | 16 | import sys
|
| 17 | +import sysconfig |
15 | 18 | import urllib.parse
|
16 | 19 | from io import StringIO
|
17 | 20 | from itertools import filterfalse, tee, zip_longest
|
|
57 | 60 | "captured_stdout",
|
58 | 61 | "ensure_dir",
|
59 | 62 | "remove_auth_from_url",
|
| 63 | + "get_externally_managed_error", |
60 | 64 | "ConfiguredBuildBackendHookCaller",
|
61 | 65 | ]
|
62 | 66 |
|
@@ -581,6 +585,58 @@ def protect_pip_from_modification_on_windows(modifying_pip: bool) -> None:
|
581 | 585 | )
|
582 | 586 |
|
583 | 587 |
|
| 588 | +_DEFAULT_EXTERNALLY_MANAGED_ERROR = f"""\ |
| 589 | +The Python environment under {sys.prefix} is managed externally, and may not be |
| 590 | +manipulated by the user. Please use specific tooling from the distributor of |
| 591 | +the Python installation to interact with this environment instead. |
| 592 | +""" |
| 593 | + |
| 594 | + |
| 595 | +def _iter_externally_managed_error_keys() -> Iterator[str]: |
| 596 | + lang, _ = locale.getlocale(locale.LC_MESSAGES) |
| 597 | + if lang is not None: |
| 598 | + yield f"Error-{lang}" |
| 599 | + for sep in ("-", "_"): |
| 600 | + before, found, _ = lang.partition(sep) |
| 601 | + if not found: |
| 602 | + continue |
| 603 | + yield f"Error-{before}" |
| 604 | + yield "Error" |
| 605 | + |
| 606 | + |
| 607 | +def get_externally_managed_error() -> Optional[str]: |
| 608 | + """Get an error message from the EXTERNALLY-MANAGED config file. |
| 609 | +
|
| 610 | + This checks whether the current environment pip is running in is externally |
| 611 | + managed. If the EXTERNALLY-MANAGED file is found, the vendor-provided error |
| 612 | + message is read and returned (if available; a default message is used |
| 613 | + otherwise), as specified in `PEP 668`_. |
| 614 | +
|
| 615 | + If the current environment is *not* externally managed, *None* is returned. |
| 616 | +
|
| 617 | + .. _`PEP 668`: https://peps.python.org/pep-0668/ |
| 618 | + """ |
| 619 | + if running_under_virtualenv(): |
| 620 | + return None |
| 621 | + marker = os.path.join(sysconfig.get_path("stdlib"), "EXTERNALLY-MANAGED") |
| 622 | + if not os.path.isfile(marker): |
| 623 | + return None |
| 624 | + try: |
| 625 | + parser = configparser.ConfigParser(interpolation=None) |
| 626 | + parser.read(marker, encoding="utf-8") |
| 627 | + except (OSError, UnicodeDecodeError) as e: |
| 628 | + logger.warning("Ignoring %s due to error %s", marker, e) |
| 629 | + return _DEFAULT_EXTERNALLY_MANAGED_ERROR |
| 630 | + try: |
| 631 | + section = parser["externally-managed"] |
| 632 | + except KeyError: |
| 633 | + return _DEFAULT_EXTERNALLY_MANAGED_ERROR |
| 634 | + for key in _iter_externally_managed_error_keys(): |
| 635 | + with contextlib.suppress(KeyError): |
| 636 | + return section[key] |
| 637 | + return _DEFAULT_EXTERNALLY_MANAGED_ERROR |
| 638 | + |
| 639 | + |
584 | 640 | def is_console_interactive() -> bool:
|
585 | 641 | """Is this console interactive?"""
|
586 | 642 | return sys.stdin is not None and sys.stdin.isatty()
|
|
0 commit comments