Skip to content

Commit e27a819

Browse files
committed
Use ExternallyManagedEnvironment to show error
This moves most of the displaying logic into the exception class so it can better leverage DiagnosticPipError and Rich functionalities.
1 parent 69cb3f8 commit e27a819

File tree

4 files changed

+82
-56
lines changed

4 files changed

+82
-56
lines changed

src/pip/_internal/commands/install.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@
4141
from pip._internal.utils.filesystem import test_writable_dir
4242
from pip._internal.utils.logging import getLogger
4343
from pip._internal.utils.misc import (
44+
check_externally_managed,
4445
ensure_dir,
45-
get_externally_managed_error,
4646
get_pip_version,
4747
protect_pip_from_modification_on_windows,
4848
write_output,
@@ -296,9 +296,7 @@ def run(self, options: Values, args: List[str]) -> int:
296296
and options.prefix_path is None
297297
)
298298
if installing_into_current_environment:
299-
externally_managed_error = get_externally_managed_error()
300-
if externally_managed_error is not None:
301-
raise InstallationError(externally_managed_error)
299+
check_externally_managed()
302300

303301
upgrade_strategy = "to-satisfy-only"
304302
if options.upgrade:

src/pip/_internal/commands/uninstall.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
install_req_from_parsed_requirement,
1616
)
1717
from pip._internal.utils.misc import (
18-
get_externally_managed_error,
18+
check_externally_managed,
1919
protect_pip_from_modification_on_windows,
2020
)
2121

@@ -93,9 +93,7 @@ def run(self, options: Values, args: List[str]) -> int:
9393
f'"pip help {self.name}")'
9494
)
9595

96-
externally_managed_error = get_externally_managed_error()
97-
if externally_managed_error is not None:
98-
raise InstallationError(externally_managed_error)
96+
check_externally_managed()
9997

10098
protect_pip_from_modification_on_windows(
10199
modifying_pip="pip" in reqs_to_uninstall

src/pip/_internal/exceptions.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66
"""
77

88
import configparser
9+
import contextlib
10+
import locale
911
import re
12+
import sys
1013
from itertools import chain, groupby, repeat
11-
from typing import TYPE_CHECKING, Dict, List, Optional, Union
14+
from typing import TYPE_CHECKING, Dict, Iterator, List, Optional, Union
1215

1316
from pip._vendor.requests.models import Request, Response
1417
from pip._vendor.rich.console import Console, ConsoleOptions, RenderResult
@@ -658,3 +661,62 @@ def __str__(self) -> str:
658661
assert self.error is not None
659662
message_part = f".\n{self.error}\n"
660663
return f"Configuration file {self.reason}{message_part}"
664+
665+
666+
_DEFAULT_EXTERNALLY_MANAGED_ERROR = f"""\
667+
The Python environment under {sys.prefix} is managed externally, and may not be
668+
manipulated by the user. Please use specific tooling from the distributor of
669+
the Python installation to interact with this environment instead.
670+
"""
671+
672+
673+
class ExternallyManagedEnvironment(DiagnosticPipError):
674+
"""The current environment is externally managed.
675+
676+
This is raised when the current environment is externally managed, as
677+
defined by `PEP 668`_. The ``EXTERNALLY-MANAGED`` configuration is checked
678+
and displayed when the error is bubbled up to the user.
679+
680+
:param error: The error message read from ``EXTERNALLY-MANAGED``.
681+
"""
682+
683+
def __init__(self, error: Optional[str]) -> None:
684+
if error is None:
685+
context = Text(_DEFAULT_EXTERNALLY_MANAGED_ERROR)
686+
else:
687+
context = Text(error)
688+
super().__init__(
689+
message="This environment is externally managed",
690+
context=context,
691+
note_stmt=(
692+
"If you believe this is a mistake, please contact your "
693+
"Python installation or OS distribution provider."
694+
),
695+
hint_stmt=Text("See PEP 668 for the detailed specification."),
696+
)
697+
698+
@staticmethod
699+
def _iter_externally_managed_error_keys() -> Iterator[str]:
700+
lang, _ = locale.getlocale(locale.LC_MESSAGES)
701+
if lang is not None:
702+
yield f"Error-{lang}"
703+
for sep in ("-", "_"):
704+
before, found, _ = lang.partition(sep)
705+
if not found:
706+
continue
707+
yield f"Error-{before}"
708+
yield "Error"
709+
710+
@classmethod
711+
def from_config(
712+
cls,
713+
parser: configparser.ConfigParser,
714+
) -> "ExternallyManagedEnvironment":
715+
try:
716+
section = parser["externally-managed"]
717+
except KeyError:
718+
return cls(None)
719+
for key in cls._iter_externally_managed_error_keys():
720+
with contextlib.suppress(KeyError):
721+
return cls(section[key])
722+
return cls(None)

src/pip/_internal/utils/misc.py

Lines changed: 15 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
import getpass
88
import hashlib
99
import io
10-
import locale
11-
import logging
1210
import os
1311
import posixpath
1412
import shutil
@@ -41,8 +39,9 @@
4139
from pip._vendor.tenacity import retry, stop_after_delay, wait_fixed
4240

4341
from pip import __version__
44-
from pip._internal.exceptions import CommandError
42+
from pip._internal.exceptions import CommandError, ExternallyManagedEnvironment
4543
from pip._internal.locations import get_major_minor_version
44+
from pip._internal.utils._log import VERBOSE, getLogger
4645
from pip._internal.utils.compat import WINDOWS
4746
from pip._internal.utils.virtualenv import running_under_virtualenv
4847

@@ -60,12 +59,12 @@
6059
"captured_stdout",
6160
"ensure_dir",
6261
"remove_auth_from_url",
63-
"get_externally_managed_error",
62+
"check_externally_managed",
6463
"ConfiguredBuildBackendHookCaller",
6564
]
6665

6766

68-
logger = logging.getLogger(__name__)
67+
logger = getLogger(__name__)
6968

7069
T = TypeVar("T")
7170
ExcInfo = Tuple[Type[BaseException], BaseException, TracebackType]
@@ -585,56 +584,25 @@ def protect_pip_from_modification_on_windows(modifying_pip: bool) -> None:
585584
)
586585

587586

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-
"""
587+
def check_externally_managed() -> None:
588+
"""Check whether the current environment is externally managed.
593589
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/
590+
If the ``EXTERNALLY-MANAGED`` config file is found, the current environment
591+
is considered externally managed, and an ExternallyManagedEnvironment is
592+
raised.
618593
"""
619594
if running_under_virtualenv():
620595
return None
621596
marker = os.path.join(sysconfig.get_path("stdlib"), "EXTERNALLY-MANAGED")
622597
if not os.path.isfile(marker):
623-
return None
598+
return
599+
parser = configparser.ConfigParser(interpolation=None)
624600
try:
625-
parser = configparser.ConfigParser(interpolation=None)
626601
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
602+
except (OSError, UnicodeDecodeError):
603+
exc_info = logger.isEnabledFor(VERBOSE)
604+
logger.warning("Failed to read %s", marker, exc_info=exc_info)
605+
raise ExternallyManagedEnvironment.from_config(parser)
638606

639607

640608
def is_console_interactive() -> bool:

0 commit comments

Comments
 (0)