fix(uv-pip-compat): bind venv python for more pip commands and add tests for compatibility wrapper

This commit is contained in:
jxxghp
2026-05-15 09:58:30 +08:00
parent f303d9e576
commit 5a06e7b8bc
2 changed files with 98 additions and 9 deletions

View File

@@ -27,7 +27,8 @@ fi
has_environment_option() {
while [ "$#" -gt 0 ]; do
case "$1" in
-p|--python|--python=*|-p*|--system)
-p|--python|--python=*|-p*|--system|--user|\
-t|--target|--target=*|-t*|--prefix|--prefix=*)
return 0
;;
--)
@@ -39,6 +40,18 @@ has_environment_option() {
return 1
}
uv_pip_with_venv_python() {
command_name="$1"
shift
if [ -x "${SCRIPT_DIR}/python" ] && ! has_environment_option "$@"; then
# uv 不会仅凭 pip 软链接位置锁定 venv本地安装也不会激活 venv。
# 因此需要在会读取或改写环境的 pip 子命令上显式绑定当前 venv 解释器。
exec "${UV_BIN}" pip "${command_name}" --python "${SCRIPT_DIR}/python" "$@"
fi
exec "${UV_BIN}" pip "${command_name}" "$@"
}
case "${COMMAND_NAME}" in
pip|pip3|pip3.*)
if [ "$#" -eq 0 ]; then
@@ -53,13 +66,10 @@ case "${COMMAND_NAME}" in
shift
exec "${UV_BIN}" help pip "$@"
;;
check)
if [ -x "${SCRIPT_DIR}/python" ] && ! has_environment_option "$@"; then
# uv 不会仅凭 pip 软链接位置锁定 venv显式绑定当前运行态解释器。
shift
exec "${UV_BIN}" pip check --python "${SCRIPT_DIR}/python" "$@"
fi
exec "${UV_BIN}" pip "$@"
check|freeze|install|list|show|sync|tree|uninstall)
pip_command="$1"
shift
uv_pip_with_venv_python "${pip_command}" "$@"
;;
*)
exec "${UV_BIN}" pip "$@"
@@ -70,7 +80,7 @@ case "${COMMAND_NAME}" in
exec "${UV_BIN}" pip compile "$@"
;;
pip-sync)
exec "${UV_BIN}" pip sync "$@"
uv_pip_with_venv_python sync "$@"
;;
*)
echo "不支持的 pip 兼容命令入口:${COMMAND_NAME}" >&2

View File

@@ -0,0 +1,79 @@
from __future__ import annotations
import os
import shutil
import subprocess
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
WRAPPER = ROOT / "scripts" / "uv-pip-compat.sh"
class UvPipCompatTests(unittest.TestCase):
def run_wrapper(self, link_name: str, *args: str) -> list[str]:
with tempfile.TemporaryDirectory() as temp_dir:
venv_bin = Path(temp_dir) / "venv" / "bin"
venv_bin.mkdir(parents=True)
(venv_bin / "python").write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
(venv_bin / "python").chmod(0o755)
argv_file = Path(temp_dir) / "argv.txt"
uv_bin = venv_bin / "uv"
uv_bin.write_text(
"#!/bin/sh\n"
# 测试只关心兼容层传给 uv 的参数,逐行记录可以避免 shell 转义差异干扰断言。
f"for arg in \"$@\"; do printf '%s\\n' \"$arg\" >> '{argv_file}'; done\n",
encoding="utf-8",
)
uv_bin.chmod(0o755)
wrapper_path = venv_bin / "uv-pip-compat"
shutil.copy2(WRAPPER, wrapper_path)
wrapper_path.chmod(0o755)
link_path = venv_bin / link_name
link_path.symlink_to(wrapper_path.name)
subprocess.run(
[str(link_path), *args],
check=True,
env={
**os.environ,
"PATH": f"{venv_bin}{os.pathsep}{os.environ.get('PATH', '')}",
},
)
return argv_file.read_text(encoding="utf-8").splitlines()
def test_pip_install_binds_venv_python(self):
argv = self.run_wrapper("pip", "install", "-r", "requirements.txt")
self.assertEqual(
[
"pip",
"install",
"--python",
argv[3],
"-r",
"requirements.txt",
],
argv,
)
self.assertTrue(argv[3].endswith("/venv/bin/python"))
def test_pip_install_keeps_explicit_environment(self):
argv = self.run_wrapper("pip", "install", "--system", "demo-package")
self.assertEqual(["pip", "install", "--system", "demo-package"], argv)
def test_pip_sync_binds_venv_python(self):
argv = self.run_wrapper("pip-sync", "requirements.txt")
self.assertEqual(["pip", "sync", "--python", argv[3], "requirements.txt"], argv)
self.assertTrue(argv[3].endswith("/venv/bin/python"))
if __name__ == "__main__":
unittest.main()