summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefano Rivera <stefano@rivera.za.net>2023-02-05 23:06:59 -0800
committerGitHub <noreply@github.com>2023-02-06 07:06:59 +0000
commit9abb3c899a3b6e4dad590791f3c8d2421bce66c5 (patch)
tree0af46e5981a1a23f0caf8a8bc604c9f79acb25e0
parent62fb64ac9697e36efce3f72193e4bff0a39dbe14 (diff)
downloadpip-9abb3c899a3b6e4dad590791f3c8d2421bce66c5.tar.gz
Implement `--break-system-packages` for EXTERNALLY-MANAGED installations (#11780)
The PEP 668 expects an override mechanism to ease the transition. This provides an override. --------- Co-authored-by: Pradyun Gedam <pradyunsg@gmail.com>
-rw-r--r--news/11780.feature.rst2
-rw-r--r--src/pip/_internal/cli/cmdoptions.py8
-rw-r--r--src/pip/_internal/commands/install.py6
-rw-r--r--src/pip/_internal/commands/uninstall.py4
-rw-r--r--src/pip/_internal/exceptions.py4
-rw-r--r--tests/functional/test_pep668.py16
6 files changed, 37 insertions, 3 deletions
diff --git a/news/11780.feature.rst b/news/11780.feature.rst
new file mode 100644
index 000000000..b765de6c5
--- /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 661c489c7..1f804097e 100644
--- a/src/pip/_internal/cli/cmdoptions.py
+++ b/src/pip/_internal/cli/cmdoptions.py
@@ -164,6 +164,14 @@ require_virtualenv: Callable[..., Option] = partial(
),
)
+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 cecaac2bc..b20aeddf8 100644
--- a/src/pip/_internal/commands/install.py
+++ b/src/pip/_internal/commands/install.py
@@ -215,6 +215,7 @@ class InstallCommand(RequirementCommand):
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 @@ class InstallCommand(RequirementCommand):
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 e5a4c8e10..f198fc313 100644
--- a/src/pip/_internal/commands/uninstall.py
+++ b/src/pip/_internal/commands/uninstall.py
@@ -58,6 +58,7 @@ class UninstallCommand(Command, SessionCommandMixin):
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 @@ class UninstallCommand(Command, SessionCommandMixin):
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 d28713ff7..d4527295d 100644
--- a/src/pip/_internal/exceptions.py
+++ b/src/pip/_internal/exceptions.py
@@ -696,7 +696,9 @@ class ExternallyManagedEnvironment(DiagnosticPipError):
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 1fed85e70..3c1085668 100644
--- a/tests/functional/test_pep668.py
+++ b/tests/functional/test_pep668.py
@@ -45,6 +45,22 @@ def test_fails(script: PipTestEnvironment, arguments: List[str]) -> None:
@pytest.mark.parametrize(
"arguments",
[
+ pytest.param(["install"], id="install"),
+ 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",
+ [
pytest.param(["install", "--root"], id="install-root"),
pytest.param(["install", "--prefix"], id="install-prefix"),
pytest.param(["install", "--target"], id="install-target"),