fix(update): handle pipx installs + --system fallback in _cmd_update_pip
Extends the uv-tool detection (briandevans, #29703) to cover the remaining no-venv install layouts that hit the same uv 'No virtual environment found' error: - pipx-managed installs (sys.prefix under .../pipx/...) -> 'pipx upgrade', matching scripts/auto-update.sh (pipx-detection idea from inchargeautomation-lab, #29852) - bare pip outside any venv -> 'uv pip install --system --upgrade' - venv (launcher shim) keeps the VIRTUAL_ENV overlay from #35224 and never gets --system, so the install always targets the venv, not system Python The four branches are mutually exclusive; VIRTUAL_ENV is exported only for the uv-pip-in-venv path (uv tool / pipx upgrade ignore it). Co-authored-by: Joshua Kimbrell <incharge.automation@gmail.com>
This commit is contained in:
parent
bebd4f8516
commit
2334228eca
2 changed files with 119 additions and 1 deletions
|
|
@ -8977,19 +8977,43 @@ def _cmd_update_pip(args):
|
|||
print("→ Checking PyPI for updates...")
|
||||
|
||||
uv = shutil.which("uv")
|
||||
in_venv = sys.prefix != sys.base_prefix
|
||||
# pipx-managed installs live under .../pipx/venvs/<name>/...
|
||||
pipx_managed = "pipx" in sys.prefix.split(os.sep)
|
||||
pipx = shutil.which("pipx") if pipx_managed else None
|
||||
|
||||
# Only the ``uv pip install`` path inside a venv needs VIRTUAL_ENV
|
||||
# exported (uv refuses to install without it when the launcher shim
|
||||
# didn't activate the venv). ``uv tool upgrade`` / ``pipx upgrade``
|
||||
# operate on a named environment and ignore VIRTUAL_ENV, so we don't
|
||||
# set it for them.
|
||||
export_virtualenv = False
|
||||
|
||||
if is_uv_tool_install():
|
||||
if not uv:
|
||||
print("✗ Detected a uv-tool install but `uv` is not on PATH; install uv and retry.")
|
||||
sys.exit(1)
|
||||
cmd = [uv, "tool", "upgrade", "hermes-agent"]
|
||||
elif pipx_managed and pipx:
|
||||
# pipx owns its own venv; ``pipx upgrade`` is the only correct path.
|
||||
# Matches scripts/auto-update.sh, which already uses pipx upgrade.
|
||||
cmd = [pipx, "upgrade", "hermes-agent"]
|
||||
elif uv:
|
||||
cmd = [uv, "pip", "install", "--upgrade", "hermes-agent"]
|
||||
if in_venv:
|
||||
# Launcher shim runs the venv interpreter but doesn't export
|
||||
# VIRTUAL_ENV; without it uv errors "No virtual environment found".
|
||||
export_virtualenv = True
|
||||
else:
|
||||
# Outside any venv, ``--system`` lets uv target the active
|
||||
# interpreter, matching pip's default behaviour.
|
||||
cmd.insert(3, "--system")
|
||||
else:
|
||||
cmd = [sys.executable, "-m", "pip", "install", "--upgrade", "hermes-agent"]
|
||||
|
||||
print(f"→ Running: {' '.join(cmd)}")
|
||||
run_kwargs = {}
|
||||
if sys.prefix != sys.base_prefix:
|
||||
if export_virtualenv:
|
||||
run_kwargs["env"] = {**os.environ, "VIRTUAL_ENV": sys.prefix}
|
||||
result = subprocess.run(cmd, **run_kwargs)
|
||||
if result.returncode != 0:
|
||||
|
|
|
|||
|
|
@ -215,3 +215,97 @@ class TestCmdUpdatePipUsesUvTool:
|
|||
_cmd_update_pip(SimpleNamespace())
|
||||
assert exc_info.value.code == 1
|
||||
mock_run.assert_not_called()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# pipx-managed installs, --system fallback, and VIRTUAL_ENV overlay
|
||||
# (issue #29700 / #35031 family — consolidated update-path handling)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCmdUpdatePipInstallLayouts:
|
||||
"""The uv pip path must adapt to where the running interpreter lives:
|
||||
|
||||
- inside a venv (launcher shim) -> export VIRTUAL_ENV, no ``--system``
|
||||
- bare pip outside any venv -> add ``--system``, no overlay
|
||||
- pipx-managed -> ``pipx upgrade``
|
||||
"""
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_pipx_managed_uses_pipx_upgrade(self, mock_run, monkeypatch):
|
||||
from hermes_cli import main as hm
|
||||
|
||||
mock_run.return_value = subprocess.CompletedProcess([], 0, stdout="", stderr="")
|
||||
monkeypatch.setattr(hm.sys, "prefix", "/home/u/.local/pipx/venvs/hermes-agent")
|
||||
monkeypatch.setattr(hm.sys, "base_prefix", "/usr")
|
||||
|
||||
def _which(name):
|
||||
return {"uv": "/usr/bin/uv", "pipx": "/usr/bin/pipx"}.get(name)
|
||||
|
||||
with patch("shutil.which", side_effect=_which), \
|
||||
patch("hermes_cli.config.is_uv_tool_install", return_value=False):
|
||||
hm._cmd_update_pip(SimpleNamespace())
|
||||
|
||||
assert mock_run.call_args[0][0] == ["/usr/bin/pipx", "upgrade", "hermes-agent"]
|
||||
# pipx upgrade ignores VIRTUAL_ENV; we must not set it.
|
||||
assert "env" not in mock_run.call_args.kwargs
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_pipx_layout_without_pipx_binary_treated_as_venv(
|
||||
self, mock_run, monkeypatch
|
||||
):
|
||||
from hermes_cli import main as hm
|
||||
|
||||
mock_run.return_value = subprocess.CompletedProcess([], 0, stdout="", stderr="")
|
||||
monkeypatch.setattr(hm.sys, "prefix", "/home/u/.local/pipx/venvs/hermes-agent")
|
||||
monkeypatch.setattr(hm.sys, "base_prefix", "/usr")
|
||||
|
||||
# pipx layout detected via prefix, but pipx binary missing on PATH.
|
||||
def _which(name):
|
||||
return "/usr/bin/uv" if name == "uv" else None
|
||||
|
||||
with patch("shutil.which", side_effect=_which), \
|
||||
patch("hermes_cli.config.is_uv_tool_install", return_value=False):
|
||||
hm._cmd_update_pip(SimpleNamespace())
|
||||
|
||||
# prefix != base_prefix, so this is treated as a venv -> overlay, no --system.
|
||||
assert mock_run.call_args[0][0] == [
|
||||
"/usr/bin/uv", "pip", "install", "--upgrade", "hermes-agent",
|
||||
]
|
||||
assert mock_run.call_args.kwargs["env"]["VIRTUAL_ENV"].endswith("hermes-agent")
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_bare_pip_outside_venv_adds_system(self, mock_run, monkeypatch):
|
||||
from hermes_cli import main as hm
|
||||
|
||||
mock_run.return_value = subprocess.CompletedProcess([], 0, stdout="", stderr="")
|
||||
# No venv: prefix == base_prefix.
|
||||
monkeypatch.setattr(hm.sys, "prefix", "/usr")
|
||||
monkeypatch.setattr(hm.sys, "base_prefix", "/usr")
|
||||
|
||||
with patch("shutil.which", return_value="/usr/bin/uv"), \
|
||||
patch("hermes_cli.config.is_uv_tool_install", return_value=False):
|
||||
hm._cmd_update_pip(SimpleNamespace())
|
||||
|
||||
assert mock_run.call_args[0][0] == [
|
||||
"/usr/bin/uv", "pip", "install", "--system", "--upgrade", "hermes-agent",
|
||||
]
|
||||
assert "env" not in mock_run.call_args.kwargs
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_venv_exports_virtualenv_and_omits_system(self, mock_run, monkeypatch):
|
||||
from hermes_cli import main as hm
|
||||
|
||||
mock_run.return_value = subprocess.CompletedProcess([], 0, stdout="", stderr="")
|
||||
monkeypatch.delenv("VIRTUAL_ENV", raising=False)
|
||||
monkeypatch.setattr(hm.sys, "prefix", "/home/u/.hermes/hermes-agent/venv")
|
||||
monkeypatch.setattr(hm.sys, "base_prefix", "/usr")
|
||||
|
||||
with patch("shutil.which", return_value="/usr/bin/uv"), \
|
||||
patch("hermes_cli.config.is_uv_tool_install", return_value=False):
|
||||
hm._cmd_update_pip(SimpleNamespace())
|
||||
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert "--system" not in cmd
|
||||
assert cmd == ["/usr/bin/uv", "pip", "install", "--upgrade", "hermes-agent"]
|
||||
assert mock_run.call_args.kwargs["env"]["VIRTUAL_ENV"] == "/home/u/.hermes/hermes-agent/venv"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue