文章目录
- 安装与更新
-
- standalone
- [pip 安装](#pip 安装)
- 创建以及初始化项目
- 依赖管理
- [uv run](#uv run)
- uv项目本地运行调试细节
-
- [vscode 中运行调试uv项目](#vscode 中运行调试uv项目)
- 命令行运行
- [深入理解 uv lock, uv sync, uv lock](#深入理解 uv lock, uv sync, uv lock)
-
- [uv lock 行为解析:](#uv lock 行为解析:)
- [uv sync 行为解析](#uv sync 行为解析)
- [uv run 行为解析](#uv run 行为解析)
- [uv项目的docker file](#uv项目的docker file)
uv是一个更现代的python项目管理器.笔者在使用uv前,笔者都是使用anaconda指令(比如 conda create -n xxx python=3.11 -y
) 为每个项目创建隔离的虚拟环境,然后使用 conda activate xxx
激活此环境,最后使用 pip install -r requirements.txt
这种方式来安装或者更新项目依赖.而有了uv之后可以让python项目管理变得更加的规范.本文着重讲解笔者最近切换到使用uv进行python项目管理过程中uv的一些常规用法和技巧,后续有用到新的feature会同步更新本文.
安装与更新
这里只讲两种笔者认为最多涉及到的安装与更新方式,分别是命令行standard安装更新和pip安装更新
standalone
本地开发机器上安装以及更新的方式,笔者是ubuntu系统,所以安装使用如下指令
bash
curl -LsSf https://astral.sh/uv/install.sh | sh
参考官网uv安装
更新的指令即是:
bash
UV_NO_MODIFY_PATH=1 uv self update
pip 安装
pip 安装适合在构建docker镜像
的时候选择的安装方式,因为一般构建docker镜像选择的基础镜像都是一个最小化的python basic image
.它可能只包含基本的python环境和pip,所以在Dockerfile
中使用pip安装uv,安装指令如下:
bash
pip install uv
既然是pip安装的包,更新指令就是
bash
pip install --upgrade uv
不过这个更新指令基本使用不到,构建镜像过程中只需要在基础镜像安装uv,不需要更新uv.
创建以及初始化项目
关键指令为
bash
uv init [OPTIONS] [PATH]
比如在python-uv
目录下执行uv init --python=3.11 test
命令会创建一个名字为test的子目录
且子目录作为项目根目录,命令指定了项目运行所需的python版本为3.11
,同时在test
目录下创建项目配置文件pyproject.toml
,配置文件中设置项目名称为test
,和项目根目录同名,同时还有一些git repo必需的文件以及文件夹,可见init指令也将项目初始化成一个git repo.先看一下目录结构:
bash
test
├── .git
├── .gitignore
├── main.py
├── pyproject.toml
├── .python-version
└── README.md
如果init指令不指定参数PATH
则是将运行uv init所在目录
作为项目根目录去创建上面这些内容.此时在项目内运行uv sync
才能看到虚拟环境目录.venv
和uv.lock
被创建.所以完整的项目初始化之后结构如下:
bash
└── test
├── .git
├── .gitignore
├── main.py
├── pyproject.toml
├── .python-version
├── README.md
├── uv.lock
└── .venv
有几个重要文件和文件夹需要提前说明:
- .venv: 项目python虚拟环境和依赖包相关文件夹. 可以在项目目录上下文执行指令
source .venv/bin/activate
激活虚拟环境.但是由于使用uv来构建以及管理的项目使用uv指令居多,所以一般情况下不需要显式的激活这个虚拟环境.同时.venv
也不应该上传到版本控制系统,每一次项目repo同步下来只需要运行uv sync
则可以更新本地的虚拟环境中lib更新到最新,继续开发项目或者本地调试. - pyproject.toml: 存放整个项目的元数据,视作项目配置文件,其内部包含项目名称,描述,依赖,构建,脚本以及工具等等,一般执行uv相关命令都会涉及到更改此配置文件,官方关于此配置文件编写教程链接pyproject.如果从远端同步下来uv项目,可以执行
uv sync
即是根据此配置文件去构建本地的开发虚拟环境和依赖包. - uv.lock 文件:项目依赖的精确版本信息,和
pyproject.toml
配置不同,配置文件一般是描述依赖的版本边界约束
,比如某个依赖包最低版本或者最高版本.lock文件中是真正虚拟环境内安装的精确版本,任何涉及到更新依赖的命令比如uv sync
,uv add {package}
等操作在依赖的版本边界约束
内可能拉取最新的版本,就会更新此lock文件.此文件真正起作用的地方是在CI构建过程,因为虚拟环境文件不会提交到版本管理系统,每次构建都需要拉取这些依赖,涉及到使用指令uv sync
.由于前面所说这个指令可能会拉取新的依赖版本可能引入不兼容问题,因此使用指令uv sync --frozen
强制使用uv.lock文件中的精确版本来完成构建过程,保证构建前后的版本一致,降低构建过程引入新版本带来的风险.uv.lock文件要求必须上传到版本控制系统且不可以手动更改.
这里可以看到init
甚至可以将项目初始化成一个git repo,实际可能不需要这个功能,多半情况下本地已经有git repo了只需要创建以及初始化uv 项目即可,这个情况下,可以稍微调整一下init指令相关的optional参数关闭git repo相关feature即可.另外--name
也可以显示指定项目的名称,这样就不是默认的以项目文件夹名字作为项目名称了,删除当前的test文件夹,使用如下命令创建新的非git repo的uv项目:
bash
uv init test --description="a test uv project" --vcs=none --no-readme --python=3.11 --managed-python
上面指令还会生成一个main.py
的入口文件,然后在test
目录下执行如下命令可以自动创建虚拟环境
bash
uv run main.py
输出:
bash
Using CPython 3.11.13
Creating virtual environment at: .venv
Hello from test!
可以看到虚拟环境文件夹.venv
和uv.lock
文件都已经创建好了,至此项目初始化结束.
这里简要说明一下刚创建好的项目是没有虚拟环境文件夹.venv
的,除了使用uv run main.py
之外还可以使用uv sync
命令.本质上uv会查看本地是否有项目指定运行的python版本,比如本项目初始化的时候指定的是3.11版本的python,没有的话会先下载此版本python到~/.local/share/uv/python
目录,然后再将此版本python"拷贝"到当前项目.venv
文件夹中.可以在项目文件夹为上下文的命令行窗口中执行指令source .venv/bin/activate
来激活此(使用deactivate
则退出)虚拟环境.只是使用uv指令的时候是完全不需要显式激活虚拟环境这个操作.
依赖管理
uv 添加依赖有uv add
命令,它会将依赖包安装到当前虚拟环境.venv
中同时更新pyproject.toml
中的dependencies配置. 比如需要把最新的fastapi
添加到项目中可以使用uv add "fastapi[standard]==0.116"
添加一个固定版本的fastapi
依赖包.当依赖安装好后查看pyproject.toml
可以看到依赖已被添加
bash
dependencies = [
"fastapi[standard]==0.116.0",
]
实际开发过程中不会这么严格限制一个版本,需要支持能够获取最新的bugfix修订版本,所以一般依赖都会给定一个版本范围比如fastapi
版本是0.116到0.117版本的最新修订版本
,那么添加依赖指令变为uv add "fastapi[standard]>=0.116,<0.117"
,此命令会覆盖pyproject.toml
中的版本约束,同时升级虚拟环境中安装的fastapi到0.116.x最新版本修. 当然也可以直接修改pyproject.toml
里面的fastapi依赖,如下
bash
dependencies = [
"fastapi[standard]>=0.116,<0.117",
]
然后再运行uv sync
也能达到同样效果.但是最佳做法还是使用uv add
增加或者修改现有的依赖.
删除依赖则是uv remove
指令.
默认情况下如果安装依赖不指定版本约束,当前会安装最新版本,且dependencies会写入 dep >= latest version
,比如执行如下指令
bash
uv add httpx
查看dependencies
bash
dependencies = [
"httpx>=0.28.1",
]
此时安装的是最新版本,当让可以修改依赖的版本约束uv add "httpx>=0.28,<0.29"
限制依赖版本为0.28.x
的最新修订版本.
uv中的依赖主要是三类,第一类是project.dependencies
项目依赖,默认情况下使用指令uv add xxx
的依赖都属于项目依赖,这些依赖在配置文件里面回添加到[project]
配置段下的dependencies
里面.这些依赖说白了都是代码中引入的包,代码运行时必不可少的包.
第二种是dependency groups.就是开发所需依赖,不会被大包到项目中去,所以这类依赖也不会出现在[project]
配置段中,只会出现在[dependency-groups]
配置段中.
有两种使用dependency groups的方法,第一种是uv add --dev xxx
把依赖放入dev这个group,可以理解为dev是内置的dependency groups.比如将pytest
这个包放入dev
group用于项目测试.
bash
uv add pytest --dev
配置文件
bash
[dependency-groups]
dev = [
"pytest>=8.4.1",
]
当然要删除在dev group中的这个包也需要加上--dev flag uv remove pytest --dev
. 注意dev
这个group在uv sync
的时候也会拉取相应的依赖包.
除了dev
这个官方定义的组之外,还可以自定义组,使用指令uv add --group {group_name} {package}
实现,比如:
bash
uv add --group lint ruff
会创建一个lint
的自定义组,且添加ruff依赖到此组.
bash
[dependency-groups]
dev = [
"pytest>=8.4.1",
]
lint = [
"ruff>=0.12.8",
]
删除的话也需要添加flaguv remove ruff --group lint
自定义组在使用uv sync
时是不会拉取依赖的,需要配置[tool.uv]
下面的default-groups
追加自定义的group
bash
[tool.uv]
default-groups = ["dev", "lint"]
uv run
uv run 指令是非常强大的指令. 下面全面说明它的用法:
直接在命令行运行python代码片段
它可以在命令行运行一段python代码片段,比如:
bash
uv run python -c "import sys;print(sys.executable)"
输出当前解释器的位置~/.local/share/uv/python/cpython-3.11.13-linux-x86_64-gnu/bin/python3.11
直接运行项目中可执行脚本文件
它还可以直接运行项目中的可执行脚本文件,比如项目中存在如下python脚本文件
python
#!/usr/bin/env python
#-*- coding:utf-8 -*-
import httpx
if __name__ == '__main__':
print(httpx.__version__)
pass
输出结果:
bash
0.28.1
这里用到了项目依赖httpx
,但是可以在不需要显式激活当前虚拟环境
的情况下直接打印当前虚拟环境中的依赖包信息.
除了python脚本外,uv run还可以直接运行shell脚本,比如有如下foo.sh的shell脚本:
shell
python cli.py
可以使用uv run bash foo.sh
也能正常运行脚本获取打印的依赖包版本信息.
再看一个细节,比如下面没有shebang行的python脚本sample.py
python
if __name__ == '__main__':
print('hello')
如果直接命令行运行./sample.py
肯定报错,大家都知道需要指定python解释器运行,比如python sample.py
使用默认的解释器运行就不会报错.使用uv 运行此脚本则是命令uv run sample.py
也能正确输出结果.uv运行python脚本的整个过程可以等价于如下操作:
bash
uv sync
source .venv/bin/activate
python sample.py
当我们知道当前要运行的脚本是python时候,甚至可以直接用指令uv run -- python sample.py
运行python脚本.
运行python包中快捷指令
我们知道有些包安装后是有在命令行运行的快捷指令的,比如环境中安装了fastapi[standard]
,那么在命令行里面就可以使用指令fastapi dev xxx.py
以develop模式快速启动fastapi web app. 那么在uv管理的项目里面依然可以运行这种命令行指令.比如先在uv项目里面添加fastapi[standard]
包
bash
uv add "fastapi[standard]"
编写一个fastapi web app 的 main.py文件:
python
#-*- coding:utf-8 -*-
from fastapi import FastAPI
app = FastAPI(title='test')
@app.get("/")
async def index():
return 'hello'
uv 命令 develop 模式启动此app:
bash
uv run -- fastapi dev main.py --port 8090
这里 uv run --
中的--
保证此字符后面都是fastapi的命令行指令与其运行参数,而不会被解析成uv run
参数,非常关键。
curl测试以及输出结果:
bash
curl http://localhost:8090
"hello"
因此基本上使用uv在命令行运行这些包中的快捷指令
基本就是 uv run -- {package cli cmd} {cli params}
这样的形式.
uv项目本地运行调试细节
上一部分完全探讨了uv run这个指令,这一部分详细探讨uv项目本地运行以及调试的细节.
这里涉及到两种运行:1. vscode 中运行调试.2. 本地命令行中调试运行细节
vscode 中运行调试uv项目
和之前笔者用conda创建虚拟环境,在vscode中调试项目那一套基本操作类似.这里首先需要创建项目vscode debug所需的launch.json文件,文件最基本内容如下,可以根据需求进行更改.
json
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "uv test single file",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"env": {
"OPENAI_BASE_URL": "https://api.deepseek.com",
"OPENAI_API_KEY": "xxxx"
}
},
{
"name": "uv test fastapi dev",
"type": "debugpy",
"request": "launch",
"module": "fastapi",
"console": "integratedTerminal",
"args": [
"dev", "main.py", "--port", "8090"
],
"env": {
"OPENAI_BASE_URL": "https://api.deepseek.com",
"OPENAI_API_KEY": "xxxx"
}
}
]
}
笔者这里创建了两个相关配置uv test single file
是项目单文件的debug配置,另一个uv test fastapi dev
是整个fastapi项目develop模式启动调试的配置.其中比较关键的配置字段是module
,需要指定为fastapi
指令,其本质上还是相当于如下uv指令
bash
OPENAI_BASE_URL='https://api.deepseek.com' OPENAI_API_KEY=xxx uv run -- fastapi dev main.py --port 8090
启动应用.
vscode中需要给项目指定python解释器,这一步其实就是将当前虚拟环境中的python解释器配置为项目的python解释器,具体在vscode中做法为:
bash
View-->Command Pattle...-->Python:Select Interpreter-->{选择你的项目文件夹}-->解释器选择本项目虚拟环境中的python(./.venv/bin/python)
这样vscode中debug选择Python Debugger: Debug using launch.json
选择uv test fastapi dev
则是以develop模式启动整个fastapi项目本地调试,选择uv test single file
则是调试项目中的单个文件.总之这是笔者总结下来的比较舒服的在vscode中开发uv项目的相关配置.
命令行运行
如果不使用vscode直接在命令行启动运行,那更简单了,直接参考前面讲到的uv运行python包中的快捷指令那一块使用uv run即可,具体指令如下:
bash
OPENAI_BASE_URL='https://api.deepseek.com' OPENAI_API_KEY=xxx uv run -- fastapi dev main.py --port 8090
深入理解 uv lock, uv sync, uv lock
uv lock 行为解析:
如果项目中没有uv.lock
文件,那么会根据pyproject.toml
解析出的精确依赖版本
生成uv.lock文件;如果项目中存在uv.lock
文件,根据解析出的精确依赖版本
更新情况决定是否更新当前uv.lock文件.
其中有一个指令uv lock --check
则是解析pyproject.toml检查是否更行uv.lock.
uv sync 行为解析
uv sync首先需要去检查uv.lock文件是否是最新的,如果不是,则解析pyproject.toml更新uv.lock中依赖的精确版本,最后再根据uv.lock文件同步下载依赖包到虚拟环境.
uv sync有两个关键参数--locked
和--frozen
,他俩区别如下:
参数 | 说明 |
---|---|
--locked | 检查uv.lock是否是最新,如果要更新则报错,如果本身就是最新的则继续同步过程 |
--frozen | 不进行uv.lock的更新检测,直接以uv.lock中依赖包版本同步虚拟环境中的依赖 |
使用较多的指令是uv sync --locked
,主要用于CI过程,保证提交的uv.lock文件中的依赖一定是最新版本,如果报错,证明uv.lock不是最新版本,需要开发提交最新的uv.lock到repo再触发ci过程.必须保证uv.lock一致性.
uv run 行为解析
可以简单理解默认的uv run之前要进行uv sync操作.
其实--frozen参数其实是是在实际部署运行时候使用,uv run --frozen xxx
表示不再check uv.lock是否更新,直接运行程序.因为前面CI过程已经保证uv.lock一致且依赖包已经最新.如果是容器部署的uv应用,基本上--frozen
参数会出现在容器入口程序中.
uv项目的docker file
根据上面的所有信息,基本上可以总结出一个uv项目的Dockerfile模板:
Dockerfile
FROM python:3.11.13-slim
WORKDIR /app
COPY ./ ./
RUN pip install uv
RUN uv sync --locked
EXPOSE 8090
ENTRYPOINT ["uv", "run", "--frozen", "--", "fastapi", "run", "main.py", "--host", "0.0.0.0", "--port", "8090"]
项目相关文件:
python
#-*- coding:utf-8 -*-
from fastapi import FastAPI
app = FastAPI(title='test')
@app.get("/")
async def index():
return 'hello'
- pyproject.toml
bash
[project]
name = "test"
version = "0.1.0"
description = "a test uv project"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"click>=8.2.1",
"fastapi[standard]>=0.116.1",
"httpx>=0.28,<0.29",
"openai>=1.99.6",
]
[dependency-groups]
dev = [
"pytest>=8.4.1",
]
lint = [
"ruff>=0.12.8",
]
构建镜像docker build -t uv-test:latest . --no-cache
并且运行docker run --rm -p 8090:8090 --name uv-test uv-test:latest
测试:
bash
curl http://localhost:8090
输出
bash
hello