uv run 完全解析:在 Python 项目中运行命令、注入临时依赖与 PEP 723 脚本隔离执行
uv 是 Astral 用 Rust 编写的 Python 包与项目管理器,其中 uv run 是在项目环境中执行任意命令的统一入口。它会在运行前自动把项目环境同步到最新,并支持逐次调用注入额外依赖、隔离执行 PEP 723 内联元数据脚本、兼容 Windows 上的遗留 setuptools 脚本,以及一套经过精细设计的 Unix/Windows 信号转发机制。读完本文,你将能够掌握 uv run 的完整用法与参数体系,并理解其背后的环境同步、临时依赖分层与子进程信号处理等源码级实现。
在项目中运行命令:为什么需要 uv run
在一个 uv 项目中工作时,项目会被安装到虚拟环境 .venv 中。这个环境默认与当前 shell 隔离——也就是说,直接在你的 shell 里执行 python -c "import example" 这样的命令会失败,因为 shell 里的 python 找不到项目及其依赖。正确做法是使用 uv run 在项目中执行命令:
$ uv run python -c "import example"
使用 run 时,uv 会先确保项目环境是最新的(即与 pyproject.toml 及 uv.lock 保持一致),然后再执行给定的命令。这就是“运行前先同步”的语义:
$ uv run python -m app
命令既可以来自项目环境,也可以位于项目环境之外。例如,假设项目声明了一个 example-cli 入口点:
$ # 项目提供了 example-cli 可执行入口
$ uv run example-cli foo
$ # 运行一个依赖项目已安装脚本的 bash 脚本
$ uv run bash scripts/foo.sh
第二种用法体现了 uv run 的通用性:它本质上是在“配置好的项目环境 + 增补后的 PATH”中启动任意子进程,而不限于 Python 解释器。从源码结构看,uv run 的完整实现在 run.rs,它负责发现项目/工作区、决定使用哪个环境、必要时同步依赖,最终把命令交给子进程等待函数 run_to_completion(定义在 child.rs)执行。
逐次调用注入额外依赖:--with 选项族
额外依赖,或依赖的不同版本,都可以按“每次调用”来请求,而不必修改 pyproject.toml。--with 选项用于为本次调用添加一个依赖。例如请求 httpx 的不同版本:
$ uv run --with httpx==0.26.0 python -c "import httpx; print(httpx.__version__)"
0.26.0
$ uv run --with httpx==0.25.0 python -c "import httpx; print(httpx.__version__)"
0.25.0
请求的版本会被严格遵守,无论项目的自身要求是什么——即使项目声明了 httpx==0.24.0,上面的输出仍然一样。这一行为的关键在于分层环境:在 RunArgs 中,--with 的帮助文本明确写道,在项目内使用时这些依赖会“layered on top of the project environment in a separate, ephemeral environment”(叠加在项目环境之上的独立临时环境),且这些依赖被允许与项目声明的要求相互冲突。也就是说,--with 注入的包永远不会破坏你的 .venv 或 uv.lock,它们只存活于本次调用的临时环境层中,调用结束后即被丢弃。
--with 家族还包括两个相关选项,从 CLI 定义中可以确认:
--with-editable:以可编辑模式安装临时依赖(lib.rs);--with-requirements:从文件批量注入依赖,支持requirements.txt、带内联元数据的.py文件以及pylock.toml三种格式;明确不允许使用pyproject.toml、setup.py或setup.cfg(lib.rs)。它与--with遵循相同的环境语义,即同样叠加在临时环境中。
配合这两个选项,典型用法如下:
$ # 用一次性文件批量注入测试工具链
$ uv run --with-requirements tools.txt pytest -q
$ # 临时以可编辑方式引入本地包
$ uv run --with-editable ../local-pkg python -c "import local_pkg"
运行脚本:PEP 723 内联元数据的隔离执行
声明了内联元数据(PEP 723 块)的脚本,会在与项目隔离的环境中自动执行。也就是说,当你直接运行一个带内联元数据的 .py 文件时,uv 不会把它当作项目的一部分,而是解析其 # /// script 块,构建一个只包含脚本声明依赖的独立环境。详见 scripts 指南。
例如,给定脚本 example.py:
# /// script
# dependencies = [
# "httpx",
# ]
# ///
import httpx
resp = httpx.get("https://peps.python.org/api/peps.json")
data = resp.json()
print([(k, v["title"]) for k, v in data.items()][:10])
此时调用 uv run example.py 会以隔离方式运行——环境中只有脚本声明的那几个依赖,项目本身的环境不参与。这带来两个直接好处:脚本可以在任何目录(包括没有 pyproject.toml 的位置)自包含地运行;脚本依赖的版本漂移也不会污染项目环境。
从源码结构看,脚本处理在 run.rs 中通过 Pep723Item / Pep723Script 等类型完成,相关解析能力来自 uv-scripts crate。此外还有两个相关的显式开关(定义于 lib.rs):
--script/-s:强制把给定路径按 PEP 723 脚本解析,无论其扩展名是什么——即使文件不是.py;--gui-script:同样按 PEP 723 脚本解析,但在 Windows 上使用pythonw.exe启动(无控制台窗口),仅限 Windows。
Windows 上的遗留脚本:.ps1 / .cmd / .bat 的自动发现
uv 支持遗留的 setuptools 脚本——这类脚本是 setuptools 在 .venv\Scripts 目录中额外安装的配套文件(用于在 Windows 控制台环境下运行包的可执行入口)。目前仅支持 .ps1、.cmd、.bat 三种扩展名。
例如,运行一个 Command Prompt 脚本(注意 -- 分隔符,其后是原样传递给子进程的参数):
$ uv run --with nuitka==2.6.7 -- nuitka.cmd --version
更实用的是,你甚至不需要写扩展名:uv 会自动按 .ps1 → .cmd → .bat 的顺序替你查找并执行对应的文件:
$ uv run --with nuitka==2.6.7 -- nuitka --version
这一行为在源码中有精确对应:WindowsRunnable 定义了四类可运行对象——Executable(.exe)、PowerShell(.ps1)、Command(.cmd)、Batch(.bat)。其 as_command 方法揭示了 uv 实际如何启动它们:.ps1 通过 powershell -NoLogo -File <path> 执行,.cmd/.bat 通过 cmd /q /c <path> 执行。当用户未提供扩展名时,from_script_path 会按上述顺序在环境脚本目录中逐一探测候选文件。
信号处理:为什么 Ctrl-C 的行为是“精心设计”的
uv 不会把进程控制权完全交给被启动的命令(这样可以在失败时提供更友好的错误信息),因此 uv 自己承担了把部分信号转发给子进程的责任。
Unix 上的信号转发策略
在 Unix 系统上,uv 会转发绝大多数信号,例外是 SIGKILL、SIGCHLD、SIGIO 和 SIGPOLL——后三者对父子进程关系没有实际意义,SIGKILL 则无法被捕获。真正微妙的是 SIGINT(Ctrl-C)的处理。终端驱动在按下 Ctrl-C 时向前台进程组发送 SIGINT,这意味着 uv 和子进程都会收到。如果 uv 再立即转发一次,子进程就会收到“两个 SIGINT”——而对某些程序,两次 SIGINT 有特定语义(第一次温和退出、第二次强制退出),这会破坏子进程自身的优雅退出逻辑。因此 uv 的策略是:
- 仅当 SIGINT 被发送超过一次,或者子进程所在进程组与 uv 不同时,才转发给子进程;
- 由于 uv 会延迟转发,终端可能仍在继续向进程组发送信号,所以转发前还会等待 200 毫秒,给子进程留出自行处理窗口期。
这些逻辑都能在 child.rs 的 run_to_completion 中逐条对上:它用 nix 读取父进程与子进程的 PGID,用 tokio::signal 注册 SIGINT、SIGTERM、SIGUSR1、SIGUSR2、SIGHUP、SIGALRM、SIGQUIT、SIGWINCH、SIGPIPE(以及部分平台上的 SIGINFO)处理器,并以“stdin 是否为 TTY”作为判断是否处于交互式终端的启发式——非交互式场景下(例如 kill -2 <pid> 直接发给 uv),SIGINT 会被无条件转发。
SIGTERM 的处理同样值得注意:注释中解释了 PID 1 的特殊性——当 uv 在 Docker 容器里被直接作为 PID 1 启动时,内核给 PID 1 的默认 SIGTERM 处理器不会终止进程,因此 uv 必须无条件转发 SIGTERM,否则容器内的 uv 将无法被 docker stop 终止。
Windows 上的策略
在 Windows 上,上述进程组概念不适用。uv 会忽略 Ctrl-C 事件,把处理权完全交给子进程,让它有机会干净地退出——这与 Unix 下“子进程自己先处理第一次 SIGINT”的目标是一致的。
关键选项速查
uv run 的完整参数面在 RunArgs 中定义。下表汇总了与本文主题最相关、且直接影响“以何种环境运行命令”的选项:
| 选项 | 作用 | 说明 |
|---|---|---|
--with / -w |
注入临时依赖 | 叠加在独立临时环境中,允许与项目要求冲突 |
--with-editable |
以可编辑模式注入临时依赖 | 环境语义与 --with 相同 |
--with-requirements |
从文件批量注入 | 支持 requirements.txt、.py(内联元数据)、pylock.toml |
--isolated |
强制使用全新环境 | 不复用项目环境以换取严格隔离;项目仍以可编辑方式安装 |
--no-project / --no-workspace |
跳过项目/工作区发现 | 在由 --with 填充的临时隔离环境中运行 |
--no-sync |
跳过同步虚拟环境 | 隐含 --frozen,不再更新 lockfile |
--frozen |
不更新 uv.lock |
直接以 lockfile 中的版本为准;lockfile 缺失时报错 |
--locked |
断言 lockfile 不变 | 要求 lockfile 已是最新,否则以错误退出 |
--exact |
精确同步 | 删除环境中多余(extraneous)的包;默认只做满足要求的最小变更 |
--module / -m |
按 Python 模块运行 | 等价于 python -m <module> |
--script / -s |
强制按 PEP 723 脚本解析 | 无论扩展名 |
--gui-script |
按 PEP 723 脚本解析并在 Windows 用 pythonw.exe 启动 |
仅 Windows |
--all-packages |
在包含全部工作区成员的环境中运行 | 更新后的 .venv 含所有工作区成员 |
--package |
在工作区内指定某个包 | 目标成员不存在时报错 |
--python / -p |
指定解释器 | 若满足请求的已被发现环境使用,则沿用该环境 |
--python-platform |
指定目标平台(target triple) | 面向交叉安装等进阶场景,安装结果可能与本机不兼容 |
其中 --isolated 与 --no-project 的组合尤其值得留意:前者“仍发现项目、但每次用全新环境”(项目仍以可编辑方式装入该环境),后者则“根本不发现项目/工作区”,适合在仓库目录里跑一个与项目完全无关的一次性命令:
$ # 完全脱离当前项目,只用临时依赖跑命令
$ uv run --no-project --with requests python -c "import requests; print(requests.__version__)"
小结
uv run 的设计核心是三层能力的叠加:
- 环境即依赖:运行任何命令前自动完成项目环境同步(除非显式
--no-sync/--frozen),使.venv永远与声明一致; - 临时依赖分层:
--with系列选项把一次性依赖隔离在独立临时环境层,允许与项目要求冲突而不污染uv.lock; - 脚本与平台兼容:PEP 723 内联元数据脚本自动隔离执行,Windows 遗留
.ps1/.cmd/.bat脚本按固定顺序自动发现,Unix 信号转发策略在“错误可见性”与“子进程优雅退出”之间取得了平衡。
对于想深入实现细节的读者,建议沿着 run.rs(命令编排与环境选择)、child.rs(子进程等待与信号转发)以及 runnable.rs(Windows 可运行文件解析)三条路径继续阅读。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00