摘要
在 Go 语言的世界里,发布你的项目非常简单——只要将代码上传到 GitHub,其他开发者就可以直接通过 go get 命令将其拉取并使用。这种“即托即用”的体验极大地促进了 Go 生态的活跃发展。
但对于 Python 开发者来说,仅仅将代码放在 GitHub 上还远远不够。如果你希望其他人可以通过 pip install your-package-name 的方式便捷地使用你的项目,就必须将它发布到 PyPI(Python Package Index)——这是 Python 官方推荐的公共包管理平台,所有优秀的第三方库几乎都托管于此。
本篇文章将带你完整体验一次 Python 包从本地开发到成功上传 PyPI 的全过程。我们将一步步梳理项目结构、配置 pyproject.toml、生成发布文件、注册 PyPI 账号并最终上传你的项目。无论你是第一次尝试发布,还是之前总在某些环节卡壳,这篇文章都能帮你扫清障碍,让你的代码真正面向全世界开放。
是时候让你的 Python 项目登上 PyPI 舞台,迈出开源分享的重要一步了!
1. 注册一个PyPI账号
点击连接:https://pypi.org/manage/projects/ 注册一个PyPI账号:

同时,我们也得遵循官方的准则:
- 不得将 PyPI 用于任何非法或有害的活动。
- 不得冒充他人,或在未经允许的情况下发布他人隐私信息。
- 请尊重其他用户,避免使用辱骂性或歧视性语言。
- 禁止发布垃圾信息或传播恶意软件。
- 不得利用 PyPI 进行安全研究或漏洞测试。
注册好后,官方会向你的邮箱发一个激活邮件,点击进入激活即可!
登录进入账号设置页面后,官方要求配置多因子认证,一步步配置即可!
2. 新建一个项目
我们任意新建一个项目:

