name: python-packaging description: 使用正确的项目结构、setup.py/pyproject.toml创建可分发的Python包,并发布到PyPI。适用于打包Python库、创建CLI工具或分发Python代码时使用。
Python打包
使用现代打包工具、pyproject.toml创建、结构化和分发Python包的综合指南,并发布到PyPI。
何时使用此技能
- 创建用于分发的Python库
- 构建带有入口点的命令行工具
- 发布包到PyPI或私有仓库
- 设置Python项目结构
- 创建带有依赖的可安装包
- 构建wheel和源分发
- 版本控制和发布Python包
- 创建命名空间包
- 实现包元数据和分类器
核心概念
1. 包结构
- 源码布局:
src/package_name/(推荐) - 扁平布局:
package_name/(更简单但灵活性较差) - 包元数据: pyproject.toml、setup.py或setup.cfg
- 分发格式: wheel (.whl) 和源分发 (.tar.gz)
2. 现代打包标准
- PEP 517/518: 构建系统要求
- PEP 621: pyproject.toml中的元数据
- PEP 660: 可编辑安装
- pyproject.toml: 单一配置源
3. 构建后端
- setuptools: 传统,广泛使用
- hatchling: 现代,有主见
- flit: 轻量级,适用于纯Python
- poetry: 依赖管理 + 打包
4. 分发
- PyPI: Python包索引(公开)
- TestPyPI: 生产前测试
- 私有仓库: JFrog、AWS CodeArtifact等
快速开始
最小包结构
my-package/
├── pyproject.toml
├── README.md
├── LICENSE
├── src/
│ └── my_package/
│ ├── __init__.py
│ └── module.py
└── tests/
└── test_module.py
最小pyproject.toml
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "my-package"
version = "0.1.0"
description = "简短描述"
authors = [{name = "你的名字", email = "you@example.com"}]
readme = "README.md"
requires-python = ">=3.8"
dependencies = [
"requests>=2.28.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"black>=22.0",
]
包结构模式
模式1:源码布局(推荐)
my-package/
├── pyproject.toml
├── README.md
├── LICENSE
├── .gitignore
├── src/
│ └── my_package/
│ ├── __init__.py
│ ├── core.py
│ ├── utils.py
│ └── py.typed # 用于类型提示
├── tests/
│ ├── __init__.py
│ ├── test_core.py
│ └── test_utils.py
└── docs/
└── index.md
优势:
- 防止意外从源码导入
- 更干净的测试导入
- 更好的隔离
源码布局的pyproject.toml:
[tool.setuptools.packages.find]
where = ["src"]
模式2:扁平布局
my-package/
├── pyproject.toml
├── README.md
├── my_package/
│ ├── __init__.py
│ └── module.py
└── tests/
└── test_module.py
更简单但:
- 可以不安装就导入包
- 对库来说不够专业
模式3:多包项目
project/
├── pyproject.toml
├── packages/
│ ├── package-a/
│ │ └── src/
│ │ └── package_a/
│ └── package-b/
│ └── src/
│ └── package_b/
└── tests/
完整pyproject.toml示例
模式4:全功能pyproject.toml
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "my-awesome-package"
version = "1.0.0"
description = "一个很棒的Python包"
readme = "README.md"
requires-python = ">=3.8"
license = {text = "MIT"}
authors = [
{name = "你的名字", email = "you@example.com"},
]
maintainers = [
{name = "维护者名字", email = "maintainer@example.com"},
]
keywords = ["example", "package", "awesome"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"requests>=2.28.0,<3.0.0",
"click>=8.0.0",
"pydantic>=2.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
"black>=23.0.0",
"ruff>=0.1.0",
"mypy>=1.0.0",
]
docs = [
"sphinx>=5.0.0",
"sphinx-rtd-theme>=1.0.0",
]
all = [
"my-awesome-package[dev,docs]",
]
[project.urls]
Homepage = "https://github.com/username/my-awesome-package"
Documentation = "https://my-awesome-package.readthedocs.io"
Repository = "https://github.com/username/my-awesome-package"
"Bug Tracker" = "https://github.com/username/my-awesome-package/issues"
Changelog = "https://github.com/username/my-awesome-package/blob/main/CHANGELOG.md"
[project.scripts]
my-cli = "my_package.cli:main"
awesome-tool = "my_package.tools:run"
[project.entry-points."my_package.plugins"]
plugin1 = "my_package.plugins:plugin1"
[tool.setuptools]
package-dir = {"" = "src"}
zip-safe = false
[tool.setuptools.packages.find]
where = ["src"]
include = ["my_package*"]
exclude = ["tests*"]
[tool.setuptools.package-data]
my_package = ["py.typed", "*.pyi", "data/*.json"]
# Black配置
[tool.black]
line-length = 100
target-version = ["py38", "py39", "py310", "py311"]
include = '\.pyi?$'
# Ruff配置
[tool.ruff]
line-length = 100
target-version = "py38"
[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP"]
# MyPy配置
[tool.mypy]
python_version = "3.8"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
# Pytest配置
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
addopts = "-v --cov=my_package --cov-report=term-missing"
# 覆盖率配置
[tool.coverage.run]
source = ["src"]
omit = ["*/tests/*"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise AssertionError",
"raise NotImplementedError",
]
模式5:动态版本控制
[build-system]
requires = ["setuptools>=61.0", "setuptools-scm>=8.0"]
build-backend = "setuptools.build_meta"
[project]
name = "my-package"
dynamic = ["version"]
description = "带有动态版本的包"
[tool.setuptools.dynamic]
version = {attr = "my_package.__version__"}
# 或使用setuptools-scm进行基于git的版本控制
[tool.setuptools_scm]
write_to = "src/my_package/_version.py"
在__init__.py中:
# src/my_package/__init__.py
__version__ = "1.0.0"
# 或使用setuptools-scm
from importlib.metadata import version
__version__ = version("my-package")
命令行界面(CLI)模式
模式6:使用Click的CLI
# src/my_package/cli.py
import click
@click.group()
@click.version_option()
def cli():
"""我的很棒CLI工具。"""
pass
@cli.command()
@click.argument("name")
@click.option("--greeting", default="Hello", help="使用的问候语")
def greet(name: str, greeting: str):
"""问候某人。"""
click.echo(f"{greeting}, {name}!")
@cli.command()
@click.option("--count", default=1, help="重复次数")
def repeat(count: int):
"""重复消息。"""
for i in range(count):
click.echo(f"消息 {i + 1}")
def main():
"""CLI的入口点。"""
cli()
if __name__ == "__main__":
main()
在pyproject.toml中注册:
[project.scripts]
my-tool = "my_package.cli:main"
用法:
pip install -e .
my-tool greet World
my-tool greet Alice --greeting="Hi"
my-tool repeat --count=3
模式7:使用argparse的CLI
# src/my_package/cli.py
import argparse
import sys
def main():
"""主CLI入口点。"""
parser = argparse.ArgumentParser(
description="我的很棒工具",
prog="my-tool"
)
parser.add_argument(
"--version",
action="version",
version="%(prog)s 1.0.0"
)
subparsers = parser.add_subparsers(dest="command", help="命令")
# 添加子命令
process_parser = subparsers.add_parser("process", help="处理数据")
process_parser.add_argument("input_file", help="输入文件路径")
process_parser.add_argument(
"--output", "-o",
default="output.txt",
help="输出文件路径"
)
args = parser.parse_args()
if args.command == "process":
process_data(args.input_file, args.output)
else:
parser.print_help()
sys.exit(1)
def process_data(input_file: str, output_file: str):
"""从输入到输出处理数据。"""
print(f"处理 {input_file} -> {output_file}")
if __name__ == "__main__":
main()
构建和发布
模式8:本地构建包
# 安装构建工具
pip install build twine
# 构建分发
python -m build
# 这会创建:
# dist/
# my-package-1.0.0.tar.gz (源分发)
# my_package-1.0.0-py3-none-any.whl (wheel)
# 检查分发
twine check dist/*
模式9:发布到PyPI
# 安装发布工具
pip install twine
# 首先在TestPyPI上测试
twine upload --repository testpypi dist/*
# 从TestPyPI安装以测试
pip install --index-url https://test.pypi.org/simple/ my-package
# 如果一切正常,发布到PyPI
twine upload dist/*
使用API令牌(推荐):
# 创建 ~/.pypirc
[distutils]
index-servers =
pypi
testpypi
[pypi]
username = __token__
password = pypi-...your-token...
[testpypi]
username = __token__
password = pypi-...your-test-token...
模式10:使用GitHub Actions自动发布
# .github/workflows/publish.yml
name: 发布到PyPI
on:
release:
types: [created]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: 设置Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: 安装依赖
run: |
pip install build twine
- name: 构建包
run: python -m build
- name: 检查包
run: twine check dist/*
- name: 发布到PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: twine upload dist/*
高级模式
模式11:包含数据文件
[tool.setuptools.package-data]
my_package = [
"data/*.json",
"templates/*.html",
"static/css/*.css",
"py.typed",
]
访问数据文件:
# src/my_package/loader.py
from importlib.resources import files
import json
def load_config():
"""从包数据加载配置。"""
config_file = files("my_package").joinpath("data/config.json")
with config_file.open() as f:
return json.load(f)
# Python 3.9+
from importlib.resources import files
data = files("my_package").joinpath("data/file.txt").read_text()
模式12:命名空间包
适用于跨多个仓库拆分的大型项目:
# 包1: company-core
company/
└── core/
├── __init__.py
└── models.py
# 包2: company-api
company/
└── api/
├── __init__.py
└── routes.py
不要在命名空间目录(company/)中包含 init.py:
# company-core/pyproject.toml
[project]
name = "company-core"
[tool.setuptools.packages.find]
where = ["."]
include = ["company.core*"]
# company-api/pyproject.toml
[project]
name = "company-api"
[tool.setuptools.packages.find]
where = ["."]
include = ["company.api*"]
用法:
# 两个包可以在相同命名空间下导入
from company.core import models
from company.api import routes
模式13:C扩展
[build-system]
requires = ["setuptools>=61.0", "wheel", "Cython>=0.29"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
ext-modules = [
{name = "my_package.fast_module", sources = ["src/fast_module.c"]},
]
# setup.py
from setuptools import setup, Extension
setup(
ext_modules=[
Extension(
"my_package.fast_module",
sources=["src/fast_module.c"],
include_dirs=["src/include"],
)
]
)
版本管理
模式14:语义化版本控制
# src/my_package/__init__.py
__version__ = "1.2.3"
# 语义化版本控制: 主版本.次版本.修订版本
# 主版本: 破坏性更改
# 次版本: 新功能(向后兼容)
# 修订版本: 错误修复
依赖中的版本约束:
dependencies = [
"requests>=2.28.0,<3.0.0", # 兼容范围
"click~=8.1.0", # 兼容发布 (~= 8.1.0 表示 >=8.1.0,<8.2.0)
"pydantic>=2.0", # 最低版本
"numpy==1.24.3", # 精确版本(尽可能避免)
]
模式15:基于Git的版本控制
[build-system]
requires = ["setuptools>=61.0", "setuptools-scm>=8.0"]
build-backend = "setuptools.build_meta"
[project]
name = "my-package"
dynamic = ["version"]
[tool.setuptools_scm]
write_to = "src/my_package/_version.py"
version_scheme = "post-release"
local_scheme = "dirty-tag"
创建版本如:
1.0.0(来自git标签)1.0.1.dev3+g1234567(标签后3次提交)
测试安装
模式16:可编辑安装
# 以开发模式安装
pip install -e .
# 带有可选依赖
pip install -e ".[dev]"
pip install -e ".[dev,docs]"
# 现在对源代码的更改会立即反映
模式17:在隔离环境中测试
# 创建虚拟环境
python -m venv test-env
source test-env/bin/activate # Linux/Mac
# test-env\Scripts\activate # Windows
# 安装包
pip install dist/my_package-1.0.0-py3-none-any.whl
# 测试是否工作
python -c "import my_package; print(my_package.__version__)"
# 测试CLI
my-tool --help
# 清理
deactivate
rm -rf test-env
文档
模式18:README.md模板
# 我的包
[](https://pypi.org/project/my-package/)
[](https://pypi.org/project/my-package/)
[](https://github.com/username/my-package/actions)
你的包的简要描述。
## 安装
```bash
pip install my-package
快速开始
from my_package import something
result = something.do_stuff()
特性
- 特性1
- 特性2
- 特性3
文档
完整文档: https://my-package.readthedocs.io
开发
git clone https://github.com/username/my-package.git
cd my-package
pip install -e ".[dev]"
pytest
许可证
MIT
## 常见模式
### 模式19:多架构Wheel
```yaml
# .github/workflows/wheels.yml
name: 构建Wheel
on: [push, pull_request]
jobs:
build_wheels:
name: 在 ${{ matrix.os }} 上构建Wheel
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v3
- name: 构建Wheel
uses: pypa/cibuildwheel@v2.16.2
- uses: actions/upload-artifact@v3
with:
path: ./wheelhouse/*.whl
模式20:私有包索引
# 从私有索引安装
pip install my-package --index-url https://private.pypi.org/simple/
# 或添加到pip.conf
[global]
index-url = https://private.pypi.org/simple/
extra-index-url = https://pypi.org/simple/
# 上传到私有索引
twine upload --repository-url https://private.pypi.org/ dist/*
文件模板
Python包的.gitignore
# 构建工件
build/
dist/
*.egg-info/
*.egg
.eggs/
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
# 虚拟环境
venv/
env/
ENV/
# IDE
.vscode/
.idea/
*.swp
# 测试
.pytest_cache/
.coverage
htmlcov/
# 分发
*.whl
*.tar.gz
MANIFEST.in
# MANIFEST.in
include README.md
include LICENSE
include pyproject.toml
recursive-include src/my_package/data *.json
recursive-include src/my_package/templates *.html
recursive-exclude * __pycache__
recursive-exclude * *.py[co]
发布清单
- [ ] 代码已测试(pytest通过)
- [ ] 文档完整(README、文档字符串)
- [ ] 版本号已更新
- [ ] CHANGELOG.md已更新
- [ ] 许可证文件包含
- [ ] pyproject.toml完整
- [ ] 包构建无错误
- [ ] 在干净环境中测试安装
- [ ] CLI工具工作(如果适用)
- [ ] PyPI元数据正确(分类器、关键词)
- [ ] GitHub仓库已链接
- [ ] 先在TestPyPI上测试
- [ ] 为发布创建Git标签
资源
- Python打包指南: https://packaging.python.org/
- PyPI: https://pypi.org/
- TestPyPI: https://test.pypi.org/
- setuptools文档: https://setuptools.pypa.io/
- build: https://pypa-build.readthedocs.io/
- twine: https://twine.readthedocs.io/
最佳实践总结
- 使用src/布局 以获得更清晰的包结构
- 使用pyproject.toml 用于现代打包
- 固定构建依赖 在build-system.requires中
- 适当版本控制 使用语义化版本控制
- 包含所有元数据(分类器、URL等)
- 在干净环境中测试安装
- 在发布到PyPI前使用TestPyPI
- 用README和文档字符串彻底文档化
- 包含LICENSE文件
- 使用CI/CD自动化发布