From 07e58802fbe61228b8953f00d4a659c498abdd6b Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Sun, 5 Feb 2023 11:47:38 -0400 Subject: [PATCH 1/2] Implement --break-system-packages for EXTERNALLY-MANAGED installations 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 | 4 +++- src/pip/_internal/exceptions.py | 4 +++- tests/functional/test_pep668.py | 17 +++++++++++++++++ 6 files changed, 38 insertions(+), 3 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..1f804097e86 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 modify 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..f198fc313ff 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: @@ -93,7 +94,8 @@ def run(self, options: Values, args: List[str]) -> int: f'"pip help {self.name}")' ) - check_externally_managed() + if not options.override_externally_managed: + check_externally_managed() protect_pip_from_modification_on_windows( modifying_pip="pip" in reqs_to_uninstall 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..99ce4eafee9 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", "--break-system-packages") + assert "I am externally managed" not in result.stderr + + @pytest.mark.parametrize( "arguments", [ From 1af8c6dc3e2200077dcf85b99404f32e3aefdd72 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Sun, 5 Feb 2023 19:29:46 +0000 Subject: [PATCH 2/2] Update tests/functional/test_pep668.py --- tests/functional/test_pep668.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/functional/test_pep668.py b/tests/functional/test_pep668.py index 99ce4eafee9..3c1085668fc 100644 --- a/tests/functional/test_pep668.py +++ b/tests/functional/test_pep668.py @@ -46,7 +46,6 @@ def test_fails(script: PipTestEnvironment, arguments: List[str]) -> None: "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"), ],