來編寫你的 setup 腳本(二)

前言

一個 bug 導致的問題

在上周發布了《來編寫你的 setup 腳本(一)》之後,很巧的是我在維護的 aredis 經過了一次重構之後重新發布時使用的 setup 腳本出了問題,幸虧有 inytar 提醒,才發現了問題,具體經過可以看 issue。

問題出在 setup 腳本中的版本號。為了只在代碼中維護一個版本號,當新版本發布時只需要更新一個版本號而不會出現顧頭不顧尾的情況,我在 setup 腳本中引入了 aredis 源碼包中的版本號,這就導致在運行 pip install 安裝運行 setup 腳本時需要引入 aredis 的版本號(這將導致 aredis 的代碼被載入),而這個時候 aredis 還沒有被安裝成功,重構之後抽離出來的 commands 包被系統認為沒有相應代碼,於是導致安裝失敗。

那麼問題來了,這種 bug 怎麼測試比較好呢?

對於 setup 腳本來說我之前一直是用的直接運行 install 安裝的方法來測試的

python setup.py install n

但是在復現這個 bug 時失效了,安裝是可以順利進行的,這一塊暫時沒有去查明原因,有知道的同學還請一定告訴我 0- 0

之後通過運行下面命令終於可以成功復現問題(其實直接從 pypi pip install 就可以了)

python setup.py sdistnpip install dist/aredis-1.0.5.tar.gzn

這次的 bug 導致了什麼後果呢?

  1. 下載了1.0.4版本 aredis 的同學可能無法成功安裝,可能我寫的包的潛在用戶就少了許多(強行有許多)
  2. pypi 看似對重傳代碼文件是支持的,其實他的內心是拒絕的(T^T),於是我只能增加了版本號來重新上傳修復後的代碼以解決這個問題,今天新增的幾個命令就只能等到 1.0.6 來發布了(其實就是導致版本號紊亂了)

接下來是正文,緊接著來編寫你的 setup 腳本(一)的內容繼續翻譯官方文檔,中間穿插一些自己的看法,如果有寫得不對或者不夠清晰的地方還請指出,我會儘快修正的~

描述擴展模塊

寫一個 Python 擴展比用純 Python 編寫模塊要複雜一些,而對 Distutils 描述他們則更複雜那麼一點。不像是針對純 Python模塊那麼簡單,你不能期待僅僅列出模塊或者包然後等著 Distutils 為你找到正確的文件,你得指出擴展的名字、源碼,以及所有編譯或者連接所需要的東西(包括目錄,需要連接的庫等等)

所有這些都是通過 ext_modules 這個選項來完成的。 ext_modules 是一個擴展的列表。假定你的發布會包含一個名為 foo 的單一的拓展(用 foo.c 實現),如果沒有別的針對編譯器或者連接器的命令,那麼描述這個擴展是十分簡單的。

Extension(foo, [foo.c])n

代碼中的 Extension 類可以和 setup() 一起從 distutils.core 中引入。對於上述情況, setup 腳本大致如下面代碼所示:

from distutils.core import setup, Extensionnsetup(name=foo,n version=1.0,n ext_modules=[Extension(foo, [foo.c])],n )n

這個 Extension 類(實際上,潛在的擴展編譯機制是由 build_ext 命令來實現的)使得描述 Python 擴展極具靈活性, 它的優點會在下文予以介紹。

描述名字和包

Extension 構造函數的首先兩個參數是擴展的名字以及所包含的文件的名字,比如說:

Extension(foo, [src/foo1.c, src/foo2.c])n

這段代碼描述了在根目錄下的擴展。

Extension(pkg.foo, [src/foo1.c, src/foo2.c])n

而這段代碼描述了在 pkg 目錄下的擴展。源碼和編譯後產生的結果在兩個例子中都清楚可見,唯一的區別是產生的擴展在文件系統中的路徑(當然,還有由此帶來的不同的 Python 命名空間層級)

如果你在一個包裡面有多個擴展(或者只是單純在同一個目錄下),請在 setup 函數中使用 ext_package 關鍵詞,比如說:

setup(...,n ext_package=pkg,n ext_modules=[Extension(foo, [foo.c]),n Extension(subpkg.bar, [bar.c])],n )n

這樣做回將 foo.c 編譯到擴展 pkg.foo 中,而會把 bar.c 編譯到 pkg.subpkg.bar 中。

擴展的源碼

Extension 構造函數的第三個參數是一個指明源碼文件的列表。因為 Distutils 目前只支持 C,C++ 和 Objective-C 擴展,所以這些一半都是 C/C++/Objective-C 的源碼文件(請使用合適的擴展名來標明 C++ 文件,對於 .cc 和 .cpp, Unix 和 Windows 平台上的編譯器都可以識別)

除此之外,你也可以將 SWIG 介面(.i) 文件包含進列表,build_ext 命令知道如何處理 SWIG 擴展,他會用介面文件運行 SWIG 並且將產生的 C/C++ 文件編譯進你的擴展。

儘管會產生告警,SWIG 選項可以通過類似下面代碼展示的方法順利運行。

setup(...,n ext_modules=[Extension(_foo, [foo.i],n swig_opts=[-modern, -I../include])],n py_modules=[foo],n )n

或者像這樣運行命令行:

> python setup.py build_ext --swig-opts="-modern -I../include"n

