Pyproject.toml 手把手教學
前言
在過去,Python 的世界裡安裝套件、打包專案各有各的工具(setup.py、requirements.txt、Pipfile 等),生態系相當分散。pyproject.toml 是 Python 社群為了統一這些標準而誕生的格式,從 PEP 518 開始逐漸成為官方推薦的專案設定入口。它就像專案的「身分證」與「說明書」,集中描述專案名稱、依賴套件、建置方式等所有關鍵資訊。
核心結構
pyproject.toml 主要由以下兩個區塊組成:
[build-system]:宣告要用哪個工具來打包專案(常見為setuptools或hatchling)。[project]:最核心的區塊,包含專案名稱、版本、作者,以及執行時所需的依賴套件(dependencies)。
建立 pyproject.toml
Step 1:建立專案資料夾
以一個需要 requests 套件的爬蟲程式為例,建立以下目錄結構:
1 | my_project/ |
Step 2:撰寫 pyproject.toml
在 pyproject.toml 中填入以下內容:
1 | [build-system] |
[build-system]說明這個區塊告訴 Python 打包工具鏈如何處理你的專案:
requires:打包前需要安裝的工具,例如setuptools。build-backend:執行打包時呼叫的核心引擎,此處使用setuptools的標準後端。
Step 3:安裝依賴
現代工具可以直接讀取 pyproject.toml 並自動安裝所有依賴,不需要手動逐一 pip install。
方式一:使用 pip(現代版本)
在 my_project/ 目錄下執行:
1 | pip install . |
方式二:使用 uv(推薦)
uv 是目前 Python 生態系中速度最快的套件管理工具,完整支援 pyproject.toml:
1 | uv sync |
uv sync 會自動建立虛擬環境(.venv)、讀取 pyproject.toml,並安裝所有需要的套件。
進階配置
依賴分組(dependency-groups)
為什麼要分組? 正式執行時只需要 requests;開發過程中還需要 pytest、black 等工具。這些開發工具不應該被打包進最終產品,否則會增加不必要的體積。
做法是透過 [dependency-groups] 區塊區隔「開發/測試用依賴」:
1 | [dependency-groups] |
安裝時加上 --group 參數即可安裝指定群組:
1 | uv sync --group dev |
多來源套件索引(uv index)
為什麼需要多個來源?
大多數公開套件從 PyPI.org 下載,但公司內部的私有套件庫或 GitHub 上的測試版本無法從 PyPI 取得。uv 允許你在設定檔中定義多個套件來源,依序搜尋。
宣告私有套件索引
使用 [[tool.uv.index]](注意雙括號,代表這是一個清單,可設定多個):
1 | [[tool.uv.index]] |
指定套件來源
使用 [tool.uv.sources] 強制特定套件從指定索引下載:
1 | [tool.uv.sources] |
注意:
[tool.uv.sources]左側的 key 必須與[project]的dependencies中的套件名稱完全一致。
除了指定 index,[tool.uv.sources] 也支援其他來源類型:
1 | [tool.uv.sources] |
搜尋優先順序
uv 決定套件來源的優先順序如下:
[tool.uv.sources]明確指定(最優先):直接去指定的 index 下載,忽略其他設定。[[tool.uv.index]]清單順序:若無明確指定,依清單順序逐一詢問各索引,找到即下載。- 預設 PyPI(最後防線):若未設定任何 index,回落至官方 PyPI.org。
憑證管理
重要:請勿將帳號密碼直接寫入
pyproject.toml,這會導致憑證被提交至 Git 並外洩。
推薦以下兩種安全方式管理私有索引的憑證:
方式一:使用 .netrc 檔案(推薦)
在使用者根目錄建立 ~/.netrc(Windows 為 %USERPROFILE%/_netrc):
1 | machine pypi.company.com |
uv 會自動偵測並使用此檔案中的憑證。
方式二:使用環境變數
在執行 uv sync 前設定以下環境變數(MY_COMPANY 對應 [[tool.uv.index]] 中設定的 name,轉為全大寫):
1 | export UV_INDEX_MY_COMPANY_USERNAME=your_username |
防範依賴混淆攻擊(Dependency Confusion)
當私有套件與公開套件同名時,攻擊者可能在 PyPI 上發布同名的惡意套件。建議的防護策略:
- 統一命名前綴:公司內部套件一律加上前綴,例如
mycorp-utils、mycorp-api。 - 私有倉庫優先:將公司索引放在
[[tool.uv.index]]清單的最前面。 - 使用 Lock 檔案:
uv會在首次安裝後產生uv.lock,精確記錄每個套件的來源 index、版本號與雜湊值(Hash)。後續執行uv sync時直接參照此檔案,確保環境完全可重現、不受搜尋順序影響。
pytest 設定(tool.pytest.ini_options)
[tool.pytest.ini_options] 讓你將 pytest 的執行參數集中寫在 pyproject.toml,不需要在每次執行時手動附加參數。以下介紹幾個專業開發中最常用的設定。
控制測試輸出
當測試數量增多,可透過 addopts 設定預設的執行參數:
1 | [tool.pytest.ini_options] |
自訂測試檔案命名規則
pytest 預設只掃描 test_*.py 或 *_test.py。若團隊有其他命名習慣,可透過 python_files 調整:
1 | [tool.pytest.ini_options] |
過濾警告訊息
第三方套件有時會輸出大量 DeprecationWarning,掩蓋真正的錯誤。可透過 filterwarnings 過濾:
1 | [tool.pytest.ini_options] |
測試分類標記(markers)
markers 讓你為測試加上自訂標籤,執行時可依標籤篩選:
1 | pytest # 執行所有測試 |
在 pyproject.toml 中宣告所有自訂標記,避免 pytest 發出未知 marker 的警告:
1 | [tool.pytest.ini_options] |
設定預設不執行 E2E 測試
E2E 測試通常耗時較長,本地開發時不需要每次都跑。將排除規則寫入 addopts,讓 pytest 指令預設跳過 e2e:
1 | [tool.pytest.ini_options] |
若某次需要臨時執行所有測試(包含 E2E),可在指令列明確覆蓋:
1 pytest -m ""指令列傳入的
-m參數優先級高於pyproject.toml的addopts。
若 E2E 測試只應在 CI/CD 環境執行,搭配 pytest.mark.skipif 與環境變數是更明確的做法:
1 | import os |
1 | # 本地執行(跳過 e2e) |
環境變數注入(pytest-env)
需要在測試時注入固定的環境變數(如測試用資料庫連線),可安裝 pytest-env 並在 pyproject.toml 設定:
1 | [tool.pytest.ini_options] |
注意:
pytest-env的設定是靜態的,無法動態切換.env檔案(例如依環境讀取.env.test或.env.prod)。若有此需求,改用pytest-dotenv插件,它支援指定要載入的.env檔案路徑。
完整範例
以下是一份結合私有索引的完整 pyproject.toml 範例:
1 | [build-system] |