mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-04 08:28:36 +00:00
fix(plugins): use uv when pip is unavailable
This commit is contained in:
parent
5c72fdcd88
commit
52bc79d3a0
@ -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)
|
||||
|
||||
@ -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]"],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user