fix(plugins): use uv when pip is unavailable

This commit is contained in:
KDB 2026-08-02 19:24:59 +08:00 committed by Xubin Ren
parent 5c72fdcd88
commit 52bc79d3a0
2 changed files with 87 additions and 1 deletions

View File

@ -2,6 +2,8 @@
from __future__ import annotations
import json
import os
import shutil
import subprocess
import sys
from dataclasses import dataclass
@ -179,13 +181,18 @@ def extra_installed(extra: str, deps: list[str] | None) -> bool:
return all(requirement_installed(dep, extra) for dep in deps)
def run_install_command(argv: list[str]) -> subprocess.CompletedProcess[str]:
def run_install_command(
argv: list[str],
*,
env: dict[str, str] | None = None,
) -> subprocess.CompletedProcess[str]:
try:
return subprocess.run(
argv,
capture_output=True,
text=True,
timeout=_INSTALL_TIMEOUT_SECONDS,
env=env,
)
except subprocess.TimeoutExpired as exc:
stdout = exc.stdout.decode(errors="replace") if isinstance(exc.stdout, bytes) else exc.stdout
@ -234,6 +241,20 @@ def install_extra(
failed_cmd = pip_cmd
failed_proc = proc
if missing_pip(proc):
if shutil.which("uv"):
uv_cmd = ["uv", "pip", "install", "--python", sys.executable, *install_args]
uv_env = os.environ.copy()
if index_url := os.environ.get("PIP_INDEX_URL", "").strip():
uv_env["UV_INDEX_URL"] = index_url
logger.info("pip missing while installing '{}'; running {}", extra, command_text(uv_cmd))
uv_proc = runner(uv_cmd, env=uv_env)
_log_completed_command(f"Optional feature '{extra}' uv install", uv_proc)
if uv_proc.returncode == 0:
importlib.invalidate_caches()
return InstallResult(True, label, pip_cmd)
output = (uv_proc.stderr or uv_proc.stdout or "").strip()
return InstallResult(False, label, pip_cmd, failed_cmd=uv_cmd, output=output)
ensure_cmd = [sys.executable, "-m", "ensurepip", "--upgrade"]
logger.info("pip missing while installing '{}'; running {}", extra, command_text(ensure_cmd))
ensure_proc = runner(ensure_cmd)

View File

@ -2479,6 +2479,69 @@ def test_optional_features_payload_preserves_legacy_flat_feishu_config(monkeypat
assert "instances" not in saved
@pytest.mark.parametrize(
"index_url",
[
"",
"https://mirror.example/simple",
],
)
def test_enable_uses_uv_when_tool_environment_has_no_pip(
monkeypatch,
index_url,
):
from nanobot import optional_features
calls: list[list[str]] = []
call_envs: list[dict[str, str] | None] = []
def _run(
argv: list[str],
*,
env: dict[str, str] | None = None,
) -> subprocess.CompletedProcess[str]:
calls.append(argv)
call_envs.append(env)
if len(calls) == 1:
return subprocess.CompletedProcess(argv, 1, stdout="", stderr="No module named pip")
if argv[0] == "uv":
return subprocess.CompletedProcess(argv, 0, stdout="", stderr="")
return subprocess.CompletedProcess(
argv,
1,
stdout="",
stderr="No module named ensurepip",
)
monkeypatch.setattr("shutil.which", lambda name: "uv" if name == "uv" else None)
monkeypatch.setenv("HTTPS_PROXY", "http://proxy.example:8080")
monkeypatch.delenv("UV_INDEX_URL", raising=False)
if index_url:
monkeypatch.setenv("PIP_INDEX_URL", index_url)
else:
monkeypatch.delenv("PIP_INDEX_URL", raising=False)
assert optional_features.install_extra("feishu", ["lark-oapi>=1.5.0"], runner=_run).ok is True
assert calls == [
[sys.executable, "-m", "pip", "install", "lark-oapi>=1.5.0"],
[
"uv",
"pip",
"install",
"--python",
sys.executable,
"lark-oapi>=1.5.0",
],
]
assert call_envs[0] is None
assert call_envs[1] is not None
assert call_envs[1]["HTTPS_PROXY"] == "http://proxy.example:8080"
if index_url:
assert call_envs[1]["UV_INDEX_URL"] == index_url
else:
assert "UV_INDEX_URL" not in call_envs[1]
def test_enable_bootstraps_pip_with_ensurepip(monkeypatch):
from nanobot import optional_features
@ -2490,6 +2553,8 @@ def test_enable_bootstraps_pip_with_ensurepip(monkeypatch):
return subprocess.CompletedProcess(argv, 1, stdout="", stderr="No module named pip")
return subprocess.CompletedProcess(argv, 0, stdout="", stderr="")
monkeypatch.setattr("shutil.which", lambda _name: None)
assert optional_features.install_extra("bedrock", None, runner=_run).ok is True
assert calls == [
[sys.executable, "-m", "pip", "install", "nanobot-ai[bedrock]"],