Skip to content

Commit 92a80f1

Browse files
committed
fix: explain externally managed Python
1 parent 023a930 commit 92a80f1

4 files changed

Lines changed: 50 additions & 0 deletions

File tree

cwmscli/commands/commands_cwms.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
build_update_package_spec,
2828
get_update_environment,
2929
launch_windows_update,
30+
looks_like_externally_managed_environment,
3031
looks_like_missing_version,
3132
)
3233
from cwmscli.utils.version import get_cwms_cli_version
@@ -496,6 +497,16 @@ def update_cli_cmd(target_version: Optional[str], pre: bool, yes: bool) -> None:
496497

497498
if result.returncode != 0:
498499
pip_output = "\n".join(part for part in [result.stdout, result.stderr] if part)
500+
if looks_like_externally_managed_environment(pip_output):
501+
raise click.ClickException(
502+
colors.err(
503+
"The selected Python installation is externally managed, so pip "
504+
"refused to update cwms-cli. Install cwms-cli in a virtual "
505+
"environment or with pipx, then run that installation's "
506+
"cwms-cli update command. cwms-cli will not use "
507+
"--break-system-packages automatically."
508+
)
509+
)
499510
if target_version and looks_like_missing_version(pip_output, package_spec):
500511
raise click.ClickException(
501512
colors.err(

cwmscli/utils/update.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ def looks_like_missing_version(pip_output: str, package_spec: str) -> bool:
106106
) and package_spec in pip_output
107107

108108

109+
def looks_like_externally_managed_environment(pip_output: str) -> bool:
110+
return "externally-managed-environment" in pip_output.lower()
111+
112+
109113
def write_windows_update_script(cmd: List[str]) -> str:
110114
quoted_cmd = subprocess.list2cmdline(cmd)
111115
script = "\r\n".join(

docs/cli/update.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,22 @@ the running ``cwms-cli.exe`` does not block its own replacement.
2727
manually, use the full Python executable displayed by ``cwms-cli update``
2828
with ``-m pip install --upgrade cwms-cli``.
2929

30+
Linux externally managed environments
31+
-------------------------------------
32+
33+
Some Linux distributions mark their system Python installation as externally
34+
managed under PEP 668. If pip reports ``externally-managed-environment``,
35+
``cwms-cli update`` explains that the selected Python installation cannot be
36+
changed safely and recommends installing ``cwms-cli`` in a virtual environment
37+
or with pipx. The updater does not automatically pass
38+
``--break-system-packages``, because doing so can conflict with packages managed
39+
by the operating system.
40+
41+
A correctly created virtual environment is not subject to the system Python's
42+
externally managed restriction. Confirm that the displayed Python executable
43+
and environment prefix both point into the intended virtual environment before
44+
continuing.
45+
3046
Editable installations
3147
----------------------
3248

tests/cli/test_update_command.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,25 @@ def fake_run(cmd, check=False, capture_output=False, text=False):
147147
assert "Requested cwms-cli version '9.9.9' was not found." in result.output
148148

149149

150+
def test_update_command_explains_externally_managed_environment(monkeypatch):
151+
def fake_run(cmd, check=False, capture_output=False, text=False):
152+
return _DummyResult(
153+
1,
154+
stderr="error: externally-managed-environment\n",
155+
)
156+
157+
_set_update_os(monkeypatch, "posix")
158+
monkeypatch.setattr("cwmscli.commands.commands_cwms.subprocess.run", fake_run)
159+
160+
runner = CliRunner()
161+
result = runner.invoke(cli, ["update", "--yes"])
162+
163+
assert result.exit_code == 1
164+
assert "selected Python installation is externally managed" in result.output
165+
assert "virtual environment or with pipx" in result.output
166+
assert "will not use --break-system-packages automatically" in result.output
167+
168+
150169
def test_update_command_cancelled_by_user(monkeypatch):
151170
calls = []
152171

0 commit comments

Comments
 (0)