第4章 包管理基础:setup.py与pyproject.toml
说到Python项目的包管理,很多新手第一反应就是「装个依赖呗」。嗯,这话没错,但只对了一半。真正要把一个项目打包、发布、让别人能方便地用起来,你得理解两个核心文件:setup.py 和 pyproject.toml。
我个人习惯把这两个文件比作项目的「身份证」和「说明书」。身份证告诉别人你是谁(元数据),说明书告诉别人怎么用你(入口点、依赖等)。今天我们就来聊聊怎么把这套东西搭起来。
4.1 从setup.py说起
先说说 setup.py。这是Python打包的老牌方式,从setuptools时代就开始用了。说白了,它就是一个Python脚本,调用 setuptools.setup() 函数来定义项目的各种信息。
一个最简的 setup.py 长这样:
from setuptools import setup, find_packages
setup(
name="my-project",
version="0.1.0",
packages=find_packages(),
install_requires=[
"requests>=2.25.0",
"click>=8.0.0",
],
)
你看,就这几行,项目名、版本、包含哪些包、依赖什么库,全交代清楚了。我在项目中遇到过不少团队,连 find_packages() 都懒得用,直接手写包列表。结果项目一重构,包路径变了,安装就炸了。所以我的建议是:永远用 find_packages(),省心。
4.2 包的元数据定义
元数据是什么?就是描述你项目的信息。比如作者、邮箱、项目主页、许可证等等。这些信息看着不起眼,但发布到PyPI上时,别人就是靠这些来了解你的项目的。
我整理了一份常用的元数据字段表,你可以直接照着填:
| 字段名 | 说明 | 示例 |
|---|---|---|
name |
项目名称,PyPI上唯一 | "my-awesome-tool" |
version |
版本号,建议遵循语义化版本 | "0.1.0" |
author |
作者姓名 | "张三" |
author_email |
作者邮箱 | "zhangsan@example.com" |
description |
简短描述(一句话) | "一个命令行工具" |
long_description |
详细描述,通常从README读取 | open("README.md").read() |
url |
项目主页或仓库地址 | "https://github.com/..." |
license |
许可证类型 | "MIT" |
classifiers |
分类标签,帮助用户搜索 | ["Programming Language :: Python :: 3"] |
long_description 直接从 README.md 读进来,这样文档只用维护一份。但要注意,PyPI默认用reStructuredText格式,如果你用Markdown,记得加一行 long_description_content_type="text/markdown"。
4.3 入口点(entry_points)配置
这是我觉得最实用的功能之一。入口点说白了就是:把你的Python函数暴露成命令行工具。你想想看,用户装完你的包,直接在终端敲个命令就能用,多爽。
配置方式很简单,在 setup.py 里加一个 entry_points 参数:
setup(
name="my-cli",
version="0.1.0",
packages=find_packages(),
entry_points={
"console_scripts": [
"mycli = my_cli.main:cli",
],
},
)
这段代码的意思是:用户安装后,在终端输入 mycli,就会执行 my_cli/main.py 文件里的 cli 函数。我曾经见过有人把入口点指向了一个类方法,结果运行时各种报错。记住:入口点只能指向可调用对象,通常是函数。
如果你用Click或Typer这类库,入口点配置就更简单了。比如用Click:
# my_cli/main.py
import click
@click.command()
@click.option("--name", default="World")
def cli(name):
click.echo(f"Hello, {name}!")
if __name__ == "__main__":
cli()
然后用户装完包,直接 mycli --name Python 就能看到输出。嗯,这里要注意:入口点的名字不要和系统命令冲突,比如别叫 ls 或 cd,否则会覆盖系统命令,后果很严重。
4.4 新时代的pyproject.toml
从Python 3.11开始,pyproject.toml 成了官方推荐的打包配置方式。它用TOML格式,比 setup.py 更简洁、更声明式。说白了,就是把原来写在Python代码里的配置,改成写在配置文件里。
一个典型的 pyproject.toml 长这样:
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "my-project"
version = "0.1.0"
description = "一个示例项目"
readme = "README.md"
license = {text = "MIT"}
authors = [
{name = "张三", email = "zhangsan@example.com"},
]
dependencies = [
"requests>=2.25.0",
"click>=8.0.0",
]
[project.scripts]
mycli = "my_cli.main:cli"
你看,结构清晰多了。而且 [project.scripts] 就是入口点的配置,比 entry_points 更直观。我个人现在新项目都优先用 pyproject.toml,除非要兼容特别老的Python版本。
setup.py 删了换成 pyproject.toml,结果CI/CD脚本里还在用 python setup.py install,直接报错。所以迁移时记得检查所有构建脚本。
4.5 如何选择?
你可能会问:到底用 setup.py 还是 pyproject.toml?我的建议是:
- 新项目:直接用
pyproject.toml,这是未来趋势 - 旧项目:如果
setup.py跑得好好的,没必要急着改 - 需要动态逻辑:比如版本号从Git标签生成,那就保留
setup.py
其实两者可以共存。你可以在 pyproject.toml 里定义大部分配置,然后用 setup.py 处理动态部分。但要注意,setup.py 里的配置会覆盖 pyproject.toml 里的同名配置。
4.6 实战:构建一个最小骨架
最后,我们动手搭一个最小的项目骨架。假设项目叫 mycli,结构如下:
mycli/
├── pyproject.toml
├── README.md
└── src/
└── mycli/
├── __init__.py
└── main.py
pyproject.toml 内容:
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "mycli"
version = "0.1.0"
description = "一个命令行工具"
readme = "README.md"
license = {text = "MIT"}
authors = [
{name = "张三", email = "zhangsan@example.com"},
]
dependencies = [
"click>=8.0.0",
]
[project.scripts]
mycli = "mycli.main:cli"
[tool.setuptools.packages.find]
where = ["src"]
src/mycli/main.py 内容:
import click
@click.command()
@click.option("--name", default="World", help="你的名字")
def cli(name):
"""一个简单的问候命令"""
click.echo(f"Hello, {name}!")
if __name__ == "__main__":
cli()
然后执行 pip install -e . 安装到开发环境,就能在终端用 mycli --name Python 了。是不是很简单?
setup.py和pyproject.toml都是定义项目元数据和依赖的方式- 入口点
entry_points或[project.scripts]能把函数暴露成命令行工具 - 新项目优先用
pyproject.toml,旧项目按需迁移 - 开发时用
pip install -e .安装,修改代码后立即生效
好了,这一章的内容就到这里。下一章我们会聊聊项目的目录结构设计,到时候我会分享一些我在大厂项目里看到的优秀实践。嗯,先消化一下今天的内容,动手搭一个自己的骨架试试看吧。