打包与发布
Python打包与发布详解:setuptools、pyproject.toml。
概述
Python 的打包与发布是将项目代码转化为可安装、可分发包的过程。现代 Python 打包以 pyproject.toml 为核心配置文件,使用 setuptools、hatchling 或 flit 等构建后端。发布到 PyPI 后,其他开发者可以通过 pip 安装使用。
基础概念
包的类型
- Source Distribution (sdist):源码包,包含项目源代码
- Wheel (bdist_wheel):预编译包,安装更快,不包含 .pyc 文件
- 纯 Python Wheel:不含 C 扩展,跨平台
- 平台相关 Wheel:含 C 扩展,需针对不同平台构建
项目结构
myproject/
├── pyproject.toml # 项目配置(核心)
├── src/
│ └── mypackage/
│ ├── __init__.py
│ └── core.py
├── tests/
│ └── test_core.py
├── README.md
└── LICENSE
推荐使用 src 布局(源代码在 src 目录下),可以避免安装前意外导入未打包的代码。
快速上手
创建 pyproject.toml
[build-system]
requires = ["setuptools>=68.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "mypackage"
version = "1.0.0"
description = "一个示例 Python 包"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.9"
authors = [
{name = "作者名", email = "author@example.com"},
]
dependencies = [
"requests>=2.28.0",
"click>=8.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"mypy>=1.0",
"ruff>=0.1.0",
]
[project.scripts]
mycli = "mypackage.cli:main"
[project.urls]
Homepage = "https://github.com/user/mypackage"
构建与安装
# 安装构建工具
pip install build
# 构建包
python -m build
# 生成 dist/mypackage-1.0.0.tar.gz 和 dist/mypackage-1.0.0-py3-none-any.whl
# 本地安装测试
pip install dist/mypackage-1.0.0-py3-none-any.whl
# 开发模式安装(可编辑安装)
pip install -e .
详细用法
版本管理
# 方式一:直接在 pyproject.toml 中指定
version = "1.2.3"
# 方式二:动态从包中读取版本
[project]
dynamic = ["version"]
[tool.setuptools.dynamic]
version = {attr = "mypackage.__version__"}
在包中定义版本:
# mypackage/__init__.py
__version__ = "1.2.3"
依赖管理
[project]
dependencies = [
# 必须依赖
"requests>=2.28.0,<3.0",
"click>=8.0",
"pydantic>=2.0",
]
[project.optional-dependencies]
# 开发依赖
dev = [
"pytest>=7.0",
"mypy>=1.0",
"ruff>=0.1.0",
]
# 文档构建依赖
docs = [
"sphinx>=5.0",
"sphinx-rtd-theme",
]
# 特定功能依赖
redis = ["redis>=4.0"]
mysql = ["pymysql>=1.0"]
# 全部可选依赖
all = ["mypackage[redis,mysql]"]
安装可选依赖:
pip install mypackage[dev]
pip install mypackage[redis,mysql]
pip install mypackage[all]
入口点
# 命令行工具
[project.scripts]
mycli = "mypackage.cli:main"
# 插件注册
[project.entry-points."mypackage.plugins"]
redis = "mypackage_redis:RedisPlugin"
mysql = "mypackage_mysql:MySQLPlugin"
包含与排除文件
[tool.setuptools.packages.find]
where = ["src"]
include = ["mypackage*"]
exclude = ["tests*"]
[tool.setuptools.package-data]
mypackage = ["py.typed", "data/*.json"]
数据文件
# 在包中包含数据文件
from importlib.resources import files
# 读取包内数据文件
data_path = files("mypackage").joinpath("data/config.json")
content = data_path.read_text(encoding="utf-8")
常见场景
场景一:发布到 PyPI
# 1. 注册 PyPI 账号
# https://pypi.org/account/register/
# 2. 安装发布工具
pip install twine
# 3. 构建包
python -m build
# 4. 检查包
twine check dist/*
# 5. 上传到 TestPyPI(先测试)
twine upload --repository testpypi dist/*
# 6. 上传到正式 PyPI
twine upload dist/*
场景二:使用 Trusted Publisher
# GitHub Actions 自动发布
# 在 PyPI 上配置 Trusted Publisher
# .github/workflows/publish.yml
# name: Publish
# on:
# release:
# types: [published]
# jobs:
# publish:
# runs-on: ubuntu-latest
# permissions:
# id-token: write
# steps:
# - uses: actions/checkout@v4
# - uses: actions/setup-python@v5
# - run: pip install build
# - run: python -m build
# - uses: pypa/gh-action-pypi-publish@release/v1
场景三:C 扩展打包
[build-system]
requires = ["setuptools>=68.0", "wheel", "Cython"]
build-backend = "setuptools.build_meta"
# setup.py(C 扩展需要)
from setuptools import setup, Extension
from Cython.Build import cythonize
extensions = [
Extension("mypackage._fast", ["src/mypackage/_fast.pyx"]),
]
setup(
ext_modules=cythonize(extensions),
)
注意事项
- 使用 src 布局避免安装前意外导入未打包代码
- 版本号遵循 PEP 440 语义化版本规范
- 发布前务必在 TestPyPI 上测试
- pyproject.toml 中的 license 字段推荐使用 SPDX 标识符
- 构建时确保不包含敏感信息(.env、密钥等)
- 使用
pip install -e .进行开发模式安装,修改代码后无需重新安装
进阶用法
使用 hatchling 构建后端
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "mypackage"
version = "1.0.0"
[tool.hatch.build.targets.wheel]
packages = ["src/mypackage"]
[tool.hatch.envs.default]
dependencies = ["pytest", "mypy"]
[tool.hatch.envs.default.scripts]
test = "pytest {args:tests}"
typecheck = "mypy src"
动态版本号
# 使用 setuptools_scm 从 git 标签自动生成版本号
[build-system]
requires = ["setuptools>=68.0", "setuptools_scm>=8.0"]
build-backend = "setuptools.build_meta"
[project]
dynamic = ["version"]
[tool.setuptools_scm]
# 从 git tag 自动推断版本号
条件依赖
[project]
dependencies = [
"tomli>=2.0; python_version < '3.11'",
"importlib-metadata>=6.0; python_version < '3.10'",
"typing-extensions>=4.0; python_version < '3.11'",
]
py.typed 标记
# 在包中包含 py.typed 文件,表示支持类型检查
[tool.setuptools.package-data]
mypackage = ["py.typed"]
src/mypackage/
├── __init__.py
├── py.typed # 空文件,标记支持类型检查
└── core.pyi # 类型存根文件(可选)