uv 实战:Python 包构建与分发
从使用方式上区分,有两种Python项目,一种是应用程序,面向最终用户;一种是Python库项目,可以供其他项目使用,比如常用的requests、Flask、SQLAlchemy等库。本文探讨下uv如何支持Python的库开发与分发。

层层春山如黛。又是一个春天到了。
Python 的包管理机制随着Python的发展经历了几个阶段,逐渐发展成熟:
- 史前时代(1998 - 2000年): 开发者编写一个
setup.py文件,用户下载源码后通过python setup.py install进行安装。用户需要手动下载和安装依赖。 - 变革时代:Setuptools 与 PyPI(2004年左右):
- PyPI (Python Package Index) 诞生,成为了 Python 包的中央仓库。
easy_install可以自动从PyPI下载和安装依赖(但不支持卸载)。 - 引入
setuptools工具和Egg格式,将代码和元数据(如版本、依赖、入口点)打包在一起。
- PyPI (Python Package Index) 诞生,成为了 Python 包的中央仓库。
- 经典时代,Pip 与 Wheel (2008 - 2014年):
- pip 取代了 easy_install,引入了卸载功能和 requirements.txt 文件管理依赖,很长一段时间这都是最流行的方法。
- Wheel 格式 (.whl) 诞生(2012年):它取代了 Egg 格式。Wheel 是预编译的二进制格式,安装速度极快,解决了“安装一个包得先装一堆编译器”的痛苦。
- 现代化(2018年至今):
PEP 517 & PEP 518定义了打包的标准接口和构建时依赖问题,降低了对setuptools的强依赖。基于新的标准,Poetry、uv和pdm等开发管理工具提供了各自可用的构建后端,比如poetry-core、uv-build和pdm-backend。
在 PEP 517 和 PEP 518 确立的现代标准下,Python 的打包流程被解耦为前端和后端。前端负责与用户交互,后端负责生成最终的产品(Wheel 或源码包)。在 pyproject.toml中可以声明项目使用的后端。项目的后端工具不一定要和前端匹配,比如uv也可以搭配
poetry-core后端。
本文将基于uv创建一个Python包,并将其上传到自建的PyPI。演示完整的Python包开发、分发和使用的生命周期。
版本信息:
- OS: Debian 13(Trixie)
- Python:3.13
- uv:0.10.7
universal-add 包开发
本文将开发 universal-add包,使其支持各类数据结构的加法操作。
创建项目
使用 uv 创建项目 universal-add, 这个项目支持 int、float、string、set、dict等数据类型的加法。该项目仅仅作为演示,其中的逻辑并不重要,也不是真实存在的项目。
$ uv init --python ">=3.10" universal-add --package --description "an universal python add package"
指定了项目支持 python 3.10 及以上版本,使用包格式(src布局)初始化项目,并为项目设置一个描述。
以下是生成的 pyproject.toml 文件
[project]
name = "universal-add"
version = "0.1.0"
description = "an universal python add package"
readme = "README.md"
authors = [
{ name = "xnow", email = "xnow@xnow.me" }
]
requires-python = ">=3.10"
dependencies = []
[project.scripts]
uadd = "universal_add:main" # 手动将 universal-add 修改为 uadd
[build-system]
requires = ["uv_build>=0.10.7,<0.11.0"]
build-backend = "uv_build"
pyproject.toml 文件的大部分内容我们都很熟悉了,有两项新增的配置:
[project.scripts],指定uadd命令的入口函数为universal_add包中的main()函数。把默认的universal-add修改为uadd,安装后这个包的命令就变为了uadd。[build-system]就是uv指定的构建后端,这里使用了uv默认的后端uv_build。
项目代码
进入项目目录,修改 src/universal_add/__init__.py 为如下内容:
import ast
import click
import sys
from typing import TypeVar, Union, TypeAlias
# Define type aliases for supported types
Addable: TypeAlias = Union[int, float, str, list, dict, set]
T = TypeVar("T", int, float, str, list, dict, set)
class IncompatibleTypeError(TypeError):
"""Raised when two types cannot be added or merged according to the defined logic."""
pass
def add(a: T, b: T) -> T:
"""
Universal addition core logic.
"""
if not isinstance(b, type(a)) and not (
isinstance(a, (int, float)) and isinstance(b, (int, float))
):
raise IncompatibleTypeError(
f"Cannot add {type(a).__name__} and {type(b).__name__}"
)
match a:
case dict() | set():
return a | b # type: ignore
case _:
return a + b # type: ignore
def smart_parse(value: str) -> any:
"""
Convert CLI string input into Python objects safely.
"""
try:
return ast.literal_eval(value)
except (ValueError, SyntaxError):
# Fallback to raw string if it's not a valid Python literal
return value
@click.command()
@click.argument("a")
@click.argument("b")
@click.version_option(version="0.1.0")
def main(a: str, b: str):
"""
Universal Add CLI: A tool to add/merge numbers, strings, lists, and dicts.
Example:
uadd 1 2
uadd "[1,2]" "[3,4]"
uadd "{'key': 'val'}" "{'new': 1}"
"""
try:
# Parse inputs
val_a = smart_parse(a)
val_b = smart_parse(b)
# Execute addition
result = add(val_a, val_b)
# Output result
click.echo(result)
except IncompatibleTypeError as e:
click.secho(f"Error: {e}", fg="red", err=True)
sys.exit(1)
except Exception as e:
click.secho(f"Unexpected Error: {e}", fg="yellow", err=True)
sys.exit(1)
if __name__ == "__main__":
main()
本项目的main()函数使用click接收和处理用户传入的参数,用户传入。安装click依赖:
$ uv add click
$ uv sync
项目名是 universal-add ,通过 uv tool install可以发布到全局uv 工具中,后续可以方便执行。
$ uv tool install . --editable # --editable 模式,会同步项目代码的更改
$ uv tool list
universal-add v0.1.0
- uadd
$ uv run uadd 100 200
300
$ uadd a b
ab
跟踪下底层逻辑:
$ which uadd
~/.local/bin/uadd
$ realpath ~/.local/bin/uadd
~/.local/share/uv/tools/universal-add/bin/uadd
$ cat ~/.local/share/uv/tools/universal-add/bin/uadd
#!/home/xnow/.local/share/uv/tools/universal-add/bin/python3
# -*- coding: utf-8 -*-
import sys
from universal_add import main
if __name__ == "__main__":
if sys.argv[0].endswith("-script.pyw"):
sys.argv[0] = sys.argv[0][:-11]
elif sys.argv[0].endswith(".exe"):
sys.argv[0] = sys.argv[0][:-4]
sys.exit(main())
查看相应的 .pth 文件,指向了 universal-add 的开发目录。
$ cat ~/.local/share/uv/tools/universal-add/lib/python3.13/site-packages/universal_add.pth
/tmp/universal-add/src
.pth 文件提供了一种声明式的方法: 只要在 site-packages 里放一个 <project name>.pth,里面写上项目的绝对路径,Python 就能像访问内置库一样访问你的项目。
好的,项目到这里就开发好了,接下来部署 PyPI 服务。
PyPI 部署和使用
Python官方中央仓库地址为 pypi.org,上传的地址为:https://upload.pypi.org/legacy/ 。 Python也提供了供练习使用的测试版本中央仓库,地址为 test.pypi.org,上传地址为 https://test.pypi.org/legacy/ 。
本文使用自建 PyPI 的方法测试Python包上传和下载使用。自建PyPI有多种方式,比较靠谱的是使用 Sonatype Nexus Repository 之类的成熟软件,可以host自己编写的包,也可以代理官方的PyPI。为了测试方便,我们使用 pypiserver 部署简易轻量PyPI,当前版本为 v2.4.1。
$ uv tool install pypiserver[passlib] --with bcrypt==4.0.1
这里的passlib是加密库,要给pypi设置密码就需要这个库。 在bcrypt做密码加密的情况下,pypiserver需要安装对应的bcrypt库,使用 --with bcrypt==4.0.1 参数指定。
创建密码文件 password.txt,使用bcrypt 算法加密,用户名为 xnow,密码为 xnowXNOWxnow。如果bcrypt依赖不存在,先pip安装下。
$ python -c 'import bcrypt; user="xnow";passwd="xnowXNOWxnow"; print("%s:%s" % (user,bcrypt.hashpw(passwd.encode(), bcrypt.gensalt()).decode()))' > password.txt
启动 pypiserver ,监听到 8082 端口上:
$ mkdir pypi_data
$ pypi-server -P password.txt -p 8082 pypi_data/
启动免密 pypiserver的方法是
pypi-server -p 8082 pypi_data/ -a . -P .
上传 universal-add 项目
进入到 universal-add 项目目录,执行构建和发布操作
$ uv build
$ ls dist/
universal_add-0.1.0-py3-none-any.whl universal_add-0.1.0.tar.gz
$ uv publish --publish-url=http://127.0.0.1:8082
Publishing 2 files to http://127.0.0.1:1888/
Enter username ('__token__' if using a token): xnow
Enter password:
Uploading universal_add-0.1.0.tar.gz (1.4KiB)
Uploading universal_add-0.1.0-py3-none-any.whl (2.4KiB)
先使用 uv build 构建,会在项目的 dist/ 目录下创建 .whl和.tar.gz 文件。运行 uv publish ... 命令就是上传这两个文件到 PyPI上。
然后使用 uv publish 上传到PyPI。按提示输入用户名和密码即可。 整个过程几乎不用关注构建过程的细节,对于新手开发者也十分的友好。
浏览器打开 http://127.0.0.1:8082/simple/,也可以看到上传的包。下载是不需要认证的。
新项目中安装 universal-add
打开新的命令行窗口,创建一个测试用的新项目 testproj
$ uv init testproj
$ cd testproj
$ uv add universal-add --index http://127.0.0.1:8082/simple
$ uv tree
Resolved 4 packages in 1ms
testproj v0.1.0
└── universal-add v0.1.0
└── click v8.3.1
$ uv run python -c 'from universal_add import add; print(add("hello ", "42"))'
hello 42
安装 universal-add 依赖的时候,用--index指定本地搭建的 pypiserver 服务。通过 uv tree 和 uv run ... 验证确认功能正常可用。
总结
本文基于 uv 完整演示了 Python 包从创建、开发、构建、发布到私有 PyPI 的流程,同时说明了 PEP 517/518 下前端与后端解耦的构建体系。借助 uv 的工具链和 pypiserver 的轻量部署,可以在本地快速完成包开发与分发的闭环验证。
在新标准中,Python 包的分发已经变得十分简单和新手友好,降低了构建门槛。对 Python 生态发展具有里程碑的意义。
Last updates at Mar 15,2026
Views (10)
total 0 comments