可以参加官网给出的案例:
packaging_tutorial/
├── LICENSE
├── pyproject.toml
├── README.md
├── src/
│ └── example_package_YOUR_USERNAME_HERE/
│ ├── __init__.py
│ └── example.py
└── tests/
3. 选择构建后端
像 pip 和 build 这样的工具本身并不会将你的源码打包成分发格式(如 .whl 轮子包);这个工作实际上是由 构建后端(build backend) 完成的。构建后端决定了你的项目如何指定构建配置,包括元数据(例如项目名称、PyPI 上展示的标签等)以及输入文件的组织方式。
不同的构建后端功能有所差异,例如是否支持构建扩展模块等。因此,你应该根据自己的需求和偏好选择合适的构建后端。
你可以从多个构建后端中进行选择;本教程默认使用 Setuptools,但同样适用于支持 [project] 元数据配置表的其他后端,比如 ** Hatchling**、Flit、PDM 等。
注意
有些构建后端是更大型工具的一部分,通常会提供命令行界面,并具备更多功能,如项目初始化、版本管理,以及打包、上传和安装等。本教程使用的是专注于某一用途的独立工具,它们彼此之间互不依赖。
你的 pyproject.toml 文件会告诉诸如 pip 和 build 这样的前端工具该使用哪个构建后端来处理你的项目。
🛠 构建后端功能对比:Setuptools、Flit、PDM、Hatchling
| 特性 / 工具 | Setuptools | Flit | PDM | Hatchling |
|---|---|---|---|---|
| 🧬 定位 | 传统构建工具,兼容性最强 | 轻量纯 Python 包打包工具 | 现代项目管理 + 构建工具 | 现代构建后端,Hatch 的一部分 |
| 📦 包类型 | 纯 Python + C 扩展 | 纯 Python 包(不支持扩展) | 支持纯 Python 和扩展模块 | 主要用于纯 Python 包(支持扩展通过插件) |
| 🧾 配置方式 | setup.py / setup.cfg / pyproject.toml | 仅 pyproject.toml | 仅 pyproject.toml | 仅 pyproject.toml |
| ⚙️ 构建命令 | python setup.py bdist_wheel | flit build / flit publish | pdm build / pdm publish | hatch build / hatch publish |
| 🔄 依赖管理 | 外部工具(如 pip、pip-tools) | 无集成 | 内建依赖管理(类 Poetry) | 通常配合 Hatch 工具使用 |
| 🧩 扩展能力 | 强(插件丰富,自定义能力强) | 弱(专注于简单) | 中(支持插件) | 中(支持插件,如 Cython 插件) |
| 🏗 打包速度 | 中等 | 快 | 快 | 快 |
| 📤 上传 PyPI | twine upload | flit publish | pdm publish | hatch publish |
| 👤 目标用户 | 兼容老项目的维护者,需要 C 扩展支持者 | 初学者、轻量包作者 | 喜欢统一管理体验的现代 Python 用户 | 喜欢现代配置风格、使用 Hatch 工具链者 |
| 🧰 工具生态 | pip、twine、tox 等 | flit 自带基本命令 | 集成开发全流程(依赖 + 构建 + 发布) | Hatch 工具链生态 |
总结推荐:
| 你是谁? | 推荐后端 | 原因说明 |
|---|---|---|
| 👴 维护老项目,项目包含扩展模块、兼容性优先 | Setuptools | 最通用、最兼容,虽然配置复杂但功能最全 |
| 🧑🎓 初学者/轻量项目作者,想快速发布纯 Python 包 | Flit | 几乎零配置,命令简单,快速构建和发布 |
| 🧑💻 想管理项目全生命周期(依赖 + 构建 + 发布) | PDM | 类似 JavaScript 的开发体验,支持虚拟环境、版本锁定等 |
| 🧪 喜欢现代化构建工具链,项目已用 Hatch | Hatchling | 构建快,配置清晰,尤其适合纯 Python 项目;若已用 Hatch 工具效果更佳 |
- 如果你只是想打包一个简单 Python 项目,并上传 PyPI,Flit 和 Hatchling 是最省心的选择。
- 如果你已经习惯使用 Hatch 管理项目,那就选 Hatchling。
- 想要像
npm那样“一站式管理项目”且兼顾虚拟环境?选 PDM。 - 需要最大兼容性、构建扩展模块或维护旧项目?首选 Setuptools。
创建pyproject.toml文件
[build-system]
requires = ["setuptools >= 77.0.3"]
build-backend = "setuptools.build_meta"
requires 键表示构建你的包时所需的依赖包列表。构建前端工具会在构建过程中自动安装这些依赖。由于前端工具通常会在隔离环境中执行构建过程,如果遗漏了这些依赖,可能会导致构建时出错。这个列表中必须包含你所使用的构建后端本身的包,也可能包括其他构建时所需的依赖。
上面示例中指定的最小版本,是因为该版本开始支持新的 license 元数据字段。
build-backend 键指定的是一个 Python 对象的名称,前端工具会通过它来实际执行构建操作。
这两个字段的值通常由你所使用的构建后端的文档提供,或者通过其命令行工具自动生成。你通常不需要手动修改这些设置。
构建工具的额外配置,要么写在 pyproject.toml 的 [tool.xxx] 区块中,要么写在构建工具定义的专用配置文件中。例如,当你使用 setuptools 作为构建后端时,还可以将附加配置写入 setup.py 或 setup.cfg 文件中,而在 build-backend 中指定 setuptools.build_meta 后,工具会自动查找并使用这些配置文件。
现在我们来配置元数据:
打开你的 pyproject.toml 文件,填写以下内容。请将 name 字段中的 YOUR_USERNAME_HERE 替换为你的用户名,以确保你的包名具有唯一性,不会与其他同样跟着本教程操作的用户上传的包发生冲突。
[project]
name = "example_package_YOUR_USERNAME_HERE"
version = "0.0.1"
authors = [
{ name="Example Author", email="author@example.com" },
]
description = "A small example package"
readme = "README.md"
requires-python = ">=3.9"
classifiers = [
"Programming Language :: Python :: 3",
"Operating System :: OS Independent",
]
license = "MIT"
license-files = ["LICEN[CS]E*"]
[project.urls]
Homepage = "https://github.com/pypa/sampleproject"
Issues = "https://github.com/pypa/sampleproject/issues"
字段说明:
-
name:这是你的包的分发名称。名称可以包含字母、数字、点 (
.)、下划线 (_) 和中划线 (-)。但它不能与 PyPI 上已有的项目重名。为避免冲突,建议在教程中加入你的用户名。 -
version:你的包的版本号。一些构建后端也允许你从文件或 Git 标签中获取版本信息。
-
authors:作者信息。你可以为每位作者提供姓名和邮箱地址。如果有维护者,也可以采用相同格式在
maintainers字段中列出。 -
description:对项目的一句简短描述。
-
readme:提供详细描述的文档路径,会显示在 PyPI 上的包详情页中。这里使用
README.md是非常常见的做法。如果你需要更复杂的格式,也可以参考 pyproject.toml 的官方指南。 -
requires-python:指定你的项目支持的 Python 版本。安装器(如 pip)会根据这个字段自动回溯查找符合条件的包版本。
-
classifiers:提供给索引系统(如 PyPI)和 pip 的额外元信息。在本例中,表示此包仅支持 Python 3,并且与操作系统无关。你应该至少指定 Python 版本和支持的操作系统。完整列表请参考:
👉 https://pypi.org/classifiers/ -
license:你的包所使用的 SPDX 许可证标识符(如 MIT、Apache-2.0)。
-
license-files:指定许可证文件的匹配路径(使用 glob 语法),路径是相对于
pyproject.toml所在目录。 -
urls:你可以在这里列出任意数量的链接,这些链接会显示在 PyPI 页面上。一般包括源码地址、文档、问题跟踪页面等。
更多关于 [project] 表中的字段定义,请参阅 pyproject.toml 指南(英文)。
常见的额外字段还包括 keywords(用于改进搜索可见性)和 dependencies(定义安装时所需依赖)。
举例:
[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "pyelasticdsl"
version = "0.1.0"
description = "A Python DSL for Elasticsearch queries"
readme = "README.md"
requires-python = ">=3.12"
license = { text = "MIT" }
keywords = ["elasticsearch", "dsl", "query", "python"]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent"
]
dependencies = [
]
[project.urls]
"Homepage" = "https://github.com/AlaricZy/pyelasticdsl"
"Repository" = "https://github.com/AlaricZy/pyelasticdsl"
requires 和 dependencies的区别
1. [build-system] requires
- 作用时机:构建时依赖(Build-time dependencies)
- 含义:告诉打包工具(比如
pip或build命令)在“打包你的项目”时,需要先安装哪些工具或库,否则无法构建出安装包。 - 举例:
["setuptools >= 77.0.3"]就是告诉系统,构建你的包时必须用setuptools77.0.3 及以上版本,否则构建失败。 - 写在:
pyproject.toml的[build-system]区块里。
2. [project].dependencies
- 作用时机:运行时依赖(Run-time dependencies)
- 含义:告诉安装你的包的用户,安装时还需要额外安装哪些库,这些是你的包运行时必须依赖的第三方库。
- 举例:
dependencies = ["requests", "numpy >= 1.21"]表示用户安装你的包时,也要自动安装requests和版本大于等于 1.21 的numpy。 - 写在:
pyproject.toml的[project]区块里。
| 特性 | [build-system] requires | [project].dependencies |
|---|---|---|
| 依赖类型 | 构建时依赖(打包工具依赖) | 运行时依赖(你的包运行时依赖) |
| 作用对象 | 构建工具(pip、build 等) | 你的包的使用者 |
| 依赖安装时间 | 构建包时自动安装 | 用户安装你的包时自动安装 |
| 示例 | ["setuptools >= 77.0.3"] | ["requests", "numpy >= 1.21"] |
4. 创建README和LICENSE(可选)
README文件可以由大模型生成,LICENSE和.gitignore可以在github创建仓库时选择,所以这两个文件基本不会用户操心!
5. 生成分发包
下一步是为你的包生成分发包(distribution packages)。这些归档文件将被上传到 Python 软件包索引(PyPI),并且可以通过 pip 进行安装。
请确保你安装了最新版本的 PyPA 的 build 工具:
Unix/macOS
python3 -m pip install --upgrade build
Windows
py -m pip install --upgrade build
接下来,在包含 pyproject.toml 文件的目录下运行以下命令:
Unix/macOS
python3 -m build
Windows
py -m build
该命令会输出大量信息,执行完成后会在 dist 目录下生成两个文件:
dist/
├── example_package_YOUR_USERNAME_HERE-0.0.1-py3-none-any.whl
└── example_package_YOUR_USERNAME_HERE-0.0.1.tar.gz
.tar.gz文件是源码分发包(source distribution).whl文件是构建好的二进制分发包(built distribution)
新版的 pip 优先安装二进制分发包,但在必要时会回退安装源码包。你应当始终上传源码分发包,并针对项目兼容的平台提供构建好的二进制分发包。
6. 上传分发包
最后,是时候将你的包上传到 Python 软件包索引(PyPI)了!
首先,你需要在 TestPyPI或PyPI注册一个账户。TestPyPI 是 PyPI 的一个独立测试实例,专门用于测试和试验。
要注册账户,请访问:https://test.pypi.org/account/register/ 并按照页面指示完成注册流程。你还需要验证邮箱,才能上传包。
为了安全地上传项目,你需要一个 PyPI API 令牌(API token)。
在这里创建令牌:https://test.pypi.org/manage/account/#api-tokens 设置“Scope”为“Entire account”。
请务必在关闭页面前复制并保存好令牌——令牌只会显示一次。

