在Python生态中,PyPI(Python Package Index)就像是一个巨大的工具箱,里面有超过40万个现成的工具供我们使用。但当我们自己造出了一个好用的工具时,如何把它放进这个工具箱让其他人也能使用呢?这就是Python打包技术要解决的问题。
我仍然记得第一次成功上传自己开发的包到PyPI时的兴奋感——那种"我的代码现在可以被任何人通过pip install直接使用了"的成就感,是纯写代码无法比拟的。更重要的是,掌握打包技能让我在团队协作和开源贡献中获得了更多机会。
打包看似简单,实则暗藏玄机。我见过不少开发者卡在奇怪的构建错误上,也见过一些包因为错误的配置导致依赖地狱。这份指南将带你避开这些陷阱,从零开始构建一个专业的PyPI包。
一个规范的Python包应该具有这样的目录结构:
code复制my_awesome_package/
├── my_awesome_package/ # 主包目录
│ ├── __init__.py # 包初始化文件
│ ├── module1.py # 模块文件
│ └── module2.py
├── tests/ # 测试目录
│ ├── __init__.py
│ └── test_module1.py
├── docs/ # 文档目录
├── README.md # 项目说明
├── pyproject.toml # 构建系统配置
└── setup.cfg # 包元数据(可选)
关键提示:包目录名(内部的my_awesome_package)应该与你想让用户导入的名称一致。比如用户会通过
import my_awesome_package来使用你的包。
这是现代Python打包的核心配置文件,取代了传统的setup.py。一个完整的配置示例:
toml复制[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "my-awesome-package"
version = "0.1.0"
authors = [
{name = "Your Name", email = "your.email@example.com"},
]
description = "A package that does awesome things"
readme = "README.md"
requires-python = ">=3.7"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
[project.urls]
Homepage = "https://github.com/you/my_awesome_package"
Documentation = "https://my-awesome-package.readthedocs.io"
在开发过程中,我们可以使用可编辑安装模式,这样对代码的修改会立即生效:
bash复制pip install -e .
这个命令会创建一个指向你项目目录的符号链接,而不是复制文件到site-packages。我强烈建议在开发任何包时都使用这种方式,它能节省大量调试时间。
__init__.py文件决定了包的公共API。一个好的实践是显式导出公共接口:
python复制# my_awesome_package/__init__.py
from .module1 import AwesomeClass
from .module2 import useful_function
__all__ = ['AwesomeClass', 'useful_function']
__version__ = '0.1.0'
这样用户就可以直接从包顶层导入所需内容,而不需要知道内部模块结构。
现代Python打包工具链使得构建过程非常简单:
bash复制python -m pip install --upgrade build
python -m build
这个命令会生成两个关键文件:
dist/my_awesome_package-0.1.0.tar.gz - 源码分发dist/my_awesome_package-0.1.0-py3-none-any.whl - 构建的分发常见错误:构建前务必确保版本号已更新。我习惯在每次发布前都检查三遍版本号,因为发布后就不能修改了。
首先确保你已注册PyPI账号并配置了API token:
bash复制pip install twine
twine upload dist/*
系统会提示输入用户名和密码(使用__token__作为用户名,API token作为密码)。
我强烈建议先上传到测试PyPI进行验证:
bash复制twine upload --repository testpypi dist/*
这样可以避免污染主PyPI仓库,特别是当你还在学习打包流程时。
在pyproject.toml中正确声明依赖关系至关重要:
toml复制[project]
dependencies = [
"requests>=2.25.0",
"numpy>=1.20.0; python_version >= '3.7'",
"pandas>=1.2.0; python_version >= '3.8'",
]
对于可选依赖(extras):
toml复制[project.optional-dependencies]
test = ["pytest>=6.0", "pytest-cov"]
dev = ["black", "flake8", "mypy"]
all = ["matplotlib", "seaborn"]
用户可以通过pip install package[test,dev]安装这些额外依赖。
如果你的包需要包含数据文件(如JSON、CSV等),需要在pyproject.toml中添加:
toml复制[tool.setuptools]
include-package-data = true
[tool.setuptools.packages.find]
where = ["."] # 从项目根目录开始查找
然后在MANIFEST.in文件中指定要包含的文件模式:
code复制include README.md
recursive-include my_awesome_package/data *.json
recursive-include docs *.md
遵循语义化版本(SemVer)规范:
我推荐使用bump2version工具自动化版本管理:
bash复制pip install bump2version
bump2version patch # 或 minor/major
在pyproject.toml中正确声明Python版本要求:
toml复制requires-python = ">=3.7"
对于特定版本的依赖:
toml复制dependencies = [
"numpy>=1.20.0; python_version >= '3.7'",
"pandas>=1.2.0; python_version < '3.8'",
]
一个完整的测试配置应该包括:
toml复制[project.optional-dependencies]
test = [
"pytest>=6.0",
"pytest-cov",
"pytest-mock",
"hypothesis",
]
[tool.pytest.ini_options]
python_files = "test_*.py"
addopts = "--cov=my_awesome_package --cov-report=term-missing"
创建.github/workflows/test.yml实现CI:
yaml复制name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .[test]
- name: Run tests
run: |
pytest
一个好的README应该包含:
我推荐使用这些徽章代码(替换相应URL):
markdown复制[](https://pypi.org/project/my-awesome-package/)
[](https://pypi.org/project/my-awesome-package/)
[](https://github.com/you/my_awesome_package/actions)
使用Sphinx生成专业文档:
bash复制pip install sphinx furo
bash复制sphinx-quickstart docs --sep -p "My Awesome Package" -a "Your Name" -v 0.1.0
docs/conf.py:python复制extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.viewcode',
'sphinx.ext.napoleon'
]
html_theme = 'furo'
rst复制.. automodule:: my_awesome_package.module1
:members:
错误: "invalid command 'bdist_wheel'"
解决方案:
bash复制pip install wheel
错误: "ModuleNotFoundError during build"
通常是因为构建环境缺少依赖。确保在构建前安装所有构建依赖:
bash复制pip install -r pyproject.toml中build-system.requires列出的依赖
一旦发布了一个版本,就不能修改或删除它(只能标记为yanked)。如果发现严重问题:
bash复制twine upload --skip-existing --repository-url https://upload.pypi.org/legacy/ -y dist/*
定期检查依赖中的已知漏洞:
bash复制pip install safety
safety check
或者集成到CI中:
yaml复制- name: Security check
run: |
pip install safety
safety check
当需要弃用某个功能时,使用warnings模块:
python复制import warnings
def old_function():
warnings.warn(
"old_function is deprecated and will be removed in v2.0. Use new_function instead.",
DeprecationWarning,
stacklevel=2
)
# 原有实现...
在文档和发布说明中明确标记弃用内容,并给出迁移指南。
掌握Python打包技术就像获得了代码共享的通行证。从个人经验来看,最常犯的错误往往是最基础的——忘记更新版本号、错误的依赖声明、不完整的文件包含。建议在第一次发布前,找一个有经验的开发者review你的配置。