uv 实战:Python 包构建与分发

Mar 15,2026 Python uv

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

sping-mountain

层层春山如黛。又是一个春天到了。

Python 的包管理机制随着Python的发展经历了几个阶段,逐渐发展成熟:

  • 史前时代(1998 - 2000年): 开发者编写一个 setup.py 文件,用户下载源码后通过 python setup.py install 进行安装。用户需要手动下载和安装依赖。
  • 变革时代:Setuptools 与 PyPI(2004年左右):
    • PyPI (Python Package Index) 诞生,成为了 Python 包的中央仓库。easy_install 可以自动从PyPI下载和安装依赖(但不支持卸载)。
    • 引入setuptools工具和 Egg 格式,将代码和元数据(如版本、依赖、入口点)打包在一起。
  • 经典时代,Pip 与 Wheel (2008 - 2014年):
    • pip 取代了 easy_install,引入了卸载功能和 requirements.txt 文件管理依赖,很长一段时间这都是最流行的方法。
    • Wheel 格式 (.whl) 诞生(2012年):它取代了 Egg 格式。Wheel 是预编译的二进制格式,安装速度极快,解决了“安装一个包得先装一堆编译器”的痛苦。
  • 现代化(2018年至今):PEP 517 & PEP 518 定义了打包的标准接口和构建时依赖问题,降低了对 setuptools 的强依赖。基于新的标准,Poetry、uv和pdm等开发管理工具提供了各自可用的构建后端,比如poetry-coreuv-buildpdm-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, 这个项目支持 intfloatstringsetdict等数据类型的加法。该项目仅仅作为演示,其中的逻辑并不重要,也不是真实存在的项目。

$ 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 treeuv run ... 验证确认功能正常可用。

总结

本文基于 uv 完整演示了 Python 包从创建、开发、构建、发布到私有 PyPI 的流程,同时说明了 PEP 517/518 下前端与后端解耦的构建体系。借助 uv 的工具链和 pypiserver 的轻量部署,可以在本地快速完成包开发与分发的闭环验证。

在新标准中,Python 包的分发已经变得十分简单和新手友好,降低了构建门槛。对 Python 生态发展具有里程碑的意义。

Last updates at Mar 15,2026

Views (10)

Leave a Comment

total 0 comments