在完成 Python 套件的開發後,版本控制和釋出是不可或缺的步驟。使用 Git 的 git tag 命令可以標記特定版本,並在 GitHub 上建立對應的發行版。接著,使用 poetry build 命令可以產生 sdist 和 wheel 兩種發行格式,方便使用者透過 pip install 安裝。為了確保套件的穩定性,建議先將套件釋出至 TestPyPI 進行測試,確認無誤後再正式釋出至 PyPI,供全球開發者使用。
3.9 使用版本控制標記套件版本
現在,我們已經完成了 pycounts 套件 v0.1.0 的所有原始檔案,包括 Python 程式碼、檔案和測試。接下來,我們將把這些原始檔案轉換成一個可以輕易分享和安裝的發行套件。不過,在這之前,如果您使用版本控制的話,標記一個版本是一個好的做法。如果您沒有使用版本控制,可以直接跳到第3.10節。
標記一個版本意味著我們永久地標記了儲存函式庫歷史中的某個特定點,並建立了一個可下載的「發行版」,包含了我們儲存函式庫在標記時的所有檔案。通常,每當您的套件有新的版本時,都會標記一個版本,我們將在第7章:發行和版本控制中進一步討論。
標記一個版本是一個兩步驟的過程,涉及 Git 和 GitHub:
- 使用
git tag命令建立一個標籤,標記儲存函式庫歷史中的某個特定點。 - 在 GitHub 上,根據您的標籤建立一個包含了您儲存函式庫所有檔案的發行版(通常是壓縮檔,如
.zip或.tar.gz)。其他人可以下載這個發行版,如果他們想檢視或使用您的套件在標籤建立時的狀態。
讓我們示範如何為 pycounts 套件的 v0.1.0 版本標記一個版本。首先,我們需要在命令列中使用以下 git 命令來建立一個標籤,並將其推播到 GitHub:
$ git tag v0.1.0
$ git push --tags
現在,如果您前往 GitHub 上的 pycounts 儲存函式庫,並導航到「Releases」標籤頁,您應該會看到如圖3.12所示的標籤。
要從這個標籤建立一個發行版,請點選「Draft a new release」。然後,您可以選擇要從哪個標籤建立發行版,並可選地新增一些關於該發行版的額外資訊,如圖3.13所示。
點選「Publish release」後,GitHub 將自動根據您的標籤建立一個發行版,包括 .zip 和 .tar.gz 格式的壓縮檔,如圖3.14所示。
直接從 GitHub 儲存函式庫安裝套件
擁有您 GitHub 儲存庫存取許可權的人可以直接使用 pip install 從儲存函式庫安裝您的套件。我們將在第4.3.4節中進一步討論這個話題。
3.10 建置和發行您的套件
3.10.1 建置您的套件
目前,我們的套件是一堆積難以與他人分享的檔案和資料夾。解決這個問題的方法是建立一個「發行套件」。發行套件是一個單一的壓縮檔,包含了安裝套件所需的所有檔案和資訊,使用像 pip 這樣的工具就可以安裝。
Python 中的主要發行格式有原始碼發行版(簡稱「sdists」)和 wheel。sdists 是所有原始碼檔案、中繼資料和建置指示的壓縮存檔,用於建構可安裝的套件版本。要從 sdist 安裝,使用者需要下載 sdist,解壓縮內容,然後按照建置指示在他們的電腦上建置並最終安裝套件。
使用 Plantuml 圖表來說明 sdists 和 wheel 的區別
@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle
title Python套件版本控制與釋出
package "軟體測試架構" {
package "測試層級" {
component [單元測試
Unit Test] as unit
component [整合測試
Integration Test] as integration
component [端對端測試
E2E Test] as e2e
}
package "測試類型" {
component [功能測試] as functional
component [效能測試] as performance
component [安全測試] as security
}
package "工具框架" {
component [pytest] as pytest
component [unittest] as unittest
component [Selenium] as selenium
component [JMeter] as jmeter
}
}
unit --> pytest : 撰寫測試
unit --> integration : 組合模組
integration --> e2e : 完整流程
functional --> selenium : UI 自動化
performance --> jmeter : 負載測試
note right of unit
測試金字塔基礎
快速回饋
高覆蓋率
end note
@enduml
相較之下,wheel 是預先建置好的套件版本。它們在開發者的機器上建置好後再分享給使用者。Wheel 是較為推薦的發行格式,因為使用者只需要下載 wheel,並將其移動到 Python 在他們電腦上搜尋套件的位置;不需要額外的建置步驟。
封裝 Python 套件的完整
建置與散佈套件
在 Python 中,pip install 可以處理從 sdist 或 wheel 安裝套件,我們將在 4.3 節中更詳細地討論這些主題。現在你需要知道的是,在散佈套件時,通常會建立 sdist 和 wheel 散佈版本。我們可以使用 poetry build 命令輕鬆地為套件建立 sdist 和 wheel。讓我們現在為我們的 pycounts 套件執行以下命令,從根套件目錄開始:
$ poetry build
Building pycounts (0.1.0)
- Building sdist
- Built pycounts-0.1.0.tar.gz
- Building wheel
- Built pycounts-0.1.0-py3-none-any.whl
內容解密:
poetry build命令用於建立套件的 sdist 和 wheel 散佈版本。- sdist(source distribution)是一種包含原始碼的壓縮檔,用於在不同環境中建置和安裝套件。
- wheel 是一種預先建置的套件格式,可以直接安裝,無需額外的建置步驟。
執行此命令後,您會在套件目錄中看到一個新的 dist/ 目錄,其中包含兩個新檔案:pycounts-0.1.0.tar.gz(sdist)和 pycounts-0.1.0-py3-none-any.whl(wheel)。使用者可以使用 pip install 安裝這些檔案。例如,要安裝 wheel(首選的散佈型別),您可以在終端機中輸入以下命令:
$ cd dist/
$ pip install pycounts-0.1.0-py3-none-any.whl
Processing ./pycounts-0.1.0-py3-none-any.whl
...
Successfully installed pycounts-0.1.0
內容解密:
pip install命令用於從 wheel 檔案安裝套件。- 安裝 wheel 無需額外的建置步驟,因此通常比安裝 sdist 更快。
若要使用 sdist 安裝,則必須先解壓縮 sdist 檔案,然後執行 pip install。具體步驟取決於您的作業系統。例如,在 Mac OS 上,可以使用 tar 命令解壓縮 sdist:
$ tar xzf pycounts-0.1.0.tar.gz
$ pip install pycounts-0.1.0/
Processing ./pycounts-0.1.0/
Installing build dependencies ... done
Getting requirements to build wheel ... done
Preparing wheel metadata ... done
...
Successfully built pycounts
Successfully installed pycounts-0.1.0
內容解密:
- 安裝 sdist 需要先解壓縮檔案,然後執行
pip install。 - 在安裝過程中,sdist 會被建置成 wheel,然後進行安裝。
釋出到 TestPyPI
在將套件釋出到 PyPI 之前,建議先釋出到 TestPyPI 進行測試。poetry 提供了 publish 命令,用於將套件釋出到指定的 repository。首先,我們需要將 TestPyPI 新增到 poetry 的 repository 清單中:
$ poetry config repositories.test-pypi https://test.pypi.org/legacy/
然後,使用以下命令將套件釋出到 TestPyPI:
$ poetry publish -r test-pypi
Username: TomasBeuzen
Password:
Publishing pycounts (0.1.0) to test-pypi
- Uploading pycounts-0.1.0-py3-none-any.whl 100%
- Uploading pycounts-0.1.0.tar.gz 100%
內容解密:
poetry config repositories.test-pypi命令用於將 TestPyPI 新增到poetry的 repository 清單中。poetry publish -r test-pypi命令用於將套件釋出到 TestPyPI。
釋出成功後,您可以存取 TestPyPI 上的套件頁面,並使用 pip install 命令安裝套件:
$ pip install --index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple \
pycounts
內容解密:
--index-url引數指定了要搜尋的套件索引 URL,在此為 TestPyPI。--extra-index-url引數指定了備用的套件索引 URL,在此為 PyPI,用於搜尋依賴套件。
釋出到 PyPI
如果您能夠成功地將套件釋出到 TestPyPI 並安裝無誤,那麼就可以將套件釋出到 PyPI。使用以下命令:
$ poetry publish
您的套件將會在 PyPI 上可用,並且任何人都可以使用 pip install 命令安裝:
$ pip install pycounts
如何封裝 Python 套件:深入解析與實務應用
在前一章中,我們已經對如何建立、安裝和分發 Python 套件有了初步的瞭解。本章將探討 Python 套件的結構、安裝和分發機制,並進一步開發我們的 pycounts 套件。
Python 套件基礎
首先,我們來探討 Python 中的模組和套件是如何被表示和使用的。Python 中的所有資料都是以物件或物件之間的關係來表示的。例如,整數和函式都是 Python 物件。我們可以使用 type() 函式來找出 Python 物件的型別。
>>> a = 1
>>> type(a)
int
>>> def hello_world(name):
... print(f"Hello world! My name is {name}.")
>>> type(hello_world)
function
內容解密:
type()函式用於取得物件的型別。int和function分別代表整數和函式這兩種物件型別。- 這裡展示瞭如何定義一個簡單的函式並檢查其型別。
在 Python 中,模組(module)是一種重要的物件,用於組織 Python 程式碼。通常,我們會將想要重用的程式碼儲存在一個 .py 檔案中,並使用 import 陳述式來匯入它。這個過程會建立一個與匯入檔案同名的模組物件(不包括 .py 字尾),我們可以透過這個物件來存取檔案中的內容。
例如,假設我們有一個名為 greetings.py 的模組,其中包含了一些列印 “Hello World!” 的函式:
def hello_world():
print("Hello World!")
def hello_world_squamish():
print("I chen tl'iḵ!")
內容解密:
- 定義了兩個函式:
hello_world()和hello_world_squamish()。 - 這兩個函式分別列印出 “Hello World!” 和 “I chen tl’iḵ!"。
- 使用
import陳述式匯入greetings模組後,可以透過greetings.hello_world()或greetings.hello_world_squamish()來呼叫這些函式。
我們可以匯入這個模組並使用 type() 函式來驗證是否建立了一個模組物件:
>>> import greetings
>>> type(greetings)
module
內容解密:
- 使用
import greetings匯入模組。 - 使用
type(greetings)檢查greetings的型別,結果是module,表明它是一個模組物件。 - 模組物件可以用於存取其內容,例如使用
greetings.hello_world()。
參考資料與工具
- 使用
cookiecutter建立套件結構 - 使用
poetry管理依賴和釋出套件 - 使用
sphinx生成檔案
重點回顧
- Python 物件和模組:瞭解 Python 中的物件和模組概念。
- 套件結構:學習如何組織和管理 Python 套件。
- 模組匯入:掌握如何匯入和使用模組中的函式和變數。
這些知識對於開發和管理 Python 套件至關重要。透過實踐這些步驟,你將能夠建立、安裝和分發自己的 Python 套件。
Python 套件結構與名稱空間基礎
Python 的名稱空間(namespace)是一個將名稱對映到物件的機制。在上述程式碼範例中,我們新增了 a(整數)、hello_world(函式)和 greetings(模組)到目前的名稱空間中,並可以使用這些名稱來參照我們所建立的物件。dir() 函式可用於檢查名稱空間。當不帶任何引數呼叫時,dir() 會傳回目前名稱空間中定義的名稱列表:
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'hello_world', 'greetings']
在輸出結果中,我們可以看到在本文中定義的三個物件的名稱:a、hello_world 和 greetings。其他被雙下劃線包圍的名稱是當我們啟動 Python 直譯器時自動初始化的物件,是實作細節,在此不作詳細討論,但可以在 Python 檔案中閱讀相關內容。
內容解密:
dir()函式用於檢查目前名稱空間中的名稱。- 名稱
a、hello_world和greetings是由使用者定義的。 - 其他名稱(如
__annotations__、__builtins__等)是由 Python 直譯器自動初始化的。
模組與名稱空間
當使用 import 陳述式匯入模組時,會建立一個模組物件,並且該物件具有自己的名稱空間,其中包含在模組中定義的 Python 物件的名稱。例如,當我們匯入 greetings.py 檔案時,我們建立了一個 greetings 模組物件和名稱空間,其中包含檔案中定義的物件名稱——hello_world 和 hello_world_squamish——我們可以使用點表示法存取這些名稱,例如 greetings.hello_world() 和 greetings.hello_world_squamish()。
>>> dir(greetings)
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'hello_world', 'hello_world_squamish']
內容解密:
- 使用
import陳述式匯入模組時會建立模組物件和其名稱空間。 - 模組物件的名稱空間包含在該模組中定義的 Python 物件名稱。
- 可以使用點表示法(如
greetings.hello_world())存取模組中的物件。
套件結構
套件只是由一個或多個模組組成的集合。它們通常被結構化為一個目錄(套件),其中包含一個或多個 .py 檔案(模組)和/或子目錄(子套件)。一個名為 __init__.py 的特殊檔案用於告知 Python 該目錄是一個套件(而不是電腦上的普通目錄)。以下是一個簡單的套件結構範例,包含兩個模組和一個子套件:
pkg
├── __init__.py
├── module1.py
└── subpkg
├── __init__.py
└── module2.py
內容解密:
- 套件是一或多個模組的集合,通常以目錄形式存在。
- 目錄中必須包含
__init__.py檔案,以告知 Python 該目錄是一個套件。 - 套件可以包含子目錄(子套件),子目錄同樣需要包含
__init__.py檔案。
匯入套件與模組
無論是匯入單一模組(.py 檔案)還是套件(包含一或多個 .py 檔案的目錄),Python 都會在目前名稱空間中建立一個模組物件。例如,讓我們匯入在第 3 章中建立的 pycounts 套件:
>>> import pycounts
>>> type(pycounts)
module
內容解密:
- 無論匯入單一模組還是套件,Python 都會建立一個模組物件。
- 可以使用點表示法存取套件或模組中的內容。
路徑搜尋
當匯入套件或模組時,Python 會在預設的目錄列表(定義在 sys.path 中)中搜尋它。但是,當從套件中匯入某個東西時,Python 使用的是該套件的 __path__ 屬性,而不是 sys.path 中的路徑。
>>> sys.path
['', '/opt/miniconda/base/envs/pycounts/lib/python39.zip', '/opt/miniconda/base/envs/pycounts/lib/python3.9', '/opt/miniconda/base/envs/pycounts/lib/python3.9/lib-dynload', '/opt/miniconda/base/envs/pycounts/lib/python3.9/site-packages']
>>> pycounts.__path__
['/Users/tomasbeuzen/pycounts/src/pycounts']
內容解密:
sys.path定義了 Python 搜尋模組和套件的預設目錄列表。- 當匯入套件中的內容時,Python 使用該套件的
__path__屬性進行搜尋。