mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-06 09:28:34 +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
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from dataclasses import dataclass
|
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)
|
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:
|
try:
|
||||||
return subprocess.run(
|
return subprocess.run(
|
||||||
argv,
|
argv,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=_INSTALL_TIMEOUT_SECONDS,
|
timeout=_INSTALL_TIMEOUT_SECONDS,
|
||||||
|
env=env,
|
||||||
)
|
)
|
||||||
except subprocess.TimeoutExpired as exc:
|
except subprocess.TimeoutExpired as exc:
|
||||||
stdout = exc.stdout.decode(errors="replace") if isinstance(exc.stdout, bytes) else exc.stdout
|
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_cmd = pip_cmd
|
||||||
failed_proc = proc
|
failed_proc = proc
|
||||||
if missing_pip(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"]
|
ensure_cmd = [sys.executable, "-m", "ensurepip", "--upgrade"]
|
||||||
logger.info("pip missing while installing '{}'; running {}", extra, command_text(ensure_cmd))
|
logger.info("pip missing while installing '{}'; running {}", extra, command_text(ensure_cmd))
|
||||||
ensure_proc = runner(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
|
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):
|
def test_enable_bootstraps_pip_with_ensurepip(monkeypatch):
|
||||||
from nanobot import optional_features
|
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, 1, stdout="", stderr="No module named pip")
|
||||||
return subprocess.CompletedProcess(argv, 0, stdout="", stderr="")
|
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 optional_features.install_extra("bedrock", None, runner=_run).ok is True
|
||||||
assert calls == [
|
assert calls == [
|
||||||
[sys.executable, "-m", "pip", "install", "nanobot-ai[bedrock]"],
|
[sys.executable, "-m", "pip", "install", "nanobot-ai[bedrock]"],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user