封面出处
相较于传统的 setup.py
+ requirements.txt
的方式, 现代 Python 常使用 pyproject.toml
来管理项目依赖. 而 uv
是一个用 Rust 编写的, 速度远超 pip
的 Python 项目管理工具.
现代 Python 项目管理
现代的 Python 项目文件结构
假设项目的名称为 myproject
, 那么它的文件结构如下:
1
2
3
4
5
6
7
8
9
| myproject/ <-- 项目根目录
├── src/ <-- 源代码目录
│ └── myproject/ <-- 包代码目录
│ ├── __init__.py <-- 包初始化文件
│ └── cli.py <-- 源代码文件
├── tests/ <-- 测试目录
├── pyproject.toml
├── README.md
└── ...
|
pyproject.toml
概览
一个标准的项目配置文件中可以包含以下几个部分:
[build-system]
: 定义了如何构建整个项目, 例如构建时需要的依赖, 构建工具等[project]
: 定义了项目的各种元数据, 包括名称, 版本, 项目网址, 项目依赖等[tool]
: 可扩展性非常强的配置, 定义了各种工具的配置
下面我们逐个检查这些字段.
[build-system]
这里主要声明构建后端和构建依赖, 构建后端常使用 setuptools
:
1
2
3
| [build-system]
requires = ["setuptools >= 77.0.3"]
build-backend = "setuptools.build_meta"
|
如果你就是不想遵循默认的项目文件结构, 那么可以在 [tool.setuptools.package.find]
中声明:
1
2
| [tool.setuptools.package.find]
where = "srb"
|
这样, setuptools
将在 srb/
目录下寻找名为 myproject
的包入口. 默认情况况下, setuptools
将在 .
或者 src
目录下寻找包入口.
[project]
一个标准的 [project]
配置项如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| [project]
name = "myproject"
version = "114.514"
# 可选动态确定的元数据
dynamic = ["version"]
# 对 Python 版本的要求
requires-python = ">= 3.8"
# 依赖项, 类似于以前的 requirements.txt
dependencies = [
"numpy",
"torch",
"transformers",
# 可以按照不同的操作系统来配置
"django>2.1; os_name != 'nt'",
"django>2.0; os_name == 'nt'",
]
# 可选依赖, 可以通过类似于 `pip install ".[cli]" 的形式安装
[project.optional-dependencies]
gui = ["PyQt5"]
cli = [
"rich",
"click",
]
|
可执行脚本入口
很多时候, 在你安装了一个软件包后, 会发现它还提供了新的命令, 例如 vllm serve
之类的. 这是通过配置 [project.scripts]
来实现的. 其格式如下:
1
2
| [project.scripts]
command_name = "module.path:function_name"
|
command_name
: 脚本命令名, 例如 myproject
或者是 vllm
module.path
: 需要调用的函数所在的模块名, 例如 myproject.cli
, 对应的文件目录是 src/myproject/cli.py
.function_name
: 调用的函数名, 例如 main
或者是 serve
.
你可以注册多个不同的命令.
项目元数据
在 [project]
中, 可以配置一些额外的信息来说明这个项目. 其格式如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| [project]
description = "My project"
authors = [
{name = "Pradyun Gedam", email = "pradyun@example.com"},
{name = "Tzu-Ping Chung", email = "tzu-ping@example.com"},
{name = "Another person"},
{email = "different.person@example.com"},
]
maintainers = [
{name = "Brett Cannon", email = "brett@exampple.com"}
]
readme = "README.md"
license = "GPL-3.0-or-later"
license-files = ["LICEN[CS]E*", "vendored/licenses/*.txt", "AUTHORS.md"]
keywords = ["egg", "bacon", "sausage", "tomatoes", "Lobster Thermidor"]
# 内容参考 https://packaging.python.org/en/latest/specifications/well-known-project-urls/#well-known-labels
[project.urls]
Homepage = "https://example.com"
Documentation = "https://readthedocs.org"
Repository = "https://github.com/me/spam.git"
Issues = "https://github.com/me/spam/issues"
Changelog = "https://github.com/me/spam/blob/master/CHANGELOG.md"
# 用于 PyPI 的分类器
classifiers = [
# How mature is this project? Common values are
# 3 - Alpha
# 4 - Beta
# 5 - Production/Stable
"Development Status :: 4 - Beta",
# Indicate who your project is intended for
"Intended Audience :: Developers",
"Topic :: Software Development :: Build Tools",
# Specify the Python versions you support here.
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
]
|
uv
uv
是一个用 Rust 编写的高性能的 Python 管理工具, 包含了管理 Python 版本, 管理虚拟环境, 管理项目依赖等功能. 为什么要引入 uv
而不是使用 Python 原生的工具? 因为:
uv
能方便地管理不同的 Python 版本, 而不需要自己需下载 system wide 的 Pythonuv
求解和下载依赖项的速度远超 pip
, 大约是 10 倍. 而且求解依赖项的效率远高于 pip
, 不会存在下载多个版本一个一个试的情况uv
提供了方便的项目管理入口
命令格式:
Python 版本管理
使用 uv python
来对 Python 进行操作, 例如:
uv python list
: 列出所有安装的 Python 版本uv python install 3.7
: 安装指定版本的 Python
虚拟环境
使用 uv venv
来对虚拟环境进行操作, 命令格式为: uv venv -p <version>
, 其中 -p
表示安装指定版本的 Python, 如果不传递这个参数, 则默认选取 PATH
中的第一个 Python.
项目管理
使用 uv init <project-name>
来初始化一个新项目. 初始项目提供了最简单的配置文件, 脚本文件和 Git.
依赖管理
uv add <package>
: 添加依赖, 将会修改 pyproject.toml
文件, 并且会智能求解依赖项uv remove <package>
: 移除依赖, 将会修改 pyproject.toml
文件, 并且会智能移除多余的依赖uv sync
: 安装依赖项, 对于可选依赖项:uv sync --with [optional]
: 安装某个可选项uv sync --with op1,op2
: 安装多个可选项uv sync --with all
: 安装所有可选项
uv tree
: 查看依赖关系树uv export -o <requirements.txt>
: 导出依赖项到 requirements.txt
uv pip
: 等效于 pip
, 提供了和 pip
一样的接口, 用于老式项目管理uv run <script.py>
: 运行脚本, 等价于 python script.py
, 但是 uv
会自动识别并激活虚拟环境
实战示例
目标
实现一个 myproject
命令, 满足:
myproject say <words> -n <n>
, 会把传入的 <words>
打印出来, 重复 <n>
次myproject test
, 会输出 OK
1. 初始化项目
找一个风水宝地, 运行命令 uv init myproject
. 此时生成了一个 myproject
文件夹. 但是这个文件夹里的内容并不是严格遵循前文提到的文件目录. 我们不去管它, 还是严格按照前文的文件结构来.
2. 编写项目配置文件
按照前文给出的字段去填就是了, 如果有依赖项, 可以直接自己手写, 或者用 uv add
来添加. 这个项目没啥依赖项, 所以就不用管它了.
在配置文件中添加构建字段:
1
2
3
| [build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
|
3. 编写代码
创建 src/myproject/__init__.py
, 是个空文件都没关系, 主要是为了让 Python 把这个目录当成一个包来处理.
然后创建 src/myproject/cli.py
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
| # src/myproject/cli.py
import argparse
import sys # 通常需要导入 sys 来访问命令行参数,尽管 argparse 会帮你处理大部分
def say_command(args):
"""处理 'say' 子命令的逻辑"""
for _ in range(args.n):
print(args.words)
def test_command(args):
"""处理 'test' 子命令的逻辑"""
print("OK")
def main():
"""
这个函数是 'myproject' 命令的入口点。
它使用 argparse 来解析命令行参数和子命令。
"""
parser = argparse.ArgumentParser(
description="My awesome project command line tool" # 命令行工具的描述
)
# 创建子解析器,用于定义子命令 (say, test)
subparsers = parser.add_subparsers(
dest="command", # 这个参数的名字将存储用户输入的子命令名称
help="Available commands"
)
# --- 定义 'say' 子命令 ---
say_parser = subparsers.add_parser("say", help="Say something multiple times")
# 添加 'say' 命令所需的参数
say_parser.add_argument(
"words", # 位置参数,用户必须提供
type=str,
help="The words to say"
)
say_parser.add_argument(
"-n", # 可选参数
type=int,
default=1, # 如果用户不提供 -n,默认值为 1
help="Number of times to say the words (default: 1)"
)
# 将处理 'say' 子命令的函数绑定到这个解析器
# 当用户输入 'myproject say ...' 时,argparse 会调用 say_command 函数
say_parser.set_defaults(func=say_command)
# --- 定义 'test' 子命令 ---
test_parser = subparsers.add_parser("test", help="Run a simple test")
# 将处理 'test' 子命令的函数绑定到这个解析器
# 当用户输入 'myproject test' 时,argparse 会调用 test_command 函数
test_parser.set_defaults(func=test_command)
# 解析命令行参数
args = parser.parse_args()
# 根据解析结果执行相应的函数
# args.func 是我们在 set_defaults 中设置的函数 (say_command 或 test_command)
# args 中包含了所有的参数值
if hasattr(args, "func"): # 检查是否输入了子命令
args.func(args)
else:
# 如果用户只输入了 'myproject' 没有子命令,则显示帮助信息
parser.print_help()
# 这个 if __name__ == "__main__": 块使得你可以直接运行 cli.py 文件进行测试
# 但是当作为入口点被调用时,是直接执行上面的 main() 函数
if __name__ == "__main__":
main()
|
4. 添加入口
在配置文件中添加如下字段:
1
2
| [project.scripts]
myproject = "myproject.cli:main"
|
5. 构建并运行
使用 uv sync
构建项目, 然后激活虚拟环境, 你将会看到 myproject
命令在终端中已经可用了.