注册完成后,你可以使用 twine 工具上传分发包。先安装 Twine:
Unix/macOS
pip install --upgrade twine
Windows
pip install --upgrade twine
安装完成后,运行 Twine 将 dist 目录下的所有包上传到 TestPyPI:
Unix/macOS
twine upload --repository testpypi dist/*
Windows
twine upload --repository testpypi dist/*
或者也可以选择直接上传到PyPI:
twine upload dist/*
上传时系统会提示你输入 API 令牌(注意Token是不共用的),请输入包含 pypi- 前缀的完整令牌值。输入时内容不会显示,请务必正确粘贴。
命令执行成功后,你会看到类似如下输出:
Uploading distributions to https://test.pypi.org/legacy/
Enter your API token:
Uploading example_package_YOUR_USERNAME_HERE-0.0.1-py3-none-any.whl
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 8.2/8.2 kB • 00:01 • ?
Uploading example_package_YOUR_USERNAME_HERE-0.0.1.tar.gz
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.8/6.8 kB • 00:00 • ?

上传完成后,你可以在 TestPyPI 或PyPI 查看你的包,例如:
https://test.pypi.org/project/example_package_YOUR_USERNAME_HERE 。

这里的README是我自己用大模型生成的!
注意:
-
如果你直接执行
twine upload dist/*,twine 默认会把包上传到正式的 PyPI,因为默认仓库就是 PyPI。 -
如果你想上传到 TestPyPI,必须加参数
--repository testpypi
7. 安装你刚上传的包
你可以用 pip 安装你的包并验证它是否正常工作。建议先创建一个虚拟环境,然后从 TestPyPI 安装你的包:
Unix/macOS
python3 -m pip install --index-url https://test.pypi.org/simple/ --no-deps example-package-YOUR-USERNAME-HERE
Windows
py -m pip install --index-url https://test.pypi.org/simple/ --no-deps example-package-YOUR-USERNAME-HERE
请务必将包名中的
YOUR-USERNAME-HERE替换为你的用户名!
pip 会从 TestPyPI 下载并安装该包,输出大致如下:
Collecting example-package-YOUR-USERNAME-HERE
Downloading https://test-files.pythonhosted.org/packages/.../example_package_YOUR_USERNAME_HERE_0.0.1-py3-none-any.whl
Installing collected packages: example_package_YOUR_USERNAME_HERE
Successfully installed example_package_YOUR_USERNAME_HERE-0.0.1
注意
这里使用了--index-url标志指定了从 TestPyPI 安装,而非正式的 PyPI。同时加上了--no-deps参数,因为 TestPyPI 上的依赖包可能不全,安装依赖可能失败或安装到意料之外的版本。虽然我们的示例包没有依赖,使用--no-deps仍是测试时的好习惯。
安装完成后,你可以通过导入包来测试安装是否成功。确保仍处于虚拟环境中,然后启动 Python:
Unix/macOS
python3
Windows
py
然后导入并调用包里的函数:
from example_package_YOUR_USERNAME_HERE import example
example.add_one(2)
# 输出: 3
这样就确认你的包安装并运行正常了。
8. 尾声
恭喜你,已经成功打包并发布了一个 Python 项目!✨ 🍰 ✨
请记住,本教程演示的是如何将包上传到 TestPyPI,而 TestPyPI 并不是永久存储平台。测试环境会不定期删除包和账户,所以最好将 TestPyPI 用于测试和试验,就像本教程这样。
当你准备将真实包上传到正式的 Python 软件包索引(PyPI)时,操作步骤大致相同,但有几个重要区别:
-
为你的包选择一个容易记住且唯一的名称。你不必像教程中那样附加用户名,但不能使用已被占用的名称。
-
在 https://pypi.org 注册账户——注意,这与测试服务器是两个独立的服务器,测试服务器的登录信息不会与正式服务器共享。
-
使用
twine upload dist/*上传包,并输入你在正式 PyPI 注册账户的凭据。因为这是正式环境上传,所以不需要指定--repository参数,包会默认上传到 https://pypi.org/ 。 -
你可以通过运行
python3 -m pip install [your-package]从正式 PyPI 安装你的包。
如果你想深入学习 Python 包的打包,这里有一些建议:
-
阅读你选择的构建后端(Hatchling、setuptools、Flit、PDM)的高级配置说明。
-
浏览官方文档中更高级的实用指南,或者查看相关讨论,了解具体主题的解释和背景。
-
了解提供一站式命令行项目管理和打包工具,如 Hatch、Flit、PDM 和 Poetry。
技术上,你也可以创建没有 __init__.py 文件的 Python 包,这类称为命名空间包,是一个进阶话题。
传统包 vs 命名空间包
简单点说,就是一种允许包的代码分散在多个地方,但看起来像同一个包的机制。
传统包(Regular Packages)
- 你写一个包,比如
mypkg,它就是一个目录,里面必须有一个__init__.py文件(哪怕是空文件)。 - 这样 Python 才认这是一个包,能用
import mypkg。 - 所有代码都放在这个目录下。
命名空间包(Namespace Packages)
- 允许没有
__init__.py文件的包。 - 这样,
mypkg可以由多个分散的目录组成,比如你自己写的一部分代码在一个目录,别的团队写的代码在另一个目录,但都属于mypkg。 - Python 会把这些分散的目录自动“拼”在一起,作为一个包来看。
假设你有两个独立的项目(或者包),它们都在不同的地方,但都叫 mypkg,而且都没有 __init__.py 文件:
项目A:
/path/to/projectA/mypkg/module_a.py
# 目录 mypkg 下没有 __init__.py
项目B:
/another/path/projectB/mypkg/module_b.py
# 目录 mypkg 下也没有 __init__.py
你怎么用?
-
假设你在一个 Python 环境里,同时安装了这两个包(比如通过 pip 安装或手动放入
sys.path里的目录)。 -
Python 的导入机制会自动把这两个
mypkg目录“合并”成一个包,导入时能同时访问module_a和module_b:import mypkg.module_a import mypkg.module_b mypkg.module_a.some_function() mypkg.module_b.another_function()
传统包不能这么玩
如果是传统包,每个包目录都需要有 __init__.py,Python 认为 mypkg 是单一目录的包,安装第二个 mypkg 时会覆盖第一个,或者报冲突。
-
就像你在电脑上同时有两个文件夹叫
mypkg,但是 Python 在导入时,会把这两个文件夹里的内容“拼”到一起,好像这两个文件夹合成了一个超大的mypkg文件夹。 -
这对于插件系统非常有用:你自己写一部分插件,别人写另一部分,用户安装了你们的插件后,大家都放在
mypkg.plugins里,Python 自动帮你合成一个包。
假设项目A里有:
# /path/to/projectA/mypkg/module_a.py
def hello_a():
print("Hello from module A")
项目B里有:
# /another/path/projectB/mypkg/module_b.py
def hello_b():
print("Hello from module B")
在 Python 中,你就能这样:
import mypkg.module_a
import mypkg.module_b
mypkg.module_a.hello_a() # 输出: Hello from module A
mypkg.module_b.hello_b() # 输出: Hello from module B
为什么需要命名空间包?
- 适合大型项目,或者插件架构。
- 不同团队可以各自发布
mypkg.plugins.foo和mypkg.plugins.bar,它们可以分开维护和安装,但用户导入时就像是同一个包。 - 解决了传统包只能对应单一目录的限制。
从 Python 3.3 开始,官方支持 隐式命名空间包,也就是不需要 __init__.py 文件,直接创建没有 __init__.py 的目录即可。
示例:
mypkg/
module1.py
mypkg/ # 另一个地方,也可以是另一个安装包
module2.py
两个 mypkg 目录都没有 __init__.py,Python 运行时会把它们“合并”为一个包。
注意事项
- 命名空间包只能包含纯 Python 代码,不适合有 C 扩展的情况。
- 不能用老旧的 Python 版本(Python 3.3 之前没支持)。
- 有时可能会带来路径解析上的复杂度。
附录
pyproject.toml详解
pyproject.toml 是一个由打包工具以及其他工具(如代码检查器、类型检查器等)使用的配置文件。该文件中可能包含三种 TOML 表格。
-
[build-system] 表格是强烈推荐包含的。它允许你声明所使用的构建后端以及构建项目所需的其他依赖。
-
[project] 表格是大多数构建后端用来指定项目基本元数据的格式,例如依赖项、作者姓名等。
-
[tool] 表格包含针对具体工具的子表格,例如
[tool.hatch]、[tool.black]、[tool.mypy]等。这里仅简单提及,因为其内容由各个工具定义,具体内容请参考对应工具的文档。
注意
无论使用哪种构建后端,[build-system]表格都应该始终存在(它定义了你所使用的构建工具)。
另一方面,虽然大多数构建后端都支持 [project] 表格,但也有部分后端使用不同的格式。
一个显著的例外是 Poetry:在 2025 年 1 月 5 日发布的 2.0 版本之前,Poetry 不使用 [project] 表格,而是使用 [tool.poetry] 表格。2.0 版本之后则同时支持两者。此外,setuptools 构建后端同时支持 [project] 表格和老旧的 setup.cfg 或 setup.py 格式。
对于新项目,建议使用 [project] 表格,并且仅在需要编写程序化配置(如构建 C 扩展)时保留 setup.py。不过,setup.cfg 和 setup.py 格式依然有效。详情请参见《setup.py 是否已弃用?》。
接下来的内容主要介绍 [project] 表格。
静态元数据与动态元数据
大多数情况下,你会直接在 [project] 表格中写入字段的值,例如 requires-python = ">= 3.8" 或 version = "1.0"。
但是,在某些情况下,让构建后端动态计算元数据更为方便。例如,许多构建后端可以从代码中的 __version__ 属性、Git 标签或类似方式读取版本号。此时,你应通过以下方式将该字段标记为动态:
[project]
dynamic = ["version"]
当字段被标记为动态后,构建后端负责填充其值。具体实现方式请参考你所使用构建后端的文档。
基本信息
name
填写你项目在 PyPI 上的名称。此字段是必填项,也是唯一不能标记为动态的字段。
[project]
name = "spam-eggs"
项目名称必须由 ASCII 字母、数字、下划线“_”、连字符“-”和点号“.”组成。名称不能以下划线、连字符或点号开头或结尾。
项目名称的比较不区分大小写,并且将任意长度的下划线、连字符和/或点号视为相同。例如,如果你注册了名为 cool-stuff 的项目,用户可以用以下任何拼写来下载或声明依赖:Cool-Stuff、cool.stuff、COOL_STUFF、CoOl__-.-__sTuFF。
version
填写你项目的版本号。
[project]
version = "2020.0.0"
也支持一些更复杂的版本标识,如 2020.0.0a1(表示 alpha 版本);
该字段是必填项,但通常会用动态方式标记,如:
[project]
dynamic = ["version"]
这样可以实现从代码中的 __version__ 属性或 Git 标签自动获取版本号。
依赖和要求
dependencies/optional-dependencies
如果你的项目有依赖项,可以这样列出:
[project]
dependencies = [
"httpx",
"gidgethub[httpx]>4.0.0",
"django>2.1; os_name != 'nt'",
"django>2.0; os_name == 'nt'",
]
如果某些依赖仅用于包的特定功能,可以将它们放入 optional-dependencies:
[project.optional-dependencies]
gui = ["PyQt5"]
cli = [
"rich",
"click",
]
这里的每个键定义一个“打包额外项”(packaging extra)。例如,可以用 pip install your-project-name[gui] 来安装带 GUI 支持的版本,从而自动安装 PyQt5。
requires-python
用于声明你支持的最低 Python 版本:
[project]
requires-python = ">= 3.8"
创建可执行脚本
如果你想把命令行脚本作为包的一部分安装,需要在 [project.scripts] 表中声明:
[project.scripts]
spam-cli = "spam:main_cli"
示例中,安装包后会生成一个 spam-cli 命令,执行该命令等同于:
import sys
from spam import main_cli
sys.exit(main_cli())
在 Windows 上,通过这种方式打包的脚本需要一个终端。如果从图形界面应用启动它们,会弹出一个终端窗口。为避免这种情况,可以使用 [project.gui-scripts] 表代替 [project.scripts]:
[project.gui-scripts]
spam-gui = "spam:main_gui"
这样从命令行启动脚本时会立即返回控制权,脚本在后台运行。
注意: [project.scripts] 和 [project.gui-scripts] 的区别只在 Windows 平台上才有意义。
举个例子:
你写了一个包,里面有个函数是命令行程序的入口,比如:
# 在 spam.py 里
def main_cli():
print("Hello from spam CLI!")
然后你在 pyproject.toml 里写:
[project.scripts]
spam-cli = "spam:main_cli"
这是什么意思?
- 当别人安装你的包时,安装程序会自动帮他们在系统里创建一个名叫
spam-cli的命令(可执行文件或者脚本)。 - 用户以后只需要打开终端,输入
spam-cli,就会执行你spam.py里的main_cli()函数。 - 这样使用起来很方便,类似于安装了一个独立的命令行工具。
关于你的项目
authors / maintainers
这两个字段都包含一组人员信息,使用姓名和/或电子邮件地址标识。
[project]
authors = [
{name = "Pradyun Gedam", email = "pradyun@example.com"},
{name = "Tzu-Ping Chung", email = "tzu-ping@example.com"},
{name = "Another person"},
{email = "different.person@example.com"},
]
maintainers = [
{name = "Brett Cannon", email = "brett@example.com"}
]
description
这是你项目的一句话描述,将显示在 PyPI 项目页面的“标题”位置,以及搜索结果列表等其他地方。
[project]
description = "Lovely Spam! Wonderful Spam!"
readme
这是你项目的详细描述,用于显示在 PyPI 项目页面上。通常你的项目会有一个 README.md 或 README.rst 文件,你只需将文件名写在这里。
[project]
readme = "README.md"
README 格式会根据文件扩展名自动检测:
README.md→ GitHub 风格的 MarkdownREADME.rst→ reStructuredText(不含 Sphinx 扩展)
你也可以显式指定格式:
[project]
readme = {file = "README.txt", content-type = "text/markdown"}
# 或者
readme = {file = "README.txt", content-type = "text/x-rst"}
license 和 license-files
根据 PEP 639,许可证应使用两个字段声明:
license是一个 SPDX 许可证表达式,由一个或多个许可证标识符组成。license-files是许可证文件的匹配模式列表。
之前的 PEP 规定 license 是一个包含 file 或 text 键的表格格式,但该格式现已废弃。大多数构建后端现在支持新版格式,具体支持版本如下:
| 构建后端 | 版本支持PEP 639 |
|---|---|
| hatchling | 1.27.0 |
| setuptools | 77.0.3 |
| flit-core | 3.12 |
| pdm-backend | 2.4.0 |
| poetry-core | 尚未支持 |
license
新版格式为有效的 SPDX 许可证表达式,包含一个或多个许可证标识符。完整许可证列表见 SPDX 许可证列表页面。支持的列表版本为 3.17 或任何兼容版本。
[project]
license = "GPL-3.0-or-later"
# 或者
license = "MIT AND (Apache-2.0 OR BSD-2-Clause)"
注意
如果构建时报错说license应为字典/表格格式,说明你的构建后端尚未支持新版格式。请参考上文说明。旧格式描述见 PEP 621。
通常建议使用标准、知名的许可证,以避免混淆,同时部分机构可能避免使用未经批准的许可证的软件。
如果你的项目使用了没有 SPDX 标识符的自定义许可证,可以使用如下格式创建:
[project]
license = "LicenseRef-My-Custom-License"
自定义标识符必须符合 SPDX 规范,具体见版本 2.2 的第 10.1 条款或兼容版本。
license-files
这是你想随包一起发布的许可证文件或其他法律信息文件列表。
[project]
license-files = ["LICEN[CS]E*", "vendored/licenses/*.txt", "AUTHORS.md"]
匹配模式需符合以下规范:
- 字母数字、下划线
_、连字符-和点.按字面匹配。 - 支持特殊字符:
*,?,**和字符范围[]。 - 路径分隔符必须是正斜杠
/。 - 模式相对于包含
pyproject.toml的目录,不能以斜杠开头。 - 不允许使用父目录指示符
..。 - 每个匹配模式必须至少匹配一个文件。
- 文字路径也视为合法匹配模式。
- 不符合以上规范的字符或序列为无效。
keywords
这些关键词有助于 PyPI 的搜索框在用户搜索时推荐你的项目。
[project]
keywords = ["egg", "bacon", "sausage", "tomatoes", "Lobster Thermidor"]
classifiers
一组适用于你项目的 PyPI 分类器。
classifiers = [
# 项目的成熟度,常见值:
# 3 - Alpha
# 4 - Beta
# 5 - 生产/稳定
"Development Status :: 4 - Beta",
# 目标用户
"Intended Audience :: Developers",
"Topic :: Software Development :: Build Tools",
# 支持的 Python 版本
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
]
虽然分类器通常用来声明项目支持的 Python 版本,但这只是供 PyPI 搜索和浏览时使用,并不会限制安装时的 Python 版本。
若要限制可安装的 Python 版本,应使用 requires-python 参数。
如果想阻止某包被上传到 PyPI,可以使用特殊分类器 Private :: Do Not Upload。PyPI 会拒绝带有此开头分类器的包。
urls
项目页面左侧栏会显示这些关联的网址。
注意
参见“知名标签”(Well-known labels)以了解 PyPI 和其他打包工具特别支持的标签,以及 PyPI 项目元数据文档中关于 URL 处理的说明。
[project.urls]
Homepage = "https://example.com"
Documentation = "https://readthedocs.org"
Repository = "https://github.com/me/spam.git"
Issues = "https://github.com/me/spam/issues"
Changelog = "https://github.com/me/spam/blob/master/CHANGELOG.md"
如果标签包含空格,需要用引号括起来,比如
Website = "https://example.com"
而 "Official Website" = "https://example.com" 也合法。
建议使用知名标签,以便元数据消费者(如包索引)能更好地展示这些链接。
示例:
[project.urls]
MyHomepage = "https://example.com"
"Download Link" = "https://example.com/abc.tar.gz"
这两个不是知名标签,会原样显示。
而下面例子里的标签是知名标签,会被识别成对应含义:
[project.urls]
HomePage = "https://example.com"
DOWNLOAD = "https://example.com/abc.tar.gz"
其中分别代表项目主页和下载链接。
高级插件
有些包可以通过插件进行扩展,比如 Pytest 和 Pygments。要创建这样的插件,你需要在 [project.entry-points] 的子表中声明,比如:
[project.entry-points."spam.magical"]
tomatoes = "spam:main_tomatoes"
完整案例
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "spam-eggs"
version = "2020.0.0"
dependencies = [
"httpx",
"gidgethub[httpx]>4.0.0",
"django>2.1; os_name != 'nt'",
"django>2.0; os_name == 'nt'",
]
requires-python = ">=3.8"
authors = [
{name = "Pradyun Gedam", email = "pradyun@example.com"},
{name = "Tzu-Ping Chung", email = "tzu-ping@example.com"},
{name = "Another person"},
{email = "different.person@example.com"},
]
maintainers = [
{name = "Brett Cannon", email = "brett@example.com"}
]
description = "Lovely Spam! Wonderful Spam!"
readme = "README.rst"
license = "MIT"
license-files = ["LICEN[CS]E.*"]
keywords = ["egg", "bacon", "sausage", "tomatoes", "Lobster Thermidor"]
classifiers = [
"Development Status :: 4 - Beta",
"Programming Language :: Python"
]
[project.optional-dependencies]
gui = ["PyQt5"]
cli = [
"rich",
"click",
]
[project.urls]
Homepage = "https://example.com"
Documentation = "https://readthedocs.org"
Repository = "https://github.com/me/spam.git"
"Bug Tracker" = "https://github.com/me/spam/issues"
Changelog = "https://github.com/me/spam/blob/master/CHANGELOG.md"
[project.scripts]
spam-cli = "spam:main_cli"
[project.gui-scripts]
spam-gui = "spam:main_gui"
[project.entry-points."spam.magical"]
tomatoes = "spam:main_tomatoes"
参考连接:https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#keywords
541

被折叠的 条评论
为什么被折叠?



