6.1 pyproject.toml 完整指南
6.1 pyproject.toml 完整指南
本章介紹 pyproject.toml 的結構與設定方式。
本章目標
學完本章後,你將能夠:
- 理解 pyproject.toml 的三個主要表
- 設定專案元數據(PEP 621)
- 設定建構系統(PEP 518)
【原理層】pyproject.toml 的演進
歷史背景
1Python 打包的演進:
2├── 2000s: setup.py(純 Python 腳本)
3├── 2010s: setup.cfg(宣告式設定)
4├── 2016: PEP 518(pyproject.toml 誕生)
5├── 2020: PEP 621(標準化元數據)
6└── 2025: pyproject.toml 成為主流標準為什麼需要 pyproject.toml?
1setup.py 的問題:
2├── 需要執行程式碼才能讀取元數據
3├── 安全風險(任意程式碼執行)
4├── 無法標準化建構依賴
5└── 不同工具使用不同設定檔
6
7pyproject.toml 的優點:
8├── 靜態設定,不需執行
9├── 標準化格式(TOML)
10├── 統一的設定位置
11└── 支援多種建構後端相關 PEP
| PEP | 內容 | 狀態 |
|---|---|---|
| PEP 518 | build-system 表 | 已採納 |
| PEP 621 | project 元數據 | 已採納 |
| PEP 639 | license 欄位改進 | 已採納 |
| PEP 660 | 可編輯安裝 | 已採納 |
| PEP 735 | 依賴群組 | 草案中 |
【設計層】三個主要表
檔案結構總覽
1# pyproject.toml 的三個主要表
2
3[build-system]
4# 定義如何建構套件
5requires = ["setuptools>=61.0"]
6build-backend = "setuptools.build_meta"
7
8[project]
9# 定義套件的元數據
10name = "my-package"
11version = "1.0.0"
12# ...
13
14[tool.xxx]
15# 各種工具的設定
16# [tool.setuptools], [tool.pytest], [tool.ruff], etc.[build-system]:建構設定
1[build-system]
2# 建構時需要的套件(PEP 518)
3requires = [
4 "setuptools>=61.0",
5 "wheel",
6 # 如果有 C 擴展
7 # "cython>=3.0",
8]
9
10# 建構後端(PEP 517)
11build-backend = "setuptools.build_meta"
12
13# 選用:後端路徑(較少使用)
14# backend-path = ["."]常見的建構後端:
1建構後端 requires
2─────────────────────────────────────────────────────
3setuptools.build_meta ["setuptools>=61.0"]
4flit_core.buildapi ["flit_core>=3.4"]
5hatchling.build ["hatchling"]
6poetry.core.masonry.api ["poetry-core>=1.0.0"]
7maturin ["maturin>=1.5"]
8scikit_build_core.build ["scikit-build-core>=0.5"]
9mesonpy ["meson-python"][project]:專案元數據
1[project]
2# === 必填欄位 ===
3name = "my-awesome-package"
4version = "1.0.0"
5
6# === 基本資訊 ===
7description = "A short description of the package"
8readme = "README.md" # 或 {file = "README.md", content-type = "text/markdown"}
9license = {text = "MIT"} # 或 {file = "LICENSE"}
10requires-python = ">=3.8"
11
12# === 作者資訊 ===
13authors = [
14 {name = "Your Name", email = "you@example.com"},
15]
16maintainers = [
17 {name = "Maintainer", email = "maintainer@example.com"},
18]
19
20# === 分類與關鍵字 ===
21keywords = ["example", "package", "demo"]
22classifiers = [
23 "Development Status :: 4 - Beta",
24 "Intended Audience :: Developers",
25 "License :: OSI Approved :: MIT License",
26 "Programming Language :: Python :: 3",
27 "Programming Language :: Python :: 3.8",
28 "Programming Language :: Python :: 3.9",
29 "Programming Language :: Python :: 3.10",
30 "Programming Language :: Python :: 3.11",
31 "Programming Language :: Python :: 3.12",
32]
33
34# === URL ===
35[project.urls]
36Homepage = "https://github.com/user/project"
37Documentation = "https://project.readthedocs.io"
38Repository = "https://github.com/user/project"
39Changelog = "https://github.com/user/project/blob/main/CHANGELOG.md"
40
41# === 依賴 ===
42dependencies = [
43 "requests>=2.28",
44 "click>=8.0",
45]
46
47[project.optional-dependencies]
48dev = [
49 "pytest>=7.0",
50 "pytest-cov",
51 "ruff",
52 "mypy",
53]
54docs = [
55 "sphinx>=6.0",
56 "sphinx-rtd-theme",
57]
58
59# === 入口點 ===
60[project.scripts]
61my-cli = "my_package.cli:main"
62
63[project.gui-scripts]
64my-gui = "my_package.gui:main"
65
66[project.entry-points."my_package.plugins"]
67plugin1 = "my_package.plugins.plugin1:Plugin"[tool.xxx]:工具設定
1# === setuptools 設定 ===
2[tool.setuptools]
3packages = ["my_package"]
4# 或使用自動發現
5package-dir = {"" = "src"}
6
7[tool.setuptools.packages.find]
8where = ["src"]
9
10[tool.setuptools.package-data]
11my_package = ["*.json", "data/*"]
12
13# === pytest 設定 ===
14[tool.pytest.ini_options]
15testpaths = ["tests"]
16python_files = ["test_*.py"]
17addopts = "-v --cov=my_package"
18
19# === ruff 設定 ===
20[tool.ruff]
21line-length = 88
22target-version = "py38"
23
24[tool.ruff.lint]
25select = ["E", "F", "W", "I", "UP"]
26ignore = ["E501"]
27
28# === mypy 設定 ===
29[tool.mypy]
30python_version = "3.8"
31strict = true
32warn_return_any = true
33
34# === coverage 設定 ===
35[tool.coverage.run]
36source = ["my_package"]
37branch = true
38
39[tool.coverage.report]
40exclude_lines = [
41 "pragma: no cover",
42 "if TYPE_CHECKING:",
43]【實作層】完整範例
最小可行設定
1# 最小的 pyproject.toml
2[build-system]
3requires = ["setuptools>=61.0"]
4build-backend = "setuptools.build_meta"
5
6[project]
7name = "my-package"
8version = "0.1.0"標準函式庫風格
1[build-system]
2requires = ["setuptools>=61.0"]
3build-backend = "setuptools.build_meta"
4
5[project]
6name = "my-package"
7version = "1.0.0"
8description = "A well-documented Python package"
9readme = "README.md"
10license = {text = "MIT"}
11requires-python = ">=3.8"
12authors = [{name = "Your Name", email = "you@example.com"}]
13classifiers = [
14 "Development Status :: 4 - Beta",
15 "Intended Audience :: Developers",
16 "License :: OSI Approved :: MIT License",
17 "Operating System :: OS Independent",
18 "Programming Language :: Python :: 3",
19 "Programming Language :: Python :: 3.8",
20 "Programming Language :: Python :: 3.9",
21 "Programming Language :: Python :: 3.10",
22 "Programming Language :: Python :: 3.11",
23 "Programming Language :: Python :: 3.12",
24 "Typing :: Typed",
25]
26dependencies = []
27
28[project.optional-dependencies]
29dev = ["pytest>=7.0", "ruff", "mypy"]
30
31[project.urls]
32Homepage = "https://github.com/user/my-package"
33Repository = "https://github.com/user/my-package"
34
35[tool.setuptools.packages.find]
36where = ["src"]含 CLI 的應用程式
1[build-system]
2requires = ["setuptools>=61.0"]
3build-backend = "setuptools.build_meta"
4
5[project]
6name = "my-cli-tool"
7version = "2.0.0"
8description = "A powerful CLI tool"
9readme = "README.md"
10license = {text = "Apache-2.0"}
11requires-python = ">=3.9"
12authors = [{name = "CLI Team"}]
13dependencies = [
14 "click>=8.0",
15 "rich>=13.0",
16]
17
18[project.optional-dependencies]
19dev = ["pytest", "pytest-cov"]
20
21[project.scripts]
22my-tool = "my_cli_tool.main:cli"
23
24[project.urls]
25Homepage = "https://my-cli-tool.dev"
26
27[tool.setuptools.packages.find]
28where = ["src"]
29
30[tool.setuptools.package-data]
31my_cli_tool = ["templates/*.txt", "config/*.yaml"]【進階】動態欄位
動態版本
1[project]
2name = "my-package"
3dynamic = ["version"] # 版本由其他來源決定
4
5[tool.setuptools.dynamic]
6version = {attr = "my_package.__version__"}
7# 或從檔案讀取
8# version = {file = "VERSION"}1# src/my_package/__init__.py
2__version__ = "1.2.3"動態依賴
1[project]
2name = "my-package"
3dynamic = ["dependencies", "optional-dependencies"]
4
5[tool.setuptools.dynamic]
6dependencies = {file = ["requirements.txt"]}
7optional-dependencies.dev = {file = ["requirements-dev.txt"]}使用 setuptools-scm(Git 標籤版本)
1[build-system]
2requires = ["setuptools>=61.0", "setuptools-scm>=8.0"]
3build-backend = "setuptools.build_meta"
4
5[project]
6name = "my-package"
7dynamic = ["version"]
8
9[tool.setuptools_scm]
10# 版本從 git tag 自動產生
11# v1.0.0 -> 1.0.0
12# v1.0.0-2-gabcdef -> 1.0.0.dev2+gabcdef【實作層】專案結構對應
Flat Layout
1my-package/
2├── pyproject.toml
3├── README.md
4├── my_package/
5│ ├── __init__.py
6│ └── module.py
7└── tests/
8 └── test_module.py1[tool.setuptools]
2packages = ["my_package"]Src Layout(推薦)
1my-package/
2├── pyproject.toml
3├── README.md
4├── src/
5│ └── my_package/
6│ ├── __init__.py
7│ └── module.py
8└── tests/
9 └── test_module.py1[tool.setuptools.packages.find]
2where = ["src"]為什麼推薦 src layout?
1Src Layout 的優點:
2├── 避免匯入本地未安裝的套件
3├── 強制測試已安裝的版本
4├── 清楚區分原始碼和專案根目錄
5└── 避免名稱衝突【進階】PEP 639 授權條款
新的 license 語法
1# PEP 639(Python 3.14+,但建構工具已支援)
2
3# SPDX 表示法
4[project]
5license = "MIT"
6# 或
7license = "Apache-2.0 OR MIT"
8# 或
9license = "GPL-3.0-only"
10
11# 授權檔案
12license-files = ["LICENSE", "LICENSES/*"]常見的 SPDX 識別碼
1識別碼 完整名稱
2─────────────────────────────────────────
3MIT MIT License
4Apache-2.0 Apache License 2.0
5GPL-3.0-only GNU GPL v3.0 only
6GPL-3.0-or-later GNU GPL v3.0 or later
7BSD-3-Clause BSD 3-Clause License
8BSD-2-Clause BSD 2-Clause License
9MPL-2.0 Mozilla Public License 2.0
10LGPL-3.0-only GNU LGPL v3.0 only
11ISC ISC License
12Unlicense The Unlicense【驗證】檢查設定
使用 validate-pyproject
1pip install validate-pyproject
2
3# 驗證 pyproject.toml
4validate-pyproject pyproject.toml使用 build 測試建構
1pip install build
2
3# 建構套件(會驗證設定)
4python -m build
5
6# 建構結果
7ls dist/
8# my_package-1.0.0.tar.gz
9# my_package-1.0.0-py3-none-any.whl常見錯誤
1錯誤:Unknown key in [project]
2原因:使用了非標準欄位
3解決:檢查 PEP 621 允許的欄位
4
5錯誤:Invalid version
6原因:版本格式不符合 PEP 440
7解決:使用正確格式,如 "1.0.0", "1.0.0a1", "1.0.0.dev1"
8
9錯誤:Missing required key
10原因:缺少必填欄位
11解決:至少要有 name 和 version(或 dynamic)思考題
- 為什麼 Python 社群花了這麼長時間才標準化打包設定?
[build-system]中的requires和[project]中的dependencies有什麼區別?- 動態欄位在什麼情況下有用?有什麼潛在問題?
實作練習
- 將一個使用 setup.py 的舊專案遷移到 pyproject.toml
- 建立一個包含 CLI 入口點的套件,並在本地測試安裝
- 使用 setuptools-scm 設定自動版本管理
延伸閱讀
下一章:建構系統比較