第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 . 安装,修改代码后立即生效

好了,这一章的内容就到这里。下一章我们会聊聊项目的目录结构设计,到时候我会分享一些我在大厂项目里看到的优秀实践。嗯,先消化一下今天的内容,动手搭一个自己的骨架试试看吧。