From d4b6bca72f1064f2344efe0391c2158569475793 Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Sun, 5 Feb 2023 11:47:38 -0400 Subject: [PATCH] Implement pip install --break-system-packages, as a PEP-668 override The PEP expected an override mechanism to ease the transition to PEP-668 managed installs. This provides a command-line-driven override. --- news/11780.feature.rst | 2 ++ src/pip/_internal/cli/cmdoptions.py | 8 ++++++++ src/pip/_internal/commands/install.py | 6 +++++- src/pip/_internal/commands/uninstall.py | 1 + src/pip/_internal/exceptions.py | 4 +++- tests/functional/test_pep668.py | 17 +++++++++++++++++ 6 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 news/11780.feature.rst diff --git a/news/11780.feature.rst b/news/11780.feature.rst new file mode 100644 index 00000000000..b765de6c59a --- /dev/null +++ b/news/11780.feature.rst @@ -0,0 +1,2 @@ +Implement ``--break-system-packages`` to permit installing packages into +``EXTERNALLY-MANAGED`` Python installations. diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 661c489c73e..6e151632ab0 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -164,6 +164,14 @@ class PipOption(Option): ), ) +override_externally_managed: Callable[..., Option] = partial( + Option, + "--break-system-packages", + dest="override_externally_managed", + action="store_true", + help="Allow pip to install into an EXTERNALLY-MANAGED Python installation", +) + python: Callable[..., Option] = partial( Option, "--python", diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index cecaac2bc5b..b20aeddf835 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -215,6 +215,7 @@ def add_options(self) -> None: self.cmd_opts.add_option(cmdoptions.use_pep517()) self.cmd_opts.add_option(cmdoptions.no_use_pep517()) self.cmd_opts.add_option(cmdoptions.check_build_deps()) + self.cmd_opts.add_option(cmdoptions.override_externally_managed()) self.cmd_opts.add_option(cmdoptions.config_settings()) self.cmd_opts.add_option(cmdoptions.install_options()) @@ -296,7 +297,10 @@ def run(self, options: Values, args: List[str]) -> int: and options.target_dir is None and options.prefix_path is None ) - if installing_into_current_environment: + if ( + installing_into_current_environment + and not options.override_externally_managed + ): check_externally_managed() upgrade_strategy = "to-satisfy-only" diff --git a/src/pip/_internal/commands/uninstall.py b/src/pip/_internal/commands/uninstall.py index e5a4c8e10d4..8c773074203 100644 --- a/src/pip/_internal/commands/uninstall.py +++ b/src/pip/_internal/commands/uninstall.py @@ -58,6 +58,7 @@ def add_options(self) -> None: help="Don't ask for confirmation of uninstall deletions.", ) self.cmd_opts.add_option(cmdoptions.root_user_action()) + self.cmd_opts.add_option(cmdoptions.override_externally_managed()) self.parser.insert_option_group(0, self.cmd_opts) def run(self, options: Values, args: List[str]) -> int: diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index d28713ff79f..d4527295da3 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -696,7 +696,9 @@ def __init__(self, error: Optional[str]) -> None: context=context, note_stmt=( "If you believe this is a mistake, please contact your " - "Python installation or OS distribution provider." + "Python installation or OS distribution provider. " + "You can override this, at the risk of breaking your Python " + "installation or OS, by passing --break-system-packages." ), hint_stmt=Text("See PEP 668 for the detailed specification."), ) diff --git a/tests/functional/test_pep668.py b/tests/functional/test_pep668.py index 1fed85e708e..5b67e4ecfc0 100644 --- a/tests/functional/test_pep668.py +++ b/tests/functional/test_pep668.py @@ -42,6 +42,23 @@ def test_fails(script: PipTestEnvironment, arguments: List[str]) -> None: assert "I am externally managed" in result.stderr +@pytest.mark.parametrize( + "arguments", + [ + pytest.param(["install"], id="install"), + pytest.param(["install", "--user"], id="install-user"), + pytest.param(["install", "--dry-run"], id="install-dry-run"), + pytest.param(["uninstall", "-y"], id="uninstall"), + ], +) +@pytest.mark.usefixtures("patch_check_externally_managed") +def test_succeeds_when_overridden( + script: PipTestEnvironment, arguments: List[str] +) -> None: + result = script.pip(*arguments, "pip") + assert "I am externally managed" not in result.stderr + + @pytest.mark.parametrize( "arguments", [