16 - 打包与分发
第 16 章:打包与分发
学会使用现代 Python 工具打包和分发你的项目。
16.1 Python 打包生态演进
| 年代 | 工具 | 配置文件 |
|---|---|---|
| 2004 | setuptools | setup.py |
| 2012 | wheels | — |
| 2018 | Poetry | pyproject.toml |
| 2020 | PEP 517/518 | pyproject.toml |
| 2021 | PEP 621 | pyproject.toml(标准元数据) |
| 2023 | uv | pyproject.toml |
2025 推荐:pyproject.toml + setuptools 或 Poetry 或 uv
16.2 pyproject.toml(标准配置)
[project]
name = "my-package"
version = "1.0.0"
description = "A short description of my package"
readme = "README.md"
license = { text = "MIT" }
requires-python = ">=3.11"
authors = [
{ name = "Alice", email = "[email protected]" },
]
keywords = ["example", "tutorial"]
classifiers = [
"Development Status :: 4 - Beta",
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
]
dependencies = [
"requests>=2.28.0",
"pydantic>=2.0,<3.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"ruff>=0.4",
"mypy>=1.10",
]
docs = [
"sphinx>=7.0",
"sphinx-rtd-theme>=2.0",
]
[project.scripts]
my-cli = "my_package.cli:main"
[project.urls]
Homepage = "https://github.com/alice/my-package"
Documentation = "https://my-package.readthedocs.io"
Repository = "https://github.com/alice/my-package"
[build-system]
requires = ["setuptools>=68.0", "wheel"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
where = ["src"]
16.3 使用 setuptools
16.3.1 项目结构
my-package/
├── src/
│ └── my_package/
│ ├── __init__.py
│ ├── cli.py
│ └── core.py
├── tests/
│ └── test_core.py
├── pyproject.toml
├── README.md
└── LICENSE
16.3.2 构建与安装
# 安装构建工具
$ pip install build twine
# 构建
$ python -m build
# 生成 dist/my_package-1.0.0.tar.gz 和 dist/my_package-1.0.0-py3-none-any.whl
# 本地安装
$ pip install dist/my_package-1.0.0-py3-none-any.whl
# 开发模式安装
$ pip install -e ".[dev]"
16.4 使用 Poetry
16.4.1 基本操作
# 安装 Poetry
$ curl -sSL https://install.python-poetry.org | python3 -
# 创建新项目
$ poetry new my-package
# 初始化已有项目
$ poetry init
# 添加依赖
$ poetry add requests pydantic
$ poetry add --group dev pytest ruff
# 安装依赖
$ poetry install
# 运行命令
$ poetry run python -m my_package
$ poetry run pytest
# 构建
$ poetry build
# 发布
$ poetry publish
16.4.2 Poetry 的 pyproject.toml
[tool.poetry]
name = "my-package"
version = "1.0.0"
description = "A short description"
authors = ["Alice <[email protected]>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.11"
requests = "^2.28"
pydantic = "^2.0"
[tool.poetry.group.dev.dependencies]
pytest = "^8.0"
ruff = "^0.4"
[tool.poetry.scripts]
my-cli = "my_package.cli:main"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
16.5 使用 uv(极速)
# 安装 uv
$ curl -LsSf https://astral.sh/uv/install.sh | sh
# 创建虚拟环境
$ uv venv
# 安装依赖
$ uv pip install requests
# 同步依赖
$ uv pip sync requirements.txt
# 构建
$ uv build
# 运行工具
$ uv run pytest
$ uv run ruff check .
16.6 发布到 PyPI
16.6.1 发布流程
# 1. 注册 PyPI 账号: https://pypi.org/account/register/
# 2. 创建 API Token
# 3. 构建
$ python -m build
# 4. 检查包
$ twine check dist/*
# 5. 上传到 TestPyPI(测试)
$ twine upload --repository testpypi dist/*
# 6. 上传到 PyPI
$ twine upload dist/*
16.6.2 使用 API Token
# ~/.pypirc
[distutils]
index-servers = pypi testpypi
[pypi]
username = __token__
password = pypi-AgEI...
[testpypi]
repository = https://test.pypi.org/legacy/
username = __token__
password = pypi-AgEI...
16.7 私有 PyPI 源
16.7.1 使用 devpi
# 安装
$ pip install devpi-server devpi-client
# 启动服务
$ devpi-init
$ devpi-server --host 0.0.0.0 --port 3141
# 使用
$ devpi use http://localhost:3141
$ devpi login root --password ''
$ devpi upload
16.7.2 配置 pip 使用私有源
# pip.conf
[global]
index-url = https://pypi.tuna.tsinghua.edu.cn/simple
extra-index-url = https://my-private-pypi.example.com/simple/
16.8 版本管理
16.8.1 语义化版本
MAJOR.MINOR.PATCH
MAJOR: 不兼容的 API 变更
MINOR: 向后兼容的功能新增
PATCH: 向后兼容的 Bug 修复
16.8.2 动态版本
# 从 Git 标签获取版本
# pyproject.toml
[tool.setuptools_scm]
$ pip install setuptools-scm
$ python -m setuptools_scm
16.9 注意事项
🔴 注意:
- 不要将
.pyc文件、__pycache__或虚拟环境发布到 PyPI - 依赖声明要指定版本范围,避免不兼容更新
- 发布前先在 TestPyPI 测试
- 不要在代码中硬编码版本号,使用
importlib.metadata
💡 提示:
- 使用
pyproject.toml统一项目配置 - 使用
uv加速依赖安装 - 使用
ruff替代black+isort+flake8 - 使用 GitHub Actions 自动发布
📌 业务场景:
# 获取包版本(PEP 508)
from importlib.metadata import version
try:
pkg_version = version("my-package")
except Exception:
pkg_version = "unknown"