本章介紹 pyproject.toml 的結構與設定方式。

本章目標

學完本章後,你將能夠:

  1. 理解 pyproject.toml 的三個主要表
  2. 設定專案元數據(PEP 621)
  3. 設定建構系統(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 518build-system 表已採納
PEP 621project 元數據已採納
PEP 639license 欄位改進已採納
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.py
1[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.py
1[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)

思考題

  1. 為什麼 Python 社群花了這麼長時間才標準化打包設定?
  2. [build-system] 中的 requires[project] 中的 dependencies 有什麼區別?
  3. 動態欄位在什麼情況下有用?有什麼潛在問題?

實作練習

  1. 將一個使用 setup.py 的舊專案遷移到 pyproject.toml
  2. 建立一個包含 CLI 入口點的套件,並在本地測試安裝
  3. 使用 setuptools-scm 設定自動版本管理

延伸閱讀


下一章:建構系統比較