本文介紹了 setuptools 架構的內容,它是 PEAK 的一個副項目,它提供了比 distutils 更加簡單的包管理和發行功能。
開始
setuptools 模組很會 “規避”。例如,如果我們下載一個使用 setuptools 而不是使用 distutils 構建的包,那麼安裝就應該可以像我們期望的一樣工作:通常使用 python setup.py install 就可以。為了實現這種功能,使用 setuptools 綁定在一起的包就會在歸檔檔案中包含一個很小的引導模組 ez_setup.py。此處惟一需要注意的是 ez_setup.py 試圖在後台下載並安裝所需要的 setuptools —— 當然,這需要有一個串連網路的機器。如果 setuptools 早已在本地機器上安裝了,那麼這個後台步驟就不再需要執行;但是如果它需要手工進行安裝,那麼很多透明性就都丟失了。不過,大部分系統現在都有一個 網際網路連線了;為沒有串連網路的機器多執行幾個特殊步驟也並非特別麻煩。
setuptools 的真正優點並不在於實現 distutils 所能實現的功能 —— 儘管它 的確 增強了 distutils 的功能並簡化了 setup.py 指令碼中的內容。setuptools 最大的優勢是它在包管理能力方面的增強。它可以使用一種更加透明的方法來尋找、下載並安裝依賴包;並可以在一個包的多個版本中自由進行切換,這些版本都安裝在同一個系統上;也可以聲明對某個包的特定版本的需求;還可以只使用一個簡單的命令就能更新到某個包的最新版本。給人印象最為深刻的是,即使有些包的開發人員可能還從未考慮過任何 setuptools 相容性問題,我們依然可以使用這些包。
下面讓我們詳細探討一下。
引導
工具 ez_setup.py 是一個簡單的指令碼,它可以引導 setuptools 中其餘部分。有點讓人困惑的是,完整 setuptools 包中所提供的 easy_install 指令碼與 ez_setup.py 所實現的功能是相同的。不過前者假設 setuptools 早已安裝了,因此它會跳過幕後的安裝過程。這兩個版本都可以接受相同的參數和開關。
這個過程中的第一個步驟是下載一個小指令碼 ez_setup.py:
清單 1. 下載引導指令碼
% wget -q http://peak.telecommunity.com/dist/ez_setup.py
然後,就可以不帶任何參數運行指令碼來安裝 setuptools 中其餘部分了(如果不作為一個單獨的步驟來執行這個步驟,在首次安裝其他包時,它還是會被完成)。會看到類似於下面的內容(當然,這要取決於所使用的版本):
清單 2. 引導 setuptools
% python ez_setup.pyDownloading http://cheeseshop.python.org/packages/2.4/s/ setuptools/setuptools-0.6b1-py2.4.egg#md5=b79a8a403e4502fbb85ee3f1941735cbProcessing setuptools-0.6b1-py2.4.eggcreating /sw/lib/python2.4/site-packages/setuptools-0.6b1-py2.4.eggExtracting setuptools-0.6b1-py2.4.egg to /sw/lib/python2.4/site-packagesRemoving setuptools 0.6a11 from easy-install.pth fileAdding setuptools 0.6b1 to easy-install.pth fileInstalling easy_install script to /sw/binInstalling easy_install-2.4 script to /sw/binInstalled /sw/lib/python2.4/site-packages/setuptools-0.6b1-py2.4.eggProcessing dependencies for setuptools
完畢。這就是我們需要確保在系統上安裝 setuptools 而需要做的工作。
安裝包
對於很多 Python 包來說,要安裝這些包,需要做的就是將這些包的名字作為一個參數傳遞給 ez_setup.py 或 easy_install。既然目前已經使用引導指令碼載入了 setuptools,那就可以使用內部更加簡化的 easy_install(實際上它與我們選擇的版本的區別很小)了。
例如,假設希望安裝 SQLObject 包。過程非常簡單,如清單 3 所示。注意訊息中說 SQLObject 依賴於一個名為 FormEncode 的包;所幸的是,這會被很好地解決:
清單 3. 安裝一個典型的包
% easy_install SQLObjectSearching for SQLObjectReading http://www.python.org/pypi/SQLObject/Reading http://sqlobject.orgBest match: SQLObject 0.7.0Downloading http://cheeseshop.python.org/packages/2.4/S/ SQLObject/SQLObject-0.7.0-py2.4.egg#md5=71830b26083afc6ea7c53b99478e1b6aProcessing SQLObject-0.7.0-py2.4.eggcreating /sw/lib/python2.4/site-packages/SQLObject-0.7.0-py2.4.eggExtracting SQLObject-0.7.0-py2.4.egg to /sw/lib/python2.4/site-packagesAdding SQLObject 0.7.0 to easy-install.pth fileInstalling sqlobject-admin script to /sw/binInstalled /sw/lib/python2.4/site-packages/SQLObject-0.7.0-py2.4.eggProcessing dependencies for SQLObjectSearching for FormEncode>=0.2.2Reading http://www.python.org/pypi/FormEncode/Reading http://formencode.orgBest match: FormEncode 0.5.1Downloading http://cheeseshop.python.org/packages/2.4/F/ FormEncode/FormEncode-0.5.1-py2.4.egg#md5=f8a19cbe95d0ed1b9d1759b033b7760dProcessing FormEncode-0.5.1-py2.4.eggcreating /sw/lib/python2.4/site-packages/FormEncode-0.5.1-py2.4.eggExtracting FormEncode-0.5.1-py2.4.egg to /sw/lib/python2.4/site-packagesAdding FormEncode 0.5.1 to easy-install.pth fileInstalled /sw/lib/python2.4/site-packages/FormEncode-0.5.1-py2.4.egg
正如可以從這些訊息中看到的一樣,easy_install 要在 www.python.org/pypi/ 上尋找有關這個包的資訊,然後尋找真正可以下載它的地方(此處 egg 包就在 cheeseshop.python.org 上;後面將介紹有關 egg 的更多內容)。
現在不僅僅可以安裝某個包的最新版本(這是預設操作)。如果願意,還可以為 easy_install 提供一個特定的版本需求。現在讓我們嘗試安裝 SQLObject 的一個 post-beta 版本。
清單 4. 安裝某個包的最小版本
% easy_install 'SQLObject>=1.0'Searching for SQLObject>=1.0Reading http://www.python.org/pypi/SQLObject/Reading http://sqlobject.orgNo local packages or download links found for SQLObject>=1.0error: Could not find suitable distribution for Requirement.parse('SQLObject>=1.0')
如果(在本文編寫時情況就是如此)SQLObject 的最新版本小於 1.0,那麼這會什麼也不安裝。
安裝 “naive” 包
SQLObject 是可以識別 setuptools 的;但是如果要安裝一個尚未相容 setuptools 的包又該如何呢?例如,在本文之前,我從沒有對自己的 “Gnosis Utilities” 使用過 setuptools。不過,現在讓我們來嘗試安裝一下這個包,已知的只有它所在的 HTTP(或 FTP、SVN、CVS)位置(setuptools 可以理解所有這些協議)。我的下載 Web 網站上有各個 Gnosis Utilities 的版本,它們的命名採用了常見的版本風格:
清單 5. 安裝不識別 setuptools 的包
% easy_install -f http://gnosis.cx/download/Gnosis_Utils.More/ Gnosis_UtilsSearching for Gnosis-UtilsReading http://gnosis.cx/download/Gnosis_Utils.More/Best match: Gnosis-Utils 1.2.1Downloading http://gnosis.cx/download/Gnosis_Utils.More/ Gnosis_Utils-1.2.1.zipProcessing Gnosis_Utils-1.2.1.zipRunning Gnosis_Utils-1.2.1/setup.py -q bdist_egg --dist-dir /tmp/easy_install-CCrXEs/Gnosis_Utils-1.2.1/egg-dist-tmp-Sh4DW1zip_safe flag not set; analyzing archive contents...gnosis.__init__: module references __file__gnosis.magic.__init__: module references __file__gnosis.xml.objectify.doc.__init__: module references __file__gnosis.xml.pickle.doc.__init__: module references __file__gnosis.xml.pickle.test.test_zdump: module references __file__Adding Gnosis-Utils 1.2.1 to easy-install.pth fileInstalled /sw/lib/python2.4/site-packages/Gnosis_Utils-1.2.1-py2.4.eggProcessing dependencies for Gnosis-Utils
所幸的是 easy_install 可以把這一切都完成得很好。它會查看給定的下載目錄,識別出可用的最高版本,展開這個包,然後將其重新打包為 “egg” 格式,後者就可以用來進行安裝了。匯入 gnosis 現在可以在一個指令碼中運行。但是假設現在需要對 Gnosis Utilities 之前的某個特定版本來測試一個指令碼又該怎麼做呢?這也非常簡單:
清單 6. 安裝一個 “naive” 包的特定版本
% easy_install -f http://gnosis.cx/download/Gnosis_Utils.More/ "Gnosis_Utils==1.2.0"Searching for Gnosis-Utils==1.2.0Reading http://gnosis.cx/download/Gnosis_Utils.More/Best match: Gnosis-Utils 1.2.0Downloading http://gnosis.cx/download/Gnosis_Utils.More/ Gnosis_Utils-1.2.0.zip[...]Removing Gnosis-Utils 1.2.1 from easy-install.pth fileAdding Gnosis-Utils 1.2.0 to easy-install.pth fileInstalled /sw/lib/python2.4/site-packages/Gnosis_Utils-1.2.0-py2.4.eggProcessing dependencies for Gnosis-Utils==1.2.0
現在通常已經安裝了兩個版本的 Gnosis Utilities,當前活動版本是 1.2.0。將活動版本切換回 1.2.1 也非常簡單:
清單 7. 在系統範圍修改 “活動” 版本
% easy_install "Gnosis_Utils==1.2.1"Searching for Gnosis-Utils==1.2.1Best match: Gnosis-Utils 1.2.1Processing Gnosis_Utils-1.2.1-py2.4.eggRemoving Gnosis-Utils 1.2.0 from easy-install.pth fileAdding Gnosis-Utils 1.2.1 to easy-install.pth fileUsing /sw/lib/python2.4/site-packages/Gnosis_Utils-1.2.1-py2.4.eggProcessing dependencies for Gnosis-Utils==1.2.1
當然,這一次只能使一個版本是活動的。不過通過在各個指令碼上面放上這樣兩行類似內容,就可以讓指令碼選擇自己希望使用的版本:
清單 8. 在指令碼中使用某個版本的包
from pkg_resources import requirerequire("Gnosis_Utils==1.2.0")
通過使用上述要求,setuptools 就可以在運行 import 語句時添加一個特定的版本(如果指定了大於比較,就是最新的可用版本)。
讓包可以識別 setuptools
我會更希望讓使用者不需要知道 Gnosis Utilities 的下載目錄就可以安裝它。這 通常都可以 工作,因為 Gnosis Utilities 在 Python Cheeseshop 上有一個資訊清單。不幸的是,因為沒有考慮 setuptools ,所以我在 python.org 上為我的 Gnosis Utilities 建立了一個 “不匹配” 的入口 http://www.python.org/pypi/Gnosis%20Utilities/1.2.1。具體地說,這個歸檔檔案是根據類似於 Gnosis_Utils-N.N.N.tar.gz 的模式進行命名的(這些工具也打包成了 .zip 和 .tar.bz2 檔案,最新的幾個版本還打包成了 win32.exe 的安裝程式,所有這些檔案 setuptools 都可以很好地處理)。不過 Cheeseshop 上的項目名的拼字與 “Gnosis Utilities” 稍微有點不同。實際上,在 Cheeseshop 的一個很小的管理版本的更改就會將 http://www.python.org/pypi/Gnosis_Utils/1.2.1-a 建立為一個發布後版本。發行版歸檔檔案本身並沒有什麼變化,不過是在 Cheeseshop 裡增加了一點中繼資料。只需要少量努力,就可以使用更加簡單的安裝程式(注意,出於測試目的,我運行了一個 easy_install -m 來刪除所安裝的包)。
清單 9. 簡單增加對 setuptools 的識別
% easy_install Gnosis_UtilsSearching for Gnosis-UtilsReading http://www.python.org/pypi/Gnosis_Utils/Reading http://www.gnosis.cx/download/Gnosis_Utils.ANNOUNCEReading http://gnosis.cx/download/Gnosis_Utils.More/Best match: Gnosis-Utils 1.2.1Downloading [...]
我把這個過程剩餘的部分忽略掉了,因為這與您前面看到的內容沒什麼兩樣。惟一的區別在於 easy_install 要在 Cheeseshop(換言之 www.python.org/pypi/)上尋找可以匹配指定名字的中繼資料,並使用這些資訊來尋找真正的下載位置。在這種情況中,所列出的 .ANNOUNCE 檔案沒有包含任何有協助的內容,不過 easy_install 還會繼續查看另一個所列的 URL,這會證明它是一個下載目錄。
關於 egg
egg 是一個包含所有包資料的檔案包。在理想情況中,egg 是一個使用 zip 壓縮的檔案,其中包括了所有需要的包檔案。但是在某些情況下,setuptools 會決定(或被開關告知)包不應該是 zip 壓縮的。在這些情況下,egg 只是一個簡單的未曾壓縮的子目錄,但是裡面的內容是相同的。使用單一的版本可以方便地進行轉換,並可以節省一點磁碟空間,但是 egg 目錄從功能和組織圖上來說都是相同的。一直使用 JAR 檔案的 Java? 技術的使用者會發現 egg 非常熟悉。
由於最新的 Python 版本中(需要 2.3.5+ 或 2.4)匯入掛鈎的更改,可以簡單地通過設定 PYTHONPATH 或 sys.path 並像往常一樣匯入相應的包來使用 egg。如果希望採用這種方法,就不需要使用 setuptools 或 ez_setup.py 了。例如,在本文使用的工作目錄中,我就為 PyYAML 包放入了一個 egg。現在我就可以使用這個包了,方法如下:
清單 10. PYTHONPATH 上的 egg
% export PYTHONPATH=~/work/dW/PyYAML-3.01-py2.4.egg% python -c 'import yaml; print yaml.dump({"foo":"bar",1:[2,3]})'1: [2, 3]foo: bar
不過,PYTHONPATH 的(或者指令碼或 Python shell 會話內的 sys.path的)這種操作有些脆弱。egg 的發現最好是在新一點的 .pth 檔案中進行。在 site-packages/ 或 PYTHONPATH 中的任何 .pth 檔案都會進行解析來執行其他匯入操作,其方法類似於檢查可能包含包的那些目錄位置一樣。如果使用 setuptools 來處理包的管理功能,那麼在安裝、更新、刪除包時,就需要修改一個名為 easy-install.pth 的檔案。而且可以按照自己喜歡的方式對這個 .pth 進行命名(只要其副檔名是 .pth 即可)。例如,下面是我的 easy-install.pth 檔案的內容:
清單 11. 用作 egg 位置配置的 .pth 檔案
% cat /sw/lib/python2.4/site-packages/easy-install.pthimport sys; sys.__plen = len(sys.path)setuptools-0.6b1-py2.4.eggSQLObject-0.7.0-py2.4.eggFormEncode-0.5.1-py2.4.eggGnosis_Utils-1.2.1-py2.4.eggimport sys; new=sys.path[sys.__plen:]; del sys.path[sys.__plen:]; p=getattr(sys,'__egginsert',0); sys.path[p:p]=new; sys.__egginsert = p+len(new)
這種格式有點特殊:它近似於一個 Python 指令碼,但卻不完全是。需要說明的是,可以在那裡添加額外列出的 egg;更好的情況是,easy_install 會在運行時實現這種功能。也可以在 site-packages/ 下建立任意多個 .pth 檔案;每個都可以列出有哪些 egg 是可用的。
增強安裝指令碼
上面所述的這種安裝 setuptools naive 包的能力(請參閱 清單 6)只部分有效。也就是說,包 Gnosis_Utils 的確安裝上了,但是並不完整。所有常見的功能都可以工作,但是在自動產生 egg 時卻忽略了很多支援檔案 —— 大部分是副檔名為 .txt 的文檔和副檔名為 .xml 的測試檔案(還有一些其他的 README、.rnc、.rng、.xsl 和圍繞子包的檔案)。在安裝時,所有這些支援檔案都 “最好要有”,而沒有嚴格要求一定要有。不過,我們仍然希望能夠包含所有的支援檔案。
Gnosis_Utils 使用的 setup.py 指令碼實際上非常複雜。除了列出基本的中繼資料之外,在第 467 行代碼中,它還對 Python 版本的功能和 bug 進行完整測試;解決舊版本的 distutils 中的一些故障;回溯跳過對不支援部分的安裝(例如,如果 pyexpat 在 Python 發行版中並沒有包括);處理 OS 行結束符的轉換;建立多個歸檔/安裝程式類型;根據測試結果重新構建 MANIFEST 檔案。能夠實現處理這些工作的能力要感謝此包的另外一個維護人員 Frank McIngvale;這些能力可以讓 Gnosis_Utils 能成功安裝回 Python 1.5.1 的版本,當然前提是需要這麼做(早期版本中的功能沒有這麼豐富)。不過此處我要向大家展示的指令碼並沒有像 distutils 指令碼一樣做這麼複雜的事情:它只是簡單地假設系統中已經安裝了一個 “普通的” 最新版本的 Python。即使這麼講,setuptools 能讓安裝指令碼變得如此簡單還是非常吸引人。
在第一次嘗試時,讓我們來建立一個 setup.py 指令碼,它是從 setuptools 手冊中借用的,並試圖使用它來建立一個 egg:
清單 12. setuptools setup.py 指令碼
% cat setup.pyfrom setuptools import setup, find_packagessetup( name = "Gnosis_Utils", version = "1.2.2", packages = find_packages(),)% python setup.py -q bdist_eggzip_safe flag not set; analyzing archive contents...gnosis.__init__: module references __file__gnosis.doc.__init__: module references __file__gnosis.magic.__init__: module references __file__gnosis.xml.objectify.doc.__init__: module references __file__gnosis.xml.pickle.doc.__init__: module references __file__gnosis.xml.pickle.test.test_zdump: module references __file__
這點努力就已經可以起作用;至少可以部分地起作用。使用這幾行內容的確可以建立一個 egg,不過這個 egg 與使用 easy_install 建立的 egg 有一些相似的缺點:缺乏對不使用 .py 命名的檔案的支援。因此讓我們再試一次,只是需要稍微再努力一點:
清單 13. 添加缺少的 package_data
from setuptools import setup, find_packagessetup( name = "Gnosis_Utils", version = "1.2.2", package_data = {'':['*.*']}, packages = find_packages(),)
這就是需要做的所有操作。當然,根據實際情況,通常希望對它進行一些調整。例如,它可能會列出下面的內容:
清單 14. 打包特定類型檔案類型
package_data = {'doc':['*.txt'], 'xml':['*.xml', 'relax/*.rnc']}
這段內容翻譯一下就是:將 .txt 檔案包括在 doc/ 子包中,將 .xml 檔案包括在 xml/ 子包中,將所有 .rnc 檔案包括在 xml/relax/ 子包中。
結束語
本文實際上只介紹了用支援 setuptools 的發行版可以執行的定製操作的表層的知識。例如,假設您現在有一個發行版(可以是首選的 egg 格式或另外一種歸檔類型),您就可以使用一個命令將這個歸檔檔案和中繼資料上傳到 Cheeseshop 上。顯然,完整的 setup.py 指令碼應該包含舊版本 distutils 指令碼中所包含的同樣詳細的中繼資料;為了簡單起見,本文跳過了這些內容,但是其參數名與 distutils 是相容的。
儘管要完全適應 setuptools 所提供的巨大功能需要一些時間,但是實際上它確實可以讓維護您自己的包和安裝外來包都要比 distutils 更加簡單。如果您所關心的內容僅僅是安裝包,那麼您所需要瞭解的內容在本文的介紹中已經全部包括了;只是您在描述您自己的包時可能會發現一些複雜性,不過仍然沒有使用 distutils 那麼複雜。