📘 NOTE |
---|
你也遇到奇怪的安裝錯誤了嗎? 看看 Common Error 能不能解決你遇到的問題! |
📘 NOTE |
---|
你也正在學 JavaScript npm / pnpm 嗎? 搭配這篇⚡ JS 套件管理大師 一起服用,效果更好! |
📗 TIP |
---|
使用 pip install -e . (editable) 就可以將自己目前開發的套件,暫時安裝到本地 venv,這樣就可以一邊開發一邊測試! (記得開發完要重新再做一次此操作,才能看到改動的結果) |
egg
Python 的一種 二進制發佈格式 (binary distribution format),現已被 wheel 取代。
wheel
Python 的一種 二進制發佈格式 (binary distribution format),旨在加速套件的安裝過程。
相較於源代碼發佈格式 (如.tar.gz
),wheel 不需在安裝時進行編譯,因而可以節省安裝時間。
wheel 檔案的副檔名為.whl
(本質上是zip
格式的壓縮檔),是目前 Python 生態系中推薦的發佈格式。
PyPI
Python 的第三方套件集中地。全稱 Python Package Index。
TestPyPI
PyPI 的測試區,與 PyPI 是分開的,不用擔心會相互影響。
build frontend
負責調用構建過程的工具,通過與 build backend 交互來實際執行構建任務。
前端工具本身不處理具體的構建邏輯,它只是啟動後端的構建。
例如 :build
/pip
/tox
。
build backend
負責實際執行構建邏輯的工具,它會處理如何將源代碼轉換成可以安裝和發佈的格式 (
.whl
和.tar.gz
)。
例如 :setuptools
/flit
/poetry
/hatchling
/meson-python
。
pip
build
pyproject.toml
配置構建distutils
setuptools
wheel
.whl
twine
🚨 CAUTION |
---|
在 PyPI 上刪除一個 project,是無法刪除過往的 distribution 的。 這是為了避免混淆,確保已經下載過某個套件的使用者,未來不會錯誤地獲取一個相同名稱但內容不同的 distribution。 |
假設某 project 叫 pdfize,它從 0.1.1 版推進到 0.3.3 版後,就被刪除了, 而某天有另一團隊想開啟新 project ,名稱也剛好叫做 pdfize (可能與前者毫無相關), 那此團隊就不能上傳 0.1.1 版,因為這名稱的這版本已被使用過了。 |
sound_project/
├── pyproject.toml # configuration (or setup.py or setup.cfg)
| README.md
| LICENSE
└── soundcraft/ # top-level package
├── __init__.py
└── ...
└── tests/ # unit testing
├── __init__.py
└── ...
soundcraft/ # top-level package
├── __init__.py
├── formats/ # subpackage for file format conversions
│ ├── __init__.py
│ ├── wavread.py
│ ├── wavwrite.py
│ ├── aiffread.py
│ ├── aiffwrite.py
│ ├── auread.py
│ └── auwrite.py
├── effects/ # subpackage for sound effects
│ ├── __init__.py
│ ├── echo.py
│ ├── surround.py
│ └── reverse.py
└── filters/ # subpackage for filters
├── __init__.py
├── equalizer.py
├── vocoder.py
└── karaoke.py
📘 NOTE |
---|
這些 config 是用來設定 distribution 的 metadata |
📗 TIP |
---|
盡量使用 pyproject.toml 和 setup.cfg 這類的靜態 config,少用 setup.py 這類的動態 config (除非必要)。 |
🚨 CAUTION |
---|
目錄底下一定要有 __init__.py 才會被視為套件 |
setup.py
setup()
# In setup.py
from setuptools import find_packages, setup
with open("README.md", "r", encoding="utf-8") as f:
long_description = f.read()
setup(
# 套件名稱
# 當你安裝這個套件時,它將會使用這個名稱
name="pdfize",
# 套件版本
# 隨著套件的更新,版本會改變
version="0.3.3",
# 套件簡短描述
description="a simple tools for converting pdf to image.",
# 構建成發佈套件後應該呈現的目錄架構
# 詳見 packages 選項
packages=[...]
# 發佈套件目錄架構映射
# 詳見 package_dir 選項
package_dir={"...": "..."},
# 套件詳細描述
# 通常是從一個 README 檔案中讀取內容
long_description=long_description,
# 詳細描述格式
long_description_content_type="text/markdown",
# 參考網址
# 通常是指向套件的源代碼或官方網站
url="https://github.com/RogelioKG/PDFize",
# 套件作者
author="RogelioKG",
# 套件作者信箱
author_email="...@gmail.com",
# 套件維護者
maintainer="WilliamHuang",
# 套件維護者信箱
maintainer_email="...@gmail.com",
# 授權條款
license="MIT",
# 分類項目
# 方便 PyPI 索引
classifiers=[
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.11",
"Operating System :: OS Independent",
],
# 套件運行所需的依賴套件
install_requires=[
"altgraph>=0.17.4",
"click>=8.1.7",
"colorama>=0.4.6",
"pillow>=10.3.0",
"PyMuPDF>=1.24.9",
"PyMuPDFb>=1.24.9",
"pywin32-ctypes>=0.2.2",
"tqdm>=4.66.4",
],
# 套件額外的依賴套件
extras_require={
"dev": ["twine>=4.0.2"], # pip install pdfize[dev]
},
# 套件所需的 Python 版本
python_requires=">=3.11",
)
packages=
構建成 distribution 後應該呈現的目錄架構
Project Directory 中,呈現的是「project 的目錄架構」
但這裡指定的是「構建成發佈套件後應該呈現的目錄架構」
假如你希望發佈套件架構應該長這樣:soundcraft/ ├── __init__.py ├── formats/ │ ├── __init__.py │ ├── ... ├── effects/ │ ├── __init__.py │ ├── ... └── filters/ ├── __init__.py ├── ...
就應該給定
packages=[ "soundcraft", "soundcraft/formats", "soundcraft/effects", "soundcraft/filters", ]
此時若沒有給定 package_dir 選項,它就會直接映射。
也就是說:
- 發佈套件中的 soundcraft 目錄
對應的是 project 中的 soundcraft 目錄- 發佈套件中的 soundcraft/formats 目錄
對應的是 project 中的 soundcraft/formats 目錄- 發佈套件中的 soundcraft/effects 目錄
對應的是 project 中的 soundcraft/effects 目錄- 發佈套件中的 soundcraft/filters 目錄
對應的是 project 中的 soundcraft/filters 目錄
package_dir=
假如你希望發佈套件架構應該長這樣:
soundcraft/ ├── __init__.py ├── encodings/ │ ├── __init__.py │ ├── ... ├── modifiers/ │ ├── __init__.py │ ├── ... └── sifters/ ├── __init__.py ├── ...
你可以指定:
packages=[ "soundcraft", "soundcraft/encodings", "soundcraft/modifiers", "soundcraft/sifters", ]
但你的 project 套件目錄架構長得像 Project Directory,
此時你會希望它們有個映射關係可以對應,
這時候你就可以指定 package_dir 選項。
像這樣:
package_dir={ "soundcraft/encodings": "soundcraft/formats", "soundcraft/modifiers": "soundcraft/effects", "soundcraft/sifters": "soundcraft/filters", }
也就是說:
- 發佈套件中的 soundcraft 目錄
對應的是 project 中的 soundcraft 目錄- 發佈套件中的 soundcraft/encodings 目錄
對應的是 project 中的 soundcraft/formats 目錄- 發佈套件中的 soundcraft/modifiers 目錄
對應的是 project 中的 soundcraft/effects 目錄- 發佈套件中的 soundcraft/sifters 目錄
對應的是 project 中的 soundcraft/filters 目錄
find_packages()
在 project 中自動搜索套件
套件 (package) 和子套件 (subpackage) 間以
.
分隔,可使用*
作為 wildcard。
比如 Project Directory 中,
若要忽略soundcraft/effects/
,要給定exclude=["soundcraft.effects"]
。
若要忽略tests/
(單元測試),要給定exclude=["tests", "tests.*"]
。
where=
: 從哪個目錄搜索套件 (預設為 '.'
,即與 setup.py
同層目錄)include=
: 包含哪些套件 (預設為 ('*',)
,即包含所有套件)exclude=
: 排除哪些套件 (預設為 ()
,即不排除任何套件)pyproject.toml
☢️ WARNING |
---|
超級警告!!!如果你使用 setuptools 作為 build backend, 這裡搜索套件的匹配字串和 find_packages() 有小差異。 |
最後使用 * 時,前面不要加 . |
☢️ WARNING |
---|
當你在套件中包含 py.typed 時,這表示該套件的 type hints 可以被靜態類型檢查工具 (mypy) 正確地識別和使用。 |
如果套件沒有包含此檔案,而使用者嘗試進行 mypy 檢查,就會出現以下錯誤:error: Skipping analyzing "...": module is installed, but missing library stubs or py.typed marker [import-untyped] |
然後記得 == 要先在 virtual environment 下載 mypy (是誰那麼蠢會忘記,是的就是我!) |
[build-system]
requires = ["setuptools>=61.0.0", "wheel"]
build-backend = "setuptools.build_meta" # 使用 setuptools 作為 build backend
[tool.setuptools.packages.find]
include = ["pdfize*"]
exclude = ["pdfize.data*", "tests*"] # 注意不是 ["pdfize.data.*", "tests.*"]
[tool.setuptools.package-data]
pdfize = ["py.typed"] # 新版 setuptools 會自動包含 py.typed 或 .pyi
[project]
name = "pdfize"
version = "1.0.6"
description = "a simple tools for converting pdf to image."
authors = [{ name = "RogelioKG", email = "...@gmail.com" }]
license = { text = "MIT" }
requires-python = ">=3.11"
dependencies = [
"altgraph>=0.17.4",
"click>=8.1.7",
"colorama>=0.4.6",
"pillow>=10.3.0",
"PyMuPDF>=1.24.9",
"PyMuPDFb>=1.24.9",
"pywin32-ctypes>=0.2.2",
"tqdm>=4.66.4",
]
readme = { file = "README.md", content-type = "text/markdown" }
classifiers = [
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.11",
"Operating System :: OS Independent",
]
[project.urls]
Repository = "https://github.com/RogelioKG/PDFize"
Changelog = "https://github.com/RogelioKG/PDFize/blob/main/CHANGELOG.md"
[project.optional-dependencies]
dev = ["build>=1.2.1", "twine>=5.1.1"]
setup.cfg
…
MANIFEST.in
包含非 Python 相關目錄或檔案
Command | Description |
---|---|
include pat1 pat2 ... |
Add all files matching any of the listed patterns (Files must be given as paths relative to the root of the project) |
exclude pat1 pat2 ... |
Remove all files matching any of the listed patterns (Files must be given as paths relative to the root of the project) |
recursive-include dir-pattern pat1 pat2 ... |
Add all files under directories matching dir-pattern that match any of the listed patterns |
recursive-exclude dir-pattern pat1 pat2 ... |
Remove all files under directories matching dir-pattern that match any of the listed patterns |
global-include pat1 pat2 ... |
Add all files anywhere in the source tree matching any of the listed patterns |
global-exclude pat1 pat2 ... |
Remove all files anywhere in the source tree matching any of the listed patterns |
graft dir-pattern |
Add all files under directories matching dir-pattern |
prune dir-pattern |
Remove all files under directories matching dir-pattern |
☢️ WARNING |
---|
重新將套件構建為 distribution 時,構建工具可能會有一些快取行為。 如果發現構建結果有問題,先刪除 dist/ 和 build/ 和 *.egg-info/ 目錄,再重新構建一次。 |
setuptools
安裝套件到環境中
py setup.py install
將套件構建為 source distribution
產生 dist/ 和 *.egg-info/
py setup.py sdist
將套件構建為 binary distribution (egg 格式)
py setup.py bdist_egg
將套件構建為 binary distribution (wheel 格式)
產生 build/ 和 dist/ 和 *.egg-info/
py setup.py bdist_wheel
build
將套件構建為 distribution
產生 dist/ 和 *.egg-info/
py -m build
🚨 CAUTION |
---|
發佈到 TestPyPI 或 PyPI 需要 API key |
🚨 CAUTION |
---|
每次發佈都要更新 version,不可重複發佈已發佈過的 version |
twine
確認
twine check dist/*
發佈 (PyPI)
twine upload dist/*
發佈 (TestPyPI)
twine upload -r testpypi dist/*
發佈 (跳過已發佈過的舊版)
twine upload --skip-existing -r testpypi dist/*
pip
🔮 IMPORTANT : 相依性 dependency |
---|
安裝一個套件時,會自動安裝它的依賴套件 |
移除一個套件時,其依賴套件也會被移除,除非它被其他套件依賴 (🚨 CAUTION : pip 沒有自動移除依賴套件的功能,poetry 則有) |
🔮 IMPORTANT : 依賴解析 dependency resolution |
---|
套件依賴關係是一種有向無環圖 (DAG) |
每個套件對其依賴套件的版本有要求,不同版本之間對依賴套件的版本的要求可能不同 |
pip 在安裝或更新套件時,會解析整個依賴圖,確保所有版本相容,同個套件不會有兩個版本(🚨 CAUTION : Node.js 的 npm 比較特別,仰賴它自身 Node.js Module Resolution Algorithm 的特性,它可以做到同個套件有多個版本,詳見此處。 |
🔮 IMPORTANT : 依賴衝突 dependency conflicts |
---|
當多個套件對同一依賴有不同版本要求且無交集,會導致衝突 |
🚨 CAUTION : 安裝或升級會影響其他套件 |
---|
pip 在安裝或升級一個套件的時候,是有可能影響到其他套件的,這不僅限於它的直接相依套件 |
範例:比如原先有個套件 B ==1.0 ,它要求套件 A >=1.5,<2.0 ,現在它相依於套件 A ==1.5 ,今天新安裝了一個套件 C,它要求套件 A >=2.0 ,因此 pip 把套件 A 重裝為 2.0 ,這個套件 B 也就需要重裝。 |
🚨 CAUTION : 安裝差異 |
---|
一次安裝 : 一次解析所有套件依賴,避免衝突 |
分批安裝 : 可能會因新套件需求,導致已安裝套件的版本變動 |
install
options
-e
| --editable
: 可編輯模式,套件安裝為一個軟連結,而不是將其複製到 site-packages-i
| --index-url
: 從指定 Python package index 下載和安裝套件-r
| --requirement
: requirements.txt-t
| --target
: 下載到某個路徑-U
| --upgrade
: 將函式庫升級到最新版--extra-index-url
: –index-url 的備案選項--force-reinstall
: 強制重新安裝--no-cache-dir
: 強制從網路下載套件,而非從本地 cache 目錄抓取--pre
: 安裝預發佈套件 (比如 alpha / beta 版本)--no-deps
: 不進行依賴解析 (dependency resolution)arguments
name[extras_require]
cache
📘 NOTE |
---|
pip 在安裝套件時,若之前有下載過,不會從網路重新下載,而是使用本地 cache 來安裝,加快速度 |
Windows : C:\Users\username\AppData\Local\pip\Cache |
Linux : ~/.cache/pip |
dir
: 本地 cache 目錄位置purge
: 完全清除本地 cache☢️ ERROR: Could not find a version that satisfies the requirement ...
翻譯蒟蒻 : 找不到對應的版本
Scenario 1. 下載某個指定版本的套件
Solution :
可能是你 Python 版本太高,
你可以去 PyPI 找一下這套件這版本的 python_requires,
可能考慮 Python 要降版,或者套件要升版。
Scenario 2. 把自己寫的套件上傳到 TestPyPI 然後想載下來測試 (結果和你說找不到依賴套件的版本)
Solution :
簡單來說,
這是因為你套件的依賴套件沒有在 TestPyPI 發佈,所以 pip 找不到套件。
解決方法也很簡單,就再給 pip 一個備案 URL 就好
(由--extra-index-url
指定,叫它找不到去 PyPI 找)。
pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ package-name
Scenario 3. 把自己寫的套件上傳到 PyPI 然後想載下來使用 (結果和你說找不到你寫的套件的版本)
Solution :
剛上傳,要等一下,再試一次就可以了。
☢️ ERROR: THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS FILE ...
翻譯蒟蒻 : 下載後的檔案 hash 值與 PyPI 提供的 hash 值不一致
Solution :
pip 會檢查 hash 值,確保下載檔案的完整性與安全性。
發生這種錯誤表示下載過程中檔案可能遭到損毀或被惡意篡改。
大部分情況是網路不好,所以下載檔案損毀了。
☢️ ERROR: To modify pip, please run the following command: python -m pip install ...
翻譯蒟蒻 : 你現在要下載的這個套件,它的 dependency 之一就是 pip,而且還要求比你現在還高版的 pip
Solution :
你可能原本使用pip install ...
,
但使用python -m pip install ...
才有辦法自動更新 pip。
☢️ WARNING: Retrying (...) after connection broken by 'ReadTimeoutError(...)'
翻譯蒟蒻 : 你斷網了
Solution :
請連上網路。