使用pyproject.toml構(gòu)建現(xiàn)代化Python項(xiàng)目的詳細(xì)步驟
前言
如果你曾經(jīng)接觸過 Python 項(xiàng)目,可能對 setup.py、setup.cfg、requirements.txt 這些文件并不陌生。但隨著 Python 生態(tài)的發(fā)展,這種分散的配置方式逐漸暴露出諸多問題:配置分散、格式不統(tǒng)一、工具兼容性差等。
pyproject.toml 的出現(xiàn)正是為了解決這些痛點(diǎn)。它是 Python 項(xiàng)目的現(xiàn)代化配置標(biāo)準(zhǔn),由 PEP 518、PEP 621 等規(guī)范定義,旨在提供一個(gè)統(tǒng)一的、聲明式的項(xiàng)目配置文件。
本文將以一個(gè)實(shí)際項(xiàng)目 PyImage Split(圖片拆分工具)為例,詳細(xì)講解如何從零開始配置 pyproject.toml,讓你的 Python 項(xiàng)目更加規(guī)范、現(xiàn)代化。
為什么選擇 pyproject.toml
傳統(tǒng)方式的問題
在 pyproject.toml 出現(xiàn)之前,一個(gè)典型的 Python 項(xiàng)目可能包含以下配置文件:
my_project/ ├── setup.py # 項(xiàng)目元數(shù)據(jù)和構(gòu)建配置 ├── setup.cfg # 部分配置的聲明式版本 ├── requirements.txt # 運(yùn)行依賴 ├── requirements-dev.txt # 開發(fā)依賴 ├── MANIFEST.in # 打包文件清單 ├── pytest.ini # pytest 配置 ├── .flake8 # flake8 配置 ├── .isort.cfg # isort 配置 └── mypy.ini # mypy 配置
這種方式存在以下問題:
- 配置分散:相關(guān)配置散落在多個(gè)文件中,難以維護(hù)
- 格式不統(tǒng)一:INI、CFG、TXT、Python 腳本混雜
- setup.py 的安全隱患:作為 Python 腳本,可能執(zhí)行任意代碼
- 依賴管理混亂:運(yùn)行依賴和開發(fā)依賴分離管理
pyproject.toml 的優(yōu)勢
- 統(tǒng)一配置:所有配置集中在一個(gè)文件
- 標(biāo)準(zhǔn)格式:使用 TOML 格式,語法清晰易讀
- 聲明式配置:無需執(zhí)行代碼,更安全
- 工具兼容:現(xiàn)代 Python 工具都支持
- PEP 標(biāo)準(zhǔn):官方推薦的配置方式
pyproject.toml 的基本結(jié)構(gòu)
一個(gè)完整的 pyproject.toml 通常包含以下幾個(gè)主要部分:
# 1. 構(gòu)建系統(tǒng)配置 [build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" # 2. 項(xiàng)目元數(shù)據(jù) [project] name = "my-project" version = "0.1.0" # ... 其他元數(shù)據(jù) # 3. 可選依賴 [project.optional-dependencies] dev = ["pytest", "black"] # 4. 入口點(diǎn) [project.scripts] my-command = "my_package.main:main" # 5. 項(xiàng)目鏈接 [project.urls] Homepage = "https://github.com/..." # 6. 構(gòu)建工具特定配置 [tool.setuptools] # setuptools 相關(guān)配置 # 7. 其他工具配置 [tool.black] line-length = 88 [tool.pytest.ini_options] testpaths = ["tests"]
實(shí)戰(zhàn):配置 PyImage Split 項(xiàng)目
讓我們以 PyImage Split 項(xiàng)目為例,這是一個(gè)使用 PySide6 構(gòu)建的圖片查看和拆分工具。
項(xiàng)目結(jié)構(gòu)
pyimage_split/
├── pyproject.toml # 項(xiàng)目配置文件
├── README.md # 項(xiàng)目說明
├── src/ # 源代碼目錄
│ └── pyimage_split/ # 主包
│ ├── __init__.py # 包初始化
│ └── main.py # 主程序
└── tests/ # 測試目錄
└── test_main.py # 單元測試
完整的 pyproject.toml
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "pyimage-split"
version = "0.1.0"
description = "圖片查看和均勻拆分工具"
readme = "README.md"
license = {text = "MIT"}
authors = [
{name = "Your Name", email = "your.email@example.com"}
]
requires-python = ">=3.8"
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"PySide6>=6.5.0",
"Pillow>=9.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"black>=23.0",
"ruff>=0.1.0",
]
[project.scripts]
pyimage-split = "pyimage_split.main:main"
[project.urls]
Homepage = "https://github.com/yourusername/pyimage-split"
Documentation = "https://github.com/yourusername/pyimage-split#readme"
Repository = "https://github.com/yourusername/pyimage-split"
[tool.setuptools.packages.find]
where = ["src"]
[tool.black]
line-length = 88
target-version = ['py38', 'py39', 'py310', 'py311', 'py312']
[tool.ruff]
line-length = 88
select = ["E", "F", "W", "I"]
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = "test_*.py"各配置部分詳解
1. [build-system] - 構(gòu)建系統(tǒng)配置
[build-system] requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta"
這是 pyproject.toml 中必須包含的部分,由 PEP 518 定義。
| 字段 | 說明 |
|---|---|
requires | 構(gòu)建項(xiàng)目所需的依賴包列表 |
build-backend | 指定構(gòu)建后端 |
常用構(gòu)建后端對比:
| 構(gòu)建后端 | 特點(diǎn) | 適用場景 |
|---|---|---|
setuptools.build_meta | 功能全面,兼容性好 | 通用項(xiàng)目 |
poetry.core.masonry.api | Poetry 生態(tài) | 使用 Poetry 管理的項(xiàng)目 |
flit_core.buildapi | 簡單輕量 | 純 Python 包 |
hatchling | 現(xiàn)代化,功能豐富 | 新項(xiàng)目推薦 |
pdm.backend | PDM 生態(tài) | 使用 PDM 管理的項(xiàng)目 |
2. [project] - 項(xiàng)目元數(shù)據(jù)
這是項(xiàng)目的核心配置部分,由 PEP 621 定義。
基本信息
[project] name = "pyimage-split" version = "0.1.0" description = "圖片查看和均勻拆分工具"
| 字段 | 必需 | 說明 |
|---|---|---|
name | 是 | 項(xiàng)目名稱,用于 pip 安裝 |
version | 是 | 版本號,遵循 SemVer 規(guī)范 |
description | 否 | 簡短描述 |
項(xiàng)目命名規(guī)范:
- 使用小寫字母和連字符(如
pyimage-split) - 包名使用下劃線(如
pyimage_split) - 避免與已有 PyPI 包重名
README 和許可證
readme = "README.md"
license = {text = "MIT"}
readme 支持多種格式:
- 字符串:
readme = "README.md" - 指定類型:
readme = {file = "README.rst", content-type = "text/x-rst"}
license 的寫法:
- 簡單文本:
license = {text = "MIT"} - 引用文件:
license = {file = "LICENSE"}
作者信息
authors = [
{name = "Your Name", email = "your.email@example.com"}
]
maintainers = [
{name = "Maintainer Name", email = "maintainer@example.com"}
]
Python 版本要求
requires-python = ">=3.8"
常見寫法:
>=3.8:3.8 及以上>=3.8,<4.0:3.8 到 3.x~=3.8:兼容 3.8.x
分類器 (Classifiers)
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
分類器用于在 PyPI 上對項(xiàng)目進(jìn)行分類,完整列表見:https://pypi.org/classifiers/
常用分類器:
| 類別 | 示例 |
|---|---|
| 開發(fā)狀態(tài) | Development Status :: 3 - Alpha |
| 目標(biāo)用戶 | Intended Audience :: Developers |
| 許可證 | License :: OSI Approved :: MIT License |
| 操作系統(tǒng) | Operating System :: OS Independent |
| 編程語言 | Programming Language :: Python :: 3 |
| 主題 | Topic :: Software Development :: Libraries |
3. dependencies - 項(xiàng)目依賴
dependencies = [
"PySide6>=6.5.0",
"Pillow>=9.0.0",
]
版本約束語法:
| 語法 | 含義 | 示例 |
|---|---|---|
>= | 大于等于 | requests>=2.28.0 |
<= | 小于等于 | requests<=3.0.0 |
== | 精確版本 | requests==2.28.1 |
!= | 排除版本 | requests!=2.28.0 |
~= | 兼容版本 | requests~=2.28.0 (等于 >=2.28.0,<2.29.0) |
* | 通配符 | requests==2.28.* |
組合使用:
dependencies = [
"requests>=2.28.0,<3.0.0",
"numpy>=1.20.0; python_version>='3.9'", # 條件依賴
]
4. [project.optional-dependencies] - 可選依賴
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"black>=23.0",
"ruff>=0.1.0",
]
docs = [
"sphinx>=5.0",
"sphinx-rtd-theme>=1.0",
]
all = [
"pyimage-split[dev,docs]",
]
安裝方式:
# 安裝開發(fā)依賴 pip install -e ".[dev]" # 安裝文檔依賴 pip install -e ".[docs]" # 安裝所有可選依賴 pip install -e ".[all]"
5. [project.scripts] - 命令行入口點(diǎn)
[project.scripts] pyimage-split = "pyimage_split.main:main"
這會創(chuàng)建一個(gè)名為 pyimage-split 的命令,執(zhí)行時(shí)調(diào)用 pyimage_split.main 模塊的 main 函數(shù)。
格式解析:
命令名 = "包名.模塊名:函數(shù)名"
安裝后可直接在命令行使用:
$ pyimage-split
其他入口點(diǎn)類型:
# GUI 腳本(Windows 下無控制臺窗口) [project.gui-scripts] pyimage-split-gui = "pyimage_split.main:main" # 插件入口點(diǎn) [project.entry-points."myapp.plugins"] plugin1 = "mypackage.plugins:Plugin1"
6. [project.urls] - 項(xiàng)目鏈接
[project.urls] Homepage = "https://github.com/yourusername/pyimage-split" Documentation = "https://github.com/yourusername/pyimage-split#readme" Repository = "https://github.com/yourusername/pyimage-split" Changelog = "https://github.com/yourusername/pyimage-split/blob/main/CHANGELOG.md" "Bug Tracker" = "https://github.com/yourusername/pyimage-split/issues"
這些鏈接會顯示在 PyPI 項(xiàng)目頁面上。
常用工具配置
[tool.setuptools] - Setuptools 配置
[tool.setuptools.packages.find] where = ["src"] include = ["pyimage_split*"] exclude = ["tests*"]
使用 src 布局的項(xiàng)目需要指定 where = ["src"]。
動態(tài)版本號:
如果想從 __init__.py 讀取版本號:
[project]
dynamic = ["version"]
[tool.setuptools.dynamic]
version = {attr = "pyimage_split.__version__"}
[tool.black] - 代碼格式化
[tool.black]
line-length = 88
target-version = ['py38', 'py39', 'py310', 'py311', 'py312']
include = '\.pyi?$'
exclude = '''
/(
\.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
)/
'''
| 配置項(xiàng) | 說明 | 默認(rèn)值 |
|---|---|---|
line-length | 每行最大字符數(shù) | 88 |
target-version | 目標(biāo) Python 版本 | 自動檢測 |
include | 包含的文件模式 | \.pyi?$ |
exclude | 排除的文件模式 | 常見緩存目錄 |
[tool.ruff] - 代碼檢查
Ruff 是一個(gè)用 Rust 編寫的快速 Python linter,可以替代 flake8、isort 等多個(gè)工具。
[tool.ruff]
line-length = 88
select = [
"E", # pycodestyle errors
"F", # pyflakes
"W", # pycodestyle warnings
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
]
ignore = [
"E501", # line too long (handled by black)
]
[tool.ruff.isort]
known-first-party = ["pyimage_split"]
[tool.pytest.ini_options] - 測試配置
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = "test_*.py"
python_classes = "Test*"
python_functions = "test_*"
addopts = "-v --tb=short"
markers = [
"slow: marks tests as slow",
"integration: marks tests as integration tests",
]
| 配置項(xiàng) | 說明 |
|---|---|
testpaths | 測試文件目錄 |
python_files | 測試文件名模式 |
addopts | 默認(rèn)命令行參數(shù) |
markers | 自定義標(biāo)記 |
[tool.mypy] - 類型檢查
[tool.mypy] python_version = "3.8" warn_return_any = true warn_unused_configs = true ignore_missing_imports = true [[tool.mypy.overrides]] module = "tests.*" ignore_errors = true
[tool.coverage] - 代碼覆蓋率
[tool.coverage.run]
source = ["src"]
branch = true
omit = ["tests/*"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise NotImplementedError",
"if __name__ == .__main__.:",
]
show_missing = true項(xiàng)目安裝與分發(fā)
本地開發(fā)安裝
# 進(jìn)入項(xiàng)目目錄 cd pyimage_split # 創(chuàng)建虛擬環(huán)境 python -m venv venv source venv/bin/activate # Linux/macOS # 或 venv\Scripts\activate # Windows # 可編輯模式安裝(推薦開發(fā)時(shí)使用) pip install -e . # 安裝開發(fā)依賴 pip install -e ".[dev]"
-e 參數(shù)表示可編輯模式(editable mode),修改源碼后無需重新安裝。
構(gòu)建分發(fā)包
# 安裝構(gòu)建工具 pip install build # 構(gòu)建 python -m build
構(gòu)建完成后,dist/ 目錄下會生成:
pyimage_split-0.1.0.tar.gz:源碼分發(fā)包 (sdist)pyimage_split-0.1.0-py3-none-any.whl:構(gòu)建分發(fā)包 (wheel)
發(fā)布到 PyPI
# 安裝 twine pip install twine # 檢查分發(fā)包 twine check dist/* # 上傳到 TestPyPI(測試) twine upload --repository testpypi dist/* # 上傳到 PyPI(正式) twine upload dist/*
最佳實(shí)踐與常見問題
最佳實(shí)踐
使用 src 布局
project/ ├── src/ │ └── package_name/ ├── tests/ └── pyproject.toml
優(yōu)點(diǎn):避免直接導(dǎo)入源碼目錄,確保測試的是安裝后的包。
版本號管理
- 遵循語義化版本 (SemVer):
主版本.次版本.修訂號 - 考慮使用動態(tài)版本或
bump2version工具
依賴版本約束
- 運(yùn)行依賴:使用寬松約束
>=1.0.0 - 開發(fā)依賴:可以使用更嚴(yán)格約束
分離可選依賴
[project.optional-dependencies] dev = ["pytest", "black"] docs = ["sphinx"]
配置所有工具
將 black、ruff、pytest、mypy 等配置都放入 pyproject.toml
常見問題
Q1: 如何從 setup.py 遷移?
可以使用 ini2toml 工具自動轉(zhuǎn)換:
pip install ini2toml[full] ini2toml setup.cfg > pyproject.toml
Q2: 如何處理動態(tài)內(nèi)容?
[project]
dynamic = ["version", "readme"]
[tool.setuptools.dynamic]
version = {attr = "mypackage.__version__"}
readme = {file = ["README.md", "CHANGELOG.md"]}
Q3: 如何添加數(shù)據(jù)文件?
[tool.setuptools.package-data] mypackage = ["*.txt", "data/*.json"]
或使用 MANIFEST.in 文件。
Q4: 為什么安裝后找不到命令?
檢查以下幾點(diǎn):
- 虛擬環(huán)境是否激活
[project.scripts]配置是否正確- 入口函數(shù)是否存在
Q5: 如何支持多個(gè) Python 版本?
requires-python = ">=3.8"
classifiers = [
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
總結(jié)
pyproject.toml 是現(xiàn)代 Python 項(xiàng)目的標(biāo)準(zhǔn)配置方式,它帶來了以下好處:
- 統(tǒng)一性:一個(gè)文件管理所有配置
- 可讀性:TOML 格式清晰易懂
- 安全性:聲明式配置,無代碼執(zhí)行
- 兼容性:所有現(xiàn)代工具都支持
通過本文的 PyImage Split 項(xiàng)目示例,我們詳細(xì)介紹了:
[build-system]:構(gòu)建系統(tǒng)配置[project]:項(xiàng)目元數(shù)據(jù)[project.optional-dependencies]:可選依賴分組[project.scripts]:命令行入口點(diǎn)[tool.*]:各種開發(fā)工具配置
建議新項(xiàng)目直接使用 pyproject.toml,老項(xiàng)目也可以逐步遷移。這不僅能讓你的項(xiàng)目更加規(guī)范,也能更好地與 Python 生態(tài)系統(tǒng)中的各種工具協(xié)作。
本文以 PyImage Split 項(xiàng)目為例,該項(xiàng)目是一個(gè)使用 PySide6 構(gòu)建的圖片查看和拆分工具,完整代碼可在項(xiàng)目倉庫中查看。
以上就是使用pyproject.toml構(gòu)建現(xiàn)代化Python項(xiàng)目的詳細(xì)步驟的詳細(xì)內(nèi)容,更多關(guān)于pyproject.toml構(gòu)建Python項(xiàng)目的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
python獲取當(dāng)前時(shí)間對應(yīng)unix時(shí)間戳的方法
這篇文章主要介紹了python獲取當(dāng)前時(shí)間對應(yīng)unix時(shí)間戳的方法,涉及Python時(shí)間操作的相關(guān)技巧,非常簡單實(shí)用,需要的朋友可以參考下2015-05-05
使用Selenium在Python中實(shí)現(xiàn)錄屏功能
Selenium 是一個(gè)強(qiáng)大的用于自動化測試的工具,但你知道它也可以用來錄制瀏覽器操作的視頻嗎?本文將介紹如何使用 Selenium 在 Python 中實(shí)現(xiàn)錄屏功能,以便記錄和分享你的網(wǎng)頁操作過程,需要的朋友可以參考下2023-11-11
教你用Python腳本快速為iOS10生成圖標(biāo)和截屏
這篇文章主要介紹了教你用Python快速為iOS10生成圖標(biāo)和截屏的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-09-09
PyTorch中 tensor.detach() 和 tensor.data 的
這篇文章主要介紹了PyTorch中 tensor.detach() 和 tensor.data 的區(qū)別解析,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-04-04
利用Python找出序列中出現(xiàn)最多的元素示例代碼
這篇文章主要給大家介紹了關(guān)于利用Python找出序列中出現(xiàn)最多的元素的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-12-12
關(guān)于pygame自定義窗口創(chuàng)建及相關(guān)操作指南
對于開發(fā)一個(gè)游戲來說,窗口的顯示肯定是前提中的前提,對于pygame來說,只需要一小段代碼就可以初始化窗口,下面這篇文章主要給大家介紹了關(guān)于pygame自定義窗口創(chuàng)建及相關(guān)操作的相關(guān)資料,需要的朋友可以參考下2022-07-07

