使用 uv 构建并发布 Python 包到 PyPI 完整教程 目录
概述
环境准备
创建项目
pyproject.toml 配置详解
编写包代码
构建包
发布到 PyPI
使用 TestPyPI 测试
版本管理
最佳实践
常见问题
概述 本教程将指导你使用 uv (一个超快的 Python 包管理器)来:
创建一个标准的 Python 包项目
配置 pyproject.toml
构建源码分发包(sdist)和 wheel 包
发布到 PyPI (Python Package Index)
为什么选择 uv?
极快 :比 pip 快 10-100 倍
一体化 :替代 pip、pip-tools、poetry、pyenv、twine 等多个工具
现代 :原生支持 pyproject.toml 和最新的 Python 打包标准
可靠 :提供锁文件和可重现的环境
环境准备 1. 安装 uv macOS 和 Linux:
1 curl -LsSf https://astral.sh/uv/install.sh | sh
Windows (PowerShell):
1 powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
通过 pip 安装:
2. 验证安装
3. 安装 Python(如果需要) 1 2 3 4 5 uv python install 3.12 uv python list
创建项目 方法一:使用 uv init 自动创建 1 2 3 4 5 6 7 uv init my-awesome-packagecd my-awesome-package
方法二:手动创建项目结构 my-awesome-package/
├── src/
│ └── my_awesome_package/
│ ├── __init__.py
│ └── core.py
├── tests/
│ ├── __init__.py
│ └── test_core.py
├── pyproject.toml
├── README.md
├── LICENSE
└── .gitignore
创建目录结构:
1 2 3 mkdir -p my-awesome-package/src/my_awesome_packagemkdir -p my-awesome-package/testscd my-awesome-package
pyproject.toml 配置详解 pyproject.toml 是 Python 项目的核心配置文件,包含三个主要部分:
完整的 pyproject.toml 示例 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 69 70 71 72 73 74 75 76 77 78 79 80 [build-system] requires = ["hatchling >= 1.26" ]build-backend = "hatchling.build" [project] name = "my-awesome-package" version = "0.1.0" authors = [ { name = "Your Name" , email = "your.email@example.com" } ]maintainers = [ { name = "Your Name" , email = "your.email@example.com" } ]description = "A short description of your package" readme = "README.md" requires-python = ">=3.9" license = "MIT" license-files = ["LICEN[CS]E*" ]keywords = ["example" , "package" , "tutorial" ]classifiers = [ "Development Status :: 3 - Alpha" , "Intended Audience :: Developers" , "License :: OSI Approved :: MIT License" , "Operating System :: OS Independent" , "Programming Language :: Python :: 3" , "Programming Language :: Python :: 3.9" , "Programming Language :: Python :: 3.10" , "Programming Language :: Python :: 3.11" , "Programming Language :: Python :: 3.12" , "Programming Language :: Python :: 3.13" , ]dependencies = [ "requests>=2.28.0" , "pydantic>=2.0.0" , ][project.optional-dependencies] dev = [ "pytest>=7.0.0" , "pytest-cov>=4.0.0" , "ruff>=0.1.0" , "mypy>=1.0.0" , ]docs = [ "mkdocs>=1.5.0" , "mkdocs-material>=9.0.0" , ][project.urls] Homepage = "https://github.com/yourusername/my-awesome-package" Documentation = "https://github.com/yourusername/my-awesome-package#readme" Repository = "https://github.com/yourusername/my-awesome-package" Issues = "https://github.com/yourusername/my-awesome-package/issues" Changelog = "https://github.com/yourusername/my-awesome-package/blob/main/CHANGELOG.md" [project.scripts] my-cli = "my_awesome_package.cli:main" [project.gui-scripts] my-gui = "my_awesome_package.gui:main" [tool.hatch.build.targets.wheel] packages = ["src/my_awesome_package" ][tool.ruff] line-length = 88 target-version = "py39" [tool.ruff.lint] select = ["E" , "F" , "I" , "N" , "W" , "UP" ][tool.pytest.ini_options] testpaths = ["tests" ]addopts = "-v --cov=my_awesome_package" [tool.mypy] python_version = "3.9" warn_return_any = true warn_unused_configs = true
各部分详解 [build-system] - 构建系统配置 1 2 3 [build-system] requires = ["hatchling >= 1.26" ]build-backend = "hatchling.build"
常用的构建后端:
构建后端
requires
build-backend
Hatchling (推荐)
"hatchling >= 1.26"
"hatchling.build"
Setuptools
"setuptools >= 77.0.3"
"setuptools.build_meta"
Flit
"flit_core >= 3.12.0, <4"
"flit_core.buildapi"
PDM
"pdm-backend >= 2.4.0"
"pdm.backend"
uv-build
"uv_build >= 0.11.7, <0.12.0"
"uv_build"
[project] - 项目元数据
字段
说明
示例
name
包名(PyPI 上的名称)
"my-awesome-package"
version
版本号
"0.1.0"
description
简短描述
"A short description"
readme
README 文件路径
"README.md"
requires-python
Python 版本要求
">=3.9"
license
SPDX 许可证表达式
"MIT"
dependencies
运行时依赖
["requests>=2.28.0"]
[project.urls] - 项目链接 1 2 3 4 5 [project.urls] Homepage = "https://github.com/user/repo" Documentation = "https://readthedocs.org" Repository = "https://github.com/user/repo" Issues = "https://github.com/user/repo/issues"
[project.scripts] - 命令行工具 1 2 [project.scripts] my-cli = "my_package.module:function"
安装后会创建一个 my-cli 命令。
[project.optional-dependencies] - 可选依赖 1 2 3 [project.optional-dependencies] dev = ["pytest" , "ruff" ]docs = ["mkdocs" ]
安装时使用:pip install my-package[dev]
编写包代码 1. 创建 init .py src/my_awesome_package/init .py:
1 2 3 4 5 6 7 8 9 """My Awesome Package - A short description.""" __version__ = "0.1.0" __author__ = "Your Name" __email__ = "your.email@example.com" from .core import hello, add_numbers __all__ = ["hello" , "add_numbers" , "__version__" ]
2. 编写核心模块 src/my_awesome_package/core.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 """Core functionality of my awesome package.""" def hello (name: str = "World" ) -> str : """Say hello to someone. Args: name: The name to greet. Defaults to "World". Returns: A greeting string. """ return f"Hello, {name} !" def add_numbers (a: int | float , b: int | float ) -> int | float : """Add two numbers together. Args: a: First number. b: Second number. Returns: The sum of a and b. """ return a + bclass Calculator : """A simple calculator class.""" def __init__ (self ) -> None : self .history: list [str ] = [] def add (self, a: float , b: float ) -> float : """Add two numbers.""" result = a + b self .history.append(f"{a} + {b} = {result} " ) return result def subtract (self, a: float , b: float ) -> float : """Subtract b from a.""" result = a - b self .history.append(f"{a} - {b} = {result} " ) return result def get_history (self ) -> list [str ]: """Get calculation history.""" return self .history.copy()
3. 创建 CLI 入口(可选) src/my_awesome_package/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 """Command-line interface for my awesome package.""" import argparseimport sysfrom . import __version__from .core import hellodef main () -> int : """Main entry point for the CLI.""" parser = argparse.ArgumentParser( description="My Awesome Package CLI" ) parser.add_argument( "--version" , action="version" , version=f"%(prog)s {__version__} " ) parser.add_argument( "--name" , default="World" , help ="Name to greet" ) args = parser.parse_args() print (hello(args.name)) return 0 if __name__ == "__main__" : sys.exit(main())
4. 编写测试 tests/test_core.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 """Tests for core module.""" import pytestfrom my_awesome_package.core import Calculator, add_numbers, helloclass TestHello : """Tests for hello function.""" def test_hello_default (self ): assert hello() == "Hello, World!" def test_hello_with_name (self ): assert hello("Alice" ) == "Hello, Alice!" def test_hello_empty_string (self ): assert hello("" ) == "Hello, !" class TestAddNumbers : """Tests for add_numbers function.""" def test_add_positive_integers (self ): assert add_numbers(2 , 3 ) == 5 def test_add_negative_integers (self ): assert add_numbers(-1 , -1 ) == -2 def test_add_floats (self ): assert add_numbers(1.5 , 2.5 ) == 4.0 def test_add_mixed (self ): assert add_numbers(1 , 2.5 ) == 3.5 class TestCalculator : """Tests for Calculator class.""" def test_add (self ): calc = Calculator() assert calc.add(2 , 3 ) == 5 def test_subtract (self ): calc = Calculator() assert calc.subtract(5 , 3 ) == 2 def test_history (self ): calc = Calculator() calc.add(1 , 2 ) calc.subtract(5 , 3 ) history = calc.get_history() assert len (history) == 2 assert "1 + 2 = 3" in history[0 ]
5. 创建 README.md 1 2 3 4 5 6 7 8 9 10 11 # My Awesome Package [](https://pypi.org/project/my-awesome-package/ ) [](https://pypi.org/project/my-awesome-package/ ) A short description of your package.## Installation ```bash pip install my-awesome-package
Quick Start 1 2 3 4 5 6 7 8 from my_awesome_package import hello, add_numbersprint (hello("World" )) result = add_numbers(2 , 3 )print (result)
CLI Usage
Development 1 2 3 4 5 6 7 8 9 10 11 12 git clone https://github.com/yourusername/my-awesome-package.gitcd my-awesome-package uv sync --extra dev uv run pytest uv run ruff check .
License MIT License - see LICENSE for details.
### 6. 创建 LICENSE
MIT License
Copyright (c) 2024 Your Name
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## 构建包
### 1. 使用 uv 管理依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 uv add requests pydantic uv add --dev pytest ruff mypy uv sync uv sync --extra dev ``` bash uv run pytest uv run pytest --cov=my_awesome_package --cov-report=html
3. 代码检查 1 2 3 4 5 6 7 8 uv run ruff check . uv run ruff format . uv run mypy src/
4. 构建分发包 1 2 3 4 5 uv build uv build --no-sources
构建完成后,会在 dist/ 目录下生成:
dist/
├── my_awesome_package-0.1.0.tar.gz # 源码分发包 (sdist)
└── my_awesome_package-0.1.0-py3-none-any.whl # wheel 包
5. 验证构建 1 2 3 4 5 6 uv pip show --files dist/my_awesome_package-0.1.0-py3-none-any.whl pip install twine twine check dist/*
发布到 PyPI 1. 注册 PyPI 账号
访问 https://pypi.org/account/register/
完成注册并验证邮箱
启用双因素认证(推荐)
2. 生成 API Token
登录 PyPI
访问 https://pypi.org/manage/account/token/
点击 “Add API token”
设置 Token 名称和权限范围
立即复制并保存 Token (只显示一次)
Token 格式:pypi-AgEI...
3. 配置认证信息 方法一:环境变量(推荐)
1 export UV_PUBLISH_TOKEN="pypi-your-token-here"
方法二:命令行参数
1 uv publish --token "pypi-your-token-here"
方法三:使用 .pypirc 文件
创建 ~/.pypirc:
1 2 3 4 5 6 7 [pypi] username = __token__password = pypi-your-token-here[testpypi] username = __token__password = pypi-your-testpypi-token-here
设置文件权限:
4. 发布到 PyPI 1 2 3 4 5 6 7 8 uv publish uv publish --token "pypi-your-token-here" uv publish --index pypi
5. 验证发布 发布成功后,访问 https://pypi.org/project/my-awesome-package/ 查看你的包。
安装测试:
1 2 uv run --with my-awesome-package --no-project -- python -c "from my_awesome_package import hello; print(hello())"
使用 TestPyPI 测试 在正式发布前,建议先在 TestPyPI 上测试。
1. 注册 TestPyPI 账号 访问 https://test.pypi.org/account/register/
2. 配置 TestPyPI 索引 在 pyproject.toml 中添加:
1 2 3 4 5 [[tool.uv.index]] name = "testpypi" url = "https://test.pypi.org/simple/" publish-url = "https://test.pypi.org/legacy/" explicit = true
3. 发布到 TestPyPI 1 2 uv publish --index testpypi --token "pypi-your-testpypi-token"
4. 从 TestPyPI 安装测试 1 2 3 4 5 6 7 8 9 10 uv venv test-envsource test-env/bin/activate uv pip install --index-url https://test.pypi.org/simple/ my-awesome-package python -c "from my_awesome_package import hello; print(hello())"
版本管理 使用 uv version 管理版本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 uv version uv version 1.0.0 uv version 2.0.0 --dry-run uv version --bump patch uv version --bump minor uv version --bump major uv version --bump patch --bump beta uv version --bump major --bump alpha uv version --bump stable
版本号规范 遵循 语义化版本 :
MAJOR.MINOR.PATCH (例如:1.2.3)
MAJOR : 不兼容的 API 变更
MINOR : 向下兼容的功能性新增
PATCH : 向下兼容的问题修正
预发布版本:1.0.0a1, 1.0.0b2, 1.0.0rc1
最佳实践 1. 项目结构 推荐使用 src 布局:
my-package/
├── src/
│ └── my_package/
│ ├── __init__.py
│ ├── core.py
│ └── py.typed # PEP 561 类型标记
├── tests/
├── pyproject.toml
├── README.md
├── LICENSE
└── .gitignore
2. 依赖管理 1 2 3 4 5 6 7 8 9 dependencies = [ "requests>=2.28.0,<3.0.0" , "pydantic>=2.0.0" , ][project.optional-dependencies] dev = ["pytest>=7.0" , "ruff>=0.1.0" ]
3. CI/CD 自动发布 GitHub Actions 示例 (.github/workflows/publish.yml):
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 name: Publish to PyPI on: release: types: [published ]jobs: publish: runs-on: ubuntu-latest permissions: id-token: write steps: - uses: actions/checkout@v4 - name: Install uv uses: astral-sh/setup-uv@v4 - name: Set up Python run: uv python install 3.12 - name: Run tests run: | uv sync --extra dev uv run pytest - name: Build package run: uv build --no-sources - name: Publish to PyPI run: uv publish env: UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
4. 可信发布(Trusted Publishing) PyPI 支持无需 Token 的可信发布:
在 PyPI 项目设置中添加 GitHub Actions 作为可信发布者
配置 GitHub Actions 使用 id-token: write 权限
不需要设置任何 Token
1 2 3 - name: Publish to PyPI run: uv publish
5. 发布检查清单 发布前确认:
1 2 3 4 5 6 uv run pytest && \ uv run ruff check . && \ uv build --no-sources && \ twine check dist/* && \ uv publish
常见问题 Q1: 包名已被占用怎么办?
使用更有创意的名称
添加前缀(如 yourname-packagename)
检查 PyPI 上是否真的需要该名称
Q2: 构建失败怎么办? 1 2 3 4 5 6 7 8 rm -rf dist/ build/ *.egg-info uv build --no-sources uv build --verbose
Q3: 如何删除已发布的版本? PyPI 不允许删除已发布的版本,只能:
发布新版本
使用 yanking 标记版本为不推荐
Q4: 如何处理二进制扩展? 如果包含 C/C++ 扩展,需要使用 cibuildwheel:
1 2 3 - name: Build wheels uses: pypa/cibuildwheel@v2.16
Q5: 发布后如何更新包? 1 2 3 4 5 6 uv version --bump patch uv build --no-sources uv publish
Q6: 如何在私有仓库发布? 1 2 3 4 5 [[tool.uv.index]] name = "private" url = "https://private.pypi.org/simple/" publish-url = "https://private.pypi.org/legacy/"
1 uv publish --index private
参考资源
快速参考命令 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 uv init my-packagecd my-package uv add requests uv add --dev pytest uv sync uv run python -m my_package uv run pytest uv version uv version --bump patch uv build --no-sources uv publish uv publish --index testpypi
最后更新:2024