在一些平台上,你可以引入經過編譯器處理的,擴展中用到的無源碼文件。目前這一項僅僅針對 Windows 消息文件(.mc) 和針對 Visual C++ 的源碼定義文件(.rc) 。這些文件會被編譯成為進位源文件(.res)並且連接為可執行的。

預處理選項

如果你需要指明目錄來搜索預處理器的 define 或 undefine 宏,有三個 Extension 類的可選參數將會很有用,他們分別是: include_dirs, define_macros, 和 undef_macros.

比如說,如果你的擴展需要根路徑下的 include 目錄下的頭文件的話,那麼可以用 include_dirs 選項:

Extension(foo, [foo.c], include_dirs=[include])n

你可以在其中指明目錄的絕對路徑。比如說,如果你知道你的擴展只會在 Unix 系統中用 安裝在 /usr 目錄下的 X11R6 來構建,你可以這麼寫:

Extension(foo, [foo.c], include_dirs=[/usr/include/X11])n

你應當避免在你的包中編寫不可移植的代碼,所以更好的做法也許是寫出這樣的 C 代碼:

#include <X11/Xlib.h>n

如果你需要引入別的 Python 擴展的頭文件,那麼你可以利用一點: Distutils 的 install_headers 命令會將頭文件安裝在一個固定的位置。比如說,Numerical 的 Python 頭文件是安裝在 /usr/local/include/python1.5/Numerical(具體位置會根據你的系統和 Python 安裝路徑相應地有所不同)在這個例子中,當你要構建 Python 擴展時,/usr/local/include/python1.5 這個路徑將一直存在於要搜索的路徑中,最好的做法是像這樣編寫 C 代碼:

#include <Numerical/arrayobject.h>n

如果你一定要將 Numerical 所在目錄包括進你的頭文件搜索路徑,你可以通過 disutils.sysconfig 來查找目錄:

from distutils.sysconfig import get_python_incnincdir = os.path.join(get_python_inc(plat_specific=1), Numerical)nsetup(...,n Extension(..., include_dirs=[incdir]),n )n

儘管這樣做已經使得你的代碼可移植性很強了,但是不得不說還是在編寫 C 代碼的時候謹慎一些會更好。

你可以用 define_macros 和 undef_macros 定義或者取消定義預處理器的宏。define_macros 的參數是一個 (name, value) 元組的列表,其中 name 是需要定義的宏的名字(string),而 value 是宏的值,可以是一個 string 或者 None(定義一個值為 None 的宏 FOO 與在你的 C 代碼中定義一個 #define FOO 是等價的: 對於絕大多數的編譯器,這將會把 FOO 定義為字元串類型的 「1」) undef_macros 只是一個指明取消定義的宏是什麼的列表。

舉個例子:

Extension(...,n define_macros=[(NDEBUG, 1),n (HAVE_STRFTIME, None)],n undef_macros=[HAVE_FOO, HAVE_BAR])n

上面的代碼相當於在每個C文件前加上了:

#define NDEBUG 1n#define HAVE_STRFTIMEn#undef HAVE_FOOn#undef HAVE_BARn

庫選項

你也可以指定構建你擴展時所要用的庫,以及這些庫所在目錄的路徑。libraries 選項是一個指定你需要用的庫的列表,而 library_dirs 是一個連接時指明這些庫所在目錄的列表, runtime_library_dirs 則是一個運行時(動態載入)搜索共享庫所在目錄的列表。

For example, if you need to link against libraries known to be in the standard library search path on target systems

比如說,如果你需要連接一個目標操作系統搜索路徑下的標準庫時可以:

Extension(...,n libraries=[gdbm, readline])n

如果你需要連接一個非標準路徑下的庫,你得先把路徑放入 library_dirs:

Extension(...,n library_dirs=[/usr/X11R6/lib],n libraries=[X11, Xt])n

別的選項

這裡還有一些別的選項用以處理特殊需求。

extra_objects 是一個傳遞給連接器的對象文件的列表。這些文件不應含有擴展,因為編譯器的默認擴展已經被使用了。

extra_compile_args 和 extra_link_args 可以被用來指定增加的編譯器和連接器命令選項。

export_symbols 只在 Windows 系統中生效。它可以包含一個列表的符號(函數或者變數) 來引出。這個選項在構建已經編譯過的擴展時是不需要的: Distutils 將會自動嚮導出的符號列表中加入 initmodule。

depends 是一個指明了擴展所依賴的文件的列表(比如說頭文件)。當這些文件與上一次構建時相比有所改變時,build 命令會調用編譯器根據源碼重新編譯擴展。

github 廣告時間

項目地址: NoneGG/aredis

aredis 是一款基於 Python async/await 原語的非同步的 redis 客戶端。

本周嘗試著使用 cython 加速,結果發現自己對於 cython 的使用方法還不夠熟練,於是打算先仔細閱讀官方文檔再進行。本周對於 aredis 只是增加了幾個 redis 3.2.0 之後引入的新命令的支持。

前排求 star 求 pr 求建議,如果大家有什麼好玩的點子歡迎給我提 issue,如果試用下來碰到什麼奇怪的問題也請一定給我反饋,我最晚會在每周周末的時候修復並且給個反饋。


推薦閱讀:

anaconda中如何安裝keras?
python中WindowsError: [Error 32] 錯誤處理
草根學 Python(二)基本數據類型和變數

TAG:Python | Python入门 | Python3x |