强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

16 - 打包与分发

第 16 章:打包与分发

学会使用现代 Python 工具打包和分发你的项目。


16.1 Python 打包生态演进

年代工具配置文件
2004setuptoolssetup.py
2012wheels
2018Poetrypyproject.toml
2020PEP 517/518pyproject.toml
2021PEP 621pyproject.toml(标准元数据)
2023uvpyproject.toml

2025 推荐pyproject.toml + setuptoolsPoetryuv


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"

16.10 扩展阅读