大多數Python開發者至少都寫過一個像工具、指令碼、庫或框架等對其他人也有用的工具。我寫這篇文章的目的是讓現有Python程式碼的開源過程儘可能清晰和無痛。我不是簡單的指——“建立一個GitHub庫,提交,在Reddit上釋出,每天呼叫它”。在本文的結尾,你可以把現有的程式碼轉換成一個能夠鼓勵他人使用和貢獻的開源專案。
然而每一個專案都是不同的,但其中將現有程式碼開源的流程對所有的Python專案都是類似的。在另一個受歡迎的文章系列裡我寫了“以正確方式開始一個Django專案”,我將概述在開源Python專案我發現的有必要的步驟。
更新 (8月17號): 感謝@pydann提醒我Cookiecutter的存在,@audreyr的一個不起的專案。我在文章結尾新增了其中的一段。看一下Audrey的專案吧!
更新 2 (8月18號):感謝@ChristianHeimes(和其他人)關於ontox這一段。Christian也讓我想起了PEP 440和其他一些都已實現很棒的改進建議。
工具和概念
特別是,我發現一些工具和概念十分有用或者說是必要的。下面我就會談及這方面主題,包括需要執行的精確的命令和需要設定的配置值。其終極目標就是讓整個流程簡單明瞭。
- 專案佈局(目錄結構)
- setuptools 和 setup.py檔案
- git版本控制
- GitHub 專案管理
- GitHub的”Issues” 如下作用:
- bug跟蹤
- 請求新特性
- 計劃好的新特性
- 釋出或者版本管理
- GitHub的”Issues” 如下作用:
- git-flow git工作流
- py.test 單元測試
- tox 標準化測試
- Sphinx 自動生成HTML文件
- TravisCI 持續測試整合
- ReadTheDocs 持續文件整合
- Cookiecutter 為開始下一個專案自動生成這些步驟
專案佈局
當準備一個專案時,正確合理的佈局(目錄結構)是十分重要的。一個合理的佈局意味著想參與開發者不必花時間來尋找某些程式碼的位置; 憑直覺就可以找到檔案的位置。因為我們在處理一個專案,就意味著可能需要到處移動一些東西。
讓我們從頂層開始。大多數專案都有很多頂層檔案(如setup.py, README.md, requirements等等)。每個專案至少應該有下面三個目錄:
- doc目錄,包括專案文件
- 專案目錄,以專案命名,儲存實際的Python包
- test目錄,包含下面兩部分
- 在這個目錄下包括了測試程式碼和資源
- 作為一個獨立頂級包
為了更好理解檔案該如何組織,這裡是一個我的簡單專案:sandman 佈局快照。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
$ pwd ~/code/sandman $ tree . |- LICENSE |- README.md |- TODO.md |- docs | |-- conf.py | |-- generated | |-- index.rst | |-- installation.rst | |-- modules.rst | |-- quickstart.rst | |-- sandman.rst |- requirements.txt |- sandman | |-- __init__.py | |-- exception.py | |-- model.py | |-- sandman.py | |-- test | |-- models.py | |-- test_sandman.py |- setup.py |
如你所看到那樣,這裡有一些頂層檔案,一個docs目錄(建立一個空目錄,因為sphinx會將生成的文件放到這裡),一個sandman目錄,以及一個在sandman目錄下的test目錄。
setuptools 和 setup.py檔案
setup.py檔案,你可能已經在其它包中看到過,被distuils包用來安裝Python包的。對於任何一個專案,它都是一個很重要的檔案,因為它包含了版本,包依賴資訊,PyPi需要的專案描述,你的名字和聯絡資訊,以及其它一些資訊。它允許以程式設計的方式搜尋安裝包,提供後設資料和指令說明讓工具如何做。
setuptools包(實際上就是對distutils的增強)簡單化了建立釋出python包。使用setuptools給python包打包,和distutils打包沒什麼區別。這實在是沒有任何理由不使用它。
setup.py應該放在你的專案的根目錄。setup.py中最重要的一部分就是呼叫setuptools.setup,這裡麵包含了此包所需的所有元資訊。這裡就是sandman的setup.py的所有內容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
from __future__ import print_function from setuptools import setup, find_packages from setuptools.command.test import test as TestCommand import io import codecs import os import sys import sandman here = os.path.abspath(os.path.dirname(__file__)) def read(*filenames, **kwargs): encoding = kwargs.get('encoding', 'utf-8') sep = kwargs.get('sep', 'n') buf = [] for filename in filenames: with io.open(filename, encoding=encoding) as f: buf.append(f.read()) return sep.join(buf) long_description = read('README.txt', 'CHANGES.txt') class PyTest(TestCommand): def finalize_options(self): TestCommand.finalize_options(self) self.test_args = [] self.test_suite = True def run_tests(self): import pytest errcode = pytest.main(self.test_args) sys.exit(errcode) setup( name='sandman', version=sandman.__version__, url='http://github.com/jeffknupp/sandman/', license='Apache Software License', author='Jeff Knupp', tests_require=['pytest'], install_requires=['Flask>=0.10.1', 'Flask-SQLAlchemy>=1.0', 'SQLAlchemy==0.8.2', ], cmdclass={'test': PyTest}, author_email='jeff@jeffknupp.com', description='Automated REST APIs for existing database-driven systems', long_description=long_description, packages=['sandman'], include_package_data=True, platforms='any', test_suite='sandman.test.test_sandman', classifiers = [ 'Programming Language :: Python', 'Development Status :: 4 - Beta', 'Natural Language :: English', 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', 'Operating System :: OS Independent', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Software Development :: Libraries :: Application Frameworks', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', ], extras_require={ 'testing': ['pytest'], } ) |
(感謝Christian Heimes的建議讓setup.py更符合人們的語言習慣。反過來,也讓我借用其它的專案一目瞭然了。)
大多數內容淺顯易懂,可以從setuptools文件檢視到,所以我只會觸及”有趣”的部分。使用sandman.__version__和gettinglong_description方法(儘管我也記不住是哪一個,但是卻可以從其它專案的setup.py中獲得)來減少我們需要寫的引用程式碼。相反,維護專案的版本有三個地方(setup.py, 包自身的__version__, 以及文件),我們也可以使用包的version來填充setup裡面的version引數
long_description被Pypi在你專案的PyPI主頁當做文件使用。這裡有其他一個檔案,README.md,其中包含幾乎相同的內容,我使用pandoc依據README.md自動生成README.rst,因此我們只需看README.rst就行了,並將它的內容設定為long_description。
py.test (上面討論過) 中有一個特殊的條目(pytest類)設定允許Python檢查setup.py可否正常工作。這段程式碼直接來自py.test指導文件。
檔案中的其他內容都是在設定文件中描述的安裝引數。
其他的setup.py引數
有一些sandman 用不到的啟動引數,在你的包裡可能會用到。舉個例子,你可能正在分派一些指令碼並希望你的使用者能夠從命令列執行。在這個例子中,指令碼會和你其他的程式碼一起安裝在正常的site-packages位置。使用者安裝完後,沒有其他的簡單方法執行它。基於這一點,setup可以帶有一個的指令碼引數來指明Python指令碼應該如何安裝。在包中安裝一個呼叫go_foo.py的指令碼,這個用來啟動的呼叫包括下面這行:
1 |
scripts = ['go_foo.py'], |
確保在指令碼中填入相對路徑,並不僅僅是一個名稱 (如scripts = [‘scripts/foo_scripts/go_foo.py’]).同樣,你的指令碼應該以”shebang”行和”python”開始,如下:
1 |
#! /usr/bin/env python |
distutils將會在安裝過程中自動用當前直譯器位置取代這一行。
如果你的包比我們這裡討論的要複雜,你可在官方文件中參看啟動工具文件和分佈python模組。
在這兩者中,你可以解決一些你可能會遇到的問題。
程式碼管理:git, 專案管理:gitHub
在“以正確的方式開始一個Django專案”中,我建議版本控制使用git 或者 mercurial。如果對於以共享與貢獻的專案來說,只有一個選擇:git。事實上,從長遠來說,如果你想人們能使用和參與貢獻,那麼不僅使用git很有必要,而且,你也能夠使用GitHub來管理維護你的專案。
這並不是誇大其詞(儘管很多人會以它為嚼頭)。然而,管它好與差,git和GitHub事實上已經成為了開源專案的實際標準了。GitHub是很多潛在的貢獻者最想註冊的和最熟悉的。所以,我深信,這並不是掉以輕心,而是深思熟慮的產物。
新建一個README.md檔案
在GitHub的程式碼倉庫中,專案的描述是從專案的根目錄中的:README.md檔案獲取的。這個檔案應該包含下面幾點:
- 專案描述
- 專案ReadTheDocs頁面連線[@Lesus 注:請檢視 工具與概念 ]
- 一個用來顯示當前構建狀態的TravisCI按鈕。
- “Quickstart” 文件 (怎麼快速安裝和使用你的專案)
- 若有非python依賴包,請列舉它以及怎麼安裝它
它(README)讀起來很傻的感覺,但是確是一個很重要的檔案。它可能是你未來的使用者或者貢獻者首先從它瞭解你的專案的。花些時間來寫一個清楚明白的說明和使用GFM(GitHubFlavoredMarkdown)來使它更好看。實際上,如果使用原生的Markdown來寫文件不爽,那麼可以在Github上使用立即預覽來建立或者修改這個檔案
我們還沒觸及列表中的第二和第三項(ReadTheDocs和TravisCI),你會在接下來看到。
使用”Issues”頁
跟生活中的很多事情一樣,你投入GitHub越多,你收穫的越多。因為使用者會使用GitHub的“Issues”頁面反饋bug,使用該頁面跟蹤特性要求和改進是很有意義的。
更重要的是,它允許貢獻者以一種優雅的方式看到:一個可能實現特性的列表以及自動化的管理合並請求流程(pull request)。GitHub的issues可以與評論、你專案裡的其他issues及其他專案裡的issues等交織,這使得“issues”頁面成為一個有關所有bug修復、改進和新特性要求資訊彙總的地方。
確保“Issues”及時更新,至少及時回應新的問題。作為一個貢獻者,沒有什麼比修復bug後看著它呈現在issues頁面並等待著被合併更有吸引力的了。
使用git-flow這個明智的git工作流
為使事情對自己和貢獻者更容易,我建議使用非常流行的git-flow分支模型。
概述
開發分支是你工作的主要分支,它也是將成為下一個release.feature的分支,代表著即將實現的新特性和尚未部署的修復內容(一個完整的功能分支有開發分支合併而來)。通過release的建立更新master。
安裝
按照你係統平臺的git-flow安裝指導操作,在這裡。
安裝完後,你可以使用下附命令遷移你的已有專案
1 |
$ git flow init |
Branch細節
指令碼將詢問你一些配置問題,git-flow的預設建議值可以很好的工作。你可能會注意到你的預設分支被設定成develop。現在,讓我們後頭描述一下git-flow…嗯,flow,更詳細一點。這樣做的最簡單的方法是討論一下不同的分支及模型中的分支型別。
Master
master分支一直是存放“生產就緒”的程式碼。所有的提交都不應該提交到master分支上。當然,master分支上的程式碼只會從一個產品釋出分支建立並結束後合併進來。這樣在master上的程式碼一直是可以釋出為產品的。並且,master也是一直處於可預計的狀態,所以你永遠不需要擔心如果master分支修改了而某一個其他分支沒有相應的修改。
Develop
你的大部分工作是在develop分支上完成的。這個分支包含所有的完成的特性和修改的bug以便釋出;每日構建或者持續整合伺服器需要針對develop分支來進行,因為它代表著將會被包含在下一個釋出裡的程式碼。
對於一次性的提交,可以隨便提交到develop上。
特性
對於一些大的特性,就需要建立一個特性分支。特性分支從develop分支建立出來。它們可以是對於下一個釋出的一些小小的增強或者更進一步的修改。而這,依然需要從現在開始工作。為了從一個新的分支上開始工作,使用:
1 |
$ git flow feature start <feature name> |
這命令建立了一個新的分支:feature/<feature name>。通常會把程式碼提交到這個分支。當特性已經完成並且準備好釋出的時候,它就應當用一下的命令將它合併會develop分支:
1 |
git flow feature finish <feature name> |
這會把程式碼合併進develop分支,並且刪除 feature/<feature name>分支
Release
一個release分支是當你準備好進行產品釋出的時候從develop分支建立出來的。使用以下的命令來建立:
1 |
$ git flow release start <release number> |
注意,這是釋出版本號第一次建立。所有完成的,準備好釋出的分支必須已經合併到develop分支上。在release分支建立後,釋出你的程式碼。任何小的bug修改需要提交到 release/<release number> 分支上。當所有的bug被修復之後,執行以下的命令:
1 |
$ git flow release finish <release number> |
這個命令會把你的release/<release number> 分支合併到master和develop分支,這意味著你永遠不需要擔心這幾個分支會缺少一些必要的產品變更(可能是因為一個快速的bug修復導致的)。
Hotfix
然而hotfix分支可能會很有用,在現實世界中很少使用,至少我是這樣認為的。hotfix就像master分支下建立的feature分支: 如果你已經關閉了release分支,但是之後又認識到還有一些很重要的東西需要一起釋出,那麼就在master分支(由$git flow release finish <release number>建立的標籤)下建立一個hotfix分支,就像這樣:
1 |
$ git flow hotfix start <release number> |
當你完成改變和增加你的版本號使之獨一無二(bump your version number),然後完成hotfix分支:
1 |
$ git flow hotfix finish <release number> |
這好像一個release分支(因為它本質上就是一種release分支),會在master和develop分支上提交修改。
我猜想它們很少使用的原因是因為已經存在一種可以給已釋出的程式碼做出修改的機制:提交到一個未完成的release分支。當然,可能一開始,團隊使用git flow release finish .. 太早了,然後第二天又發現需要快速修改。隨著時間的推移,他們就會為一個release 分支多留一些時間,所以,不會再需要hotfix分支。另一種需要hotfix分支情況就是如果你立即需要在產品中加入新的特性,等不及在develop分支中加入改變。不過(期望)這些都是小概率事件。
virtualenv和virtualenvwrapper
lan Bicking的virtualenv工具事實上已經成為了隔離Python環境的標準途徑了。它的目標很簡單:如果你的一臺機子中有很多Python專案,每個都有不同的依賴(可能相同的包,但是依賴不同的版本),僅僅在一個Python安裝環境中管理這些依賴幾乎是不可能的。
virtualenv建立了一個“虛擬的”Python安裝環境,每個環境都是相互隔離的,都有自己的site-packages, distribute和 使用pip安裝包到虛擬環境而不是系統Python安裝環境。 而且在你的虛擬環境中來回切換隻是一個命令的事。
Doug Hellmann的virtualenvwrapper使建立和管理多個虛擬環境更容易的隔離工具。讓我們繼續前進,馬上安裝這兩個工具:
1 2 3 4 |
$ pip install `virtualenvwrapper` ... Successfully installed `virtualenvwrapper` `virtualenv` `virtualenv`-clone stevedore Cleaning up... |
如你所見,後者依賴於前者,所以簡單的安裝virtualenvwrapper就足夠了。注意,如果你使用的是Python3,PEP-405通過venv包和pyvenv命令提供了Python原生虛擬環境的支援,在python3.3中已實現。你應該使用這個而不是前面提到的工具。
一旦你安裝了virtualenvwrapper,你需要新增一行內容到你的.zhsrc檔案(對bash使用者來說是.bashrc檔案):
1 |
$ echo "source /usr/local/bin/virtualenvwrapper.sh" >> ~/.zshrc |
這樣在你的shell中增加了一些有用的命令(記得第一次使用時source一下你的.zshrc檔案以使它生效)。雖然你可以使用mkvirtualenv命令直接建立一個virtualenv,但使用mkproject [OPTIONS] DEST_DIR建立一個“專案”將更有用。因為我們已經有一個現有的專案了,所有我們只需為我們的專案建立一個新的virtualenv,下附命令可以達到這效果:
1 2 3 4 5 6 |
$ mkvirtualenv ossproject New python executable in ossproject/bin/python Installing setuptools............done. Installing pip...............done. (ossproject)$ |
你會注意到你的shell提示符在你的virtualenv之後(我的是“ossproject”,你可以使用任何你喜歡的名字)。現在任何通過pip安裝的模組將安裝到你的virtualenv下的site-packages。
要停止在你的專案上工作並切換回系統使用deactivate命令。你會看到命令提示符前你的virtualenv名字消失了。要重新回到你的專案上工作的話執行workon <project name>,你會回到你的virtualenv。
除了簡單地為你的專案建立virtualenv,你還會用它做其他事:生成你的requirements.txt檔案,使用requirements.txt檔案和-r標識可安裝所有專案的依賴項。要建立該檔案,在你的virtualenv執行以下命令(一旦你程式碼和virtualenv一起工作,就是那裡):
1 |
(ossproject)$ pip freeze > requirements.txt |
你會得到一個所有你專案需要模組的列表,它以後可以被setup.py檔案使用列出你的依賴關係。這裡有一點需要注意:我經常在requirements.txt中將“==”改為“>=“,這樣代表“我正使用包的任何的後來版本”。你是否應該或需要在專案這樣做取決於實際情況,但我應該指出來。
將requirements.txt提交到你的git程式碼庫中。此外,你現在可以新增這裡的列出的包列表作為install_requirement引數的值到setup.py檔案中的distutils.setup。這樣做我們可以確保當上傳包到PyPI後,它可以被pip安裝並自動解決依賴關係。
使用py.test測試
在Python的自動測試系統裡有兩個主要的Python標準單元測試包(很有用)的替代品:nose和py.test。兩個方案都將單元測試擴充的易於使用且增加額外的功能。說真的,哪個都是很好的選擇。我更喜歡py.test因為下述幾個原因:
- 支援setuptools/distutils專案
- Python的setup.py測試技能始終其作用
- 支援常見的斷言(assert)語法 (而不是需要記住所有jUnit風格的斷言函式)
- 更少的樣板
- 支援多種測試風格
- 單元測試
- 文件測試
- nose測試
注意
如果你已經有了一個自動測試的解決方案那繼續使用它吧,跳過這一節。但請記住以後的章節你將被認為在使用py.test測試,這可能會影響到配置值。
測試安裝
在測試目錄裡,無論你如何決定都要有這個目錄,建立一個名為test_<project_name>.py的檔案。py.test的測試發現機制將把所有test_字首的檔案當做測試檔案處理(除非明確告知)。
在這個檔案裡放什麼很大程度上取決於你。寫測試是一個很大的話題,超出這篇文章的範圍。最重要的,測試對你的和潛在的捐助者都是有用的。應該標識清楚每個用例是測試的什麼函式。用例應該以相同的“風格”書寫,這樣潛在的貢獻者不必猜測在你的專案中他/她應該使用三種測試風格中的哪種。
覆蓋測試
自動化測試的覆蓋率是一個有爭議的話題。一些人認為它給出了錯誤的保證是一個毫無意義的度量,其他人認為它很有用。在我看在,我建議如果你已經使用自動化測試但從來沒有檢查過你的測試覆蓋率,現在做這樣一個練習。
使用py.test,我們可以使用Ned Batchelder的覆蓋測試工具。使用pip安裝pytest-cov。如果你之前這樣執行你的測試:
1 |
$ py.test |
你可以通過傳遞一些新的標識生成覆蓋率報告,下面是執行sandman的一個例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
$ py.test --cov=path/to/package $ py.test --cov=path/to/package --cov-report=term --cov-report=html ====================================================== test session starts ======================================================= platform darwin -- Python 2.7.5 -- pytest-2.3.5 plugins: cov collected 23 items sandman/test/test_sandman.py ....................... ---------------------------------------- coverage: platform darwin, python 2.7.5-final-0 ----------------------------------------- Name Stmts Miss Cover -------------------------------------------------- sandman/__init__ 5 0 100% sandman/exception 10 0 100% sandman/model 48 0 100% sandman/sandman 142 0 100% sandman/test/__init__ 0 0 100% sandman/test/models 29 0 100% sandman/test/test_sandman 114 0 100% -------------------------------------------------- TOTAL 348 0 100% Coverage HTML written to dir htmlcov =================================================== 23 passed in 1.14 seconds =========================================================== |
當然不是所有專案都有100%的測試覆蓋率(事實上,正如你讀到的,sandman沒有100%覆蓋),但獲得100%的覆蓋率是一個有用的練習。它能夠揭示我之前沒有留意的缺陷與重構機會。
因為,作為測試本身,自動生成的測試覆蓋報可以作為你持續整合的一部分。如果你選擇這樣做,部署一個標記來顯示當前的測試覆蓋率會為你的專案增加透明度(大多數時候會極大的鼓勵他人貢獻)。
使用Tox進行標準化測試
一個所有Python專案維護者都需要面對的問題是相容性。如果你的目標是同時支援Python 2.x和Python 3.x(如果你目前只支援Python 2.x,應該這樣做),實際中你如何確保你的專案支援你所說的所有版本呢?畢竟,當你執行測試時,你只使用特定的版本環境來執行測試,它很可能在Python2.7.5中執行良好但在Python 2.6和3.3出現問題。
幸運的是有一個工具致力於解決這個問題。tox提供了“Python的標準化測試”,它不僅僅是在多個版本環境中執行你的測試。它創造了一個完整的沙箱環境,在這個環境中你的包和需求被安裝和測試。如果你做了更改在測試時沒有異常,但意外地影響了安裝,使用Tox你會發現這類問題。
通過一個.ini檔案配置tox:tox.ini。它是一個很容易配置的檔案,下面是從tox文件中摘出來的一個最小化配置的tox.ini:
1 2 3 4 5 6 |
# content of: tox.ini , put in same dir as setup.py [tox] envlist = py26,py27 [testenv] deps=pytest # install pytest in the venvs commands=py.test # or 'nosetests' or ... |
通過設定envlist為py26和py27,tox知道需要在這兩種版本環境下執行測試。tox大約支援十幾個“預設”的環境沙箱,包括jython和pypy。tox這個強大的工具使用不同的版本進行測試,在不支援多版本時可配置警示。
deps是你的包依賴列表。你甚至可以讓tox從PyPI地址安裝所有或一些你依賴包。顯然,相當多的想法和工作已融入了專案。
實際在你的所有環境下執行測試現在只需要四個按鍵:
1 |
$ tox |
一個更復雜的設定
我的書——“寫地道的Python”,實際上寫的是一系列的Python模組和程式碼。這樣做是為了確保所有的示例程式碼按預期工作。作為我的構建過程的一部分,我執行tox來確保任何新的語法程式碼能正常執行。我偶爾也看看我的測試覆蓋率,以確保沒有語法在測試中被無意跳過。因此,我的tox.ini比上面的複雜一些,一起來看一看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
[tox] envlist=py27, py34 [testenv] deps= pytest coverage pytest-cov setenv= PYTHONWARNINGS=all [pytest] adopts=--doctest-modules python_files=*.py python_functions=test_ norecursedirs=.tox .git [testenv:py27] commands= py.test --doctest-module [testenv:py34] commands= py.test --doctest-module [testenv:py27verbose] basepython=python commands= py.test --doctest-module --cov=. --cov-report term [testenv:py34verbose] basepython=python3.4 commands= py.test --doctest-module --cov=. --cov-report term |
這個配置檔案依舊比較簡單。而結果呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
(idiom)~/c/g/idiom git:master >>> tox GLOB sdist-make: /home/jeff/code/github_code/idiom/setup.py py27 inst-nodeps: /home/jeff/code/github_code/idiom/.tox/dist/Writing Idiomatic Python-1.0.zip py27 runtests: commands[0] | py.test --doctest-module /home/jeff/code/github_code/idiom/.tox/py27/lib/python2.7/site-packages/_pytest/assertion/oldinterpret.py:3: DeprecationWarning: The compiler package is deprecated and removed in Python 3.x. from compiler import parse, ast, pycodegen =============================================================== test session starts ================================================================ platform linux2 -- Python 2.7.5 -- pytest-2.3.5 plugins: cov collected 150 items ... ============================================================ 150 passed in 0.44 seconds ============================================================ py33 inst-nodeps: /home/jeff/code/github_code/idiom/.tox/dist/Writing Idiomatic Python-1.0.zip py33 runtests: commands[0] | py.test --doctest-module =============================================================== test session starts ================================================================ platform linux -- Python 3.3.2 -- pytest-2.3.5 plugins: cov collected 150 items ... ============================================================ 150 passed in 0.62 seconds ============================================================ _____________________________________________________________________ summary ______________________________________________________________________ py27: commands succeeded py33: commands succeeded congratulations :) |
(我從輸出列表裡擷取了一部分)。如果想看我的測試對一個環境的覆蓋率,只需執行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
$ tox -e py33verbose -------------------------------------------------- coverage: platform linux, python 3.3.2-final-0 -------------------------------------------------- Name Stmts Miss Cover ------------------------------------------------------------------------------------------------------------------ control_structures_and_functions/a_if_statement/if_statement_multiple_lines 11 0 100% control_structures_and_functions/a_if_statement/if_statement_repeating_variable_name 10 0 100% control_structures_and_functions/a_if_statement/make_use_of_pythons_truthiness 20 3 85% control_structures_and_functions/b_for_loop/enumerate 10 0 100% control_structures_and_functions/b_for_loop/in_statement 10 0 100% control_structures_and_functions/b_for_loop/use_else_to_determine_when_break_not_hit 31 0 100% control_structures_and_functions/functions/2only/2only_use_print_as_function 4 0 100% control_structures_and_functions/functions/avoid_list_dict_as_default_value 22 0 100% control_structures_and_functions/functions/use_args_and_kwargs_to_accept_arbitrary_arguments 39 31 21% control_structures_and_functions/zexceptions/aaa_dont_fear_exceptions 0 0 100% control_structures_and_functions/zexceptions/aab_eafp 22 2 91% control_structures_and_functions/zexceptions/avoid_swallowing_exceptions 17 12 29% general_advice/dont_reinvent_the_wheel/pypi 0 0 100% general_advice/dont_reinvent_the_wheel/standard_library 0 0 100% general_advice/modules_of_note/itertools 0 0 100% general_advice/modules_of_note/working_with_file_paths 39 1 97% general_advice/testing/choose_a_testing_tool 0 0 100% general_advice/testing/separate_tests_from_code 0 0 100% general_advice/testing/unit_test_your_code 1 0 100% organizing_your_code/aa_formatting/constants 16 0 100% organizing_your_code/aa_formatting/formatting 0 0 100% organizing_your_code/aa_formatting/multiple_statements_single_line 17 0 100% organizing_your_code/documentation/follow_pep257 6 2 67% organizing_your_code/documentation/use_inline_documentation_sparingly 13 1 92% organizing_your_code/documentation/what_not_how 24 0 100% organizing_your_code/imports/arrange_imports_in_a_standard_order 4 0 100% organizing_your_code/imports/avoid_relative_imports 4 0 100% organizing_your_code/imports/do_not_import_from_asterisk 4 0 100% organizing_your_code/modules_and_packages/use_modules_where_other_languages_use_object 0 0 100% organizing_your_code/scripts/if_name 22 0 100% organizing_your_code/scripts/return_with_sys_exit 32 2 94% working_with_data/aa_variables/temporary_variables 12 0 100% working_with_data/ab_strings/chain_string_functions 10 0 100% working_with_data/ab_strings/string_join 10 0 100% working_with_data/ab_strings/use_format_function 18 0 100% working_with_data/b_lists/2only/2only_prefer_xrange_to_range 14 14 0% working_with_data/b_lists/3only/3only_unpacking_rest 16 0 100% working_with_data/b_lists/list_comprehensions 13 0 100% working_with_data/ca_dictionaries/dict_dispatch 23 0 100% working_with_data/ca_dictionaries/dict_get_default 10 1 90% working_with_data/ca_dictionaries/dictionary_comprehensions 21 0 100% working_with_data/cb_sets/make_use_of_mathematical_set_operations 25 0 100% working_with_data/cb_sets/set_comprehensions 12 0 100% working_with_data/cb_sets/use_sets_to_remove_duplicates 34 6 82% working_with_data/cc_tuples/named_tuples 26 0 100% working_with_data/cc_tuples/tuple_underscore 15 0 100% working_with_data/cc_tuples/tuples 12 0 100% working_with_data/classes/2only/2only_prepend_private_data_with_underscore 43 43 0% working_with_data/classes/2only/2only_use_str_for_human_readable_class_representation 18 18 0% working_with_data/classes/3only/3only_prepend_private_data_with_underscore 45 2 96% working_with_data/classes/3only/3only_use_str_for_human_readable_class_representation 18 0 100% working_with_data/context_managers/context_managers 16 7 56% working_with_data/generators/use_generator_expression_for_iteration 16 0 100% working_with_data/generators/use_generators_to_lazily_load_sequences 44 1 98% ------------------------------------------------------------------------------------------------------------------ TOTAL 849 146 83% ============================================================ 150 passed in 1.73 seconds ============================================================ _____________________________________________________________________ summary ______________________________________________________________________ py33verbose: commands succeeded congratulations :) |
結果很可怕啊。
setuptools整合
tox可以和setuptools整合,這樣python的setup.py測試可以執行你的tox測試。將下面的程式碼段放到你的setup.py檔案裡,這段程式碼是直接從tox的文件裡拿來的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
from setuptools.command.test import test as TestCommand import sys class Tox(TestCommand): def finalize_options(self): TestCommand.finalize_options(self) self.test_args = [] self.test_suite = True def run_tests(self): #import here, cause outside the eggs aren't loaded import tox errcode = tox.cmdline(self.test_args) sys.exit(errcode) setup( #..., tests_require=['tox'], cmdclass = {'test': Tox}, ) |
現在Python的setup.py測試將下載tox並執行它。真的很酷並且很節省時間。
Sphinx文件生成器
Sphinx是由pocoo團隊開發的工具[@Lesus 注:pocoo團隊開發了很多優秀的產品:如Flask, Jinja2等等]。它已經用來生成Python官方文件和大多數流行的Python包的文件。它以更容易的方式從Python程式碼中自動產生Python文件。
使用它完成工作
Sphinx不用瞭解Python程式以及怎樣從它們中提取出來。它只能翻譯reStructuredText檔案,也就意味著你的程式碼文件的reStructuredText譯文需要讓Sphinx知道才能工作,但是管理維護所有的.py檔案[至少是函式和類的部分]的reStructuredText譯文顯然是不可行的。
幸運的是,Sphinx有一個類似javadoc的擴充套件,叫做autodoc,可以用來從你的程式碼文件中萃取出reStructuredText。為了能夠充分利用Sphinx和autodoc的能力,你需要已一種特別的方式格式化你的文件。特別是,你需要使用Sphinx的Python指令時。這裡就是使用reStructuredText指令來為一個函式生成文件,使輸出結果的HTML文件更漂亮:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
def _validate(cls, method, resource=None): """Return ``True`` if the the given *cls* supports the HTTP *method* found on the incoming HTTP request. :param cls: class associated with the request's endpoint :type cls: :class:`sandman.model.Model` instance :param string method: HTTP method of incoming request :param resource: *cls* instance associated with the request :type resource: :class:`sandman.model.Model` or None :rtype: bool """ if not method in cls.__methods__: return False class_validator_name = 'validate_' + method if hasattr(cls, class_validator_name): class_validator = getattr(cls, class_validator_name) return class_validator(resource) return True |
文件需要花費一點功夫,但是為了你的使用者,這個付出是值得的。好吧,好的文件使一個可用的專案去其糟粕。
Sphinx的autodoc擴充套件讓我們可以使用很多指令,而這些指令可以自動的從你文件中生成文件。
安裝
確認將Sphinx安裝在你的virtualenv內,因為文件在專案裡也是按版本來的。Sphinx不同的版本可能會產生不同的HTML輸出。通過將其安裝在你的virtualenv內,你可以以受控的方式升級你的文件。
我們要保持我們的文件在docs資料夾,將文件生成到docs/generated資料夾。在專案的根目錄執行以下命令將根據你的文件字元自動重構文字文件:
1 |
$ sphinx-apidoc -F -o docs <package name> |
這將產生一個包含多個文件檔案的docs資料夾。此外,它建立了一個叫conf.py的檔案,它將負責你的文件配置。你還會發現一個Makefile,方便使用一個命令(生成html)構建HTML文件。
在你最終生成文件之前,確保你已經在本地安裝了相應的包(儘管可以使用pip,但python setup.py develop是最簡單的保持更新的方法),否則sphinx-apidoc無法找到你的包。
配置:conf.py
conf.py檔案建立用來控制產生的文件的各個方面。它自己會很好生成文件,所以我只簡單地觸及兩點。
版本和釋出
首先,確保你的版本和釋出版本號保持最新。這些數字會作為生成的文件的一部分顯示,所以你不希望它們遠離了實際值。
保持你的版本最新的最簡單方式就是在你的文件和setup.py檔案中都從你的包的__version__屬性讀取。我從Flask的conf.py借用過來配置sandman的conf.py:
1 2 3 4 5 6 7 8 9 10 11 12 |
import pkg_resources try: release = pkg_resources.get_distribution('sandman').version except pkg_resources.DistributionNotFound: print 'To build the documentation, The distribution information of sandman' print 'Has to be available. Either install the package into your' print 'development environment or run "setup.py develop" to setup the' print 'metadata. A virtualenv is recommended!' sys.exit(1) del pkg_resources version = '.'.join(release.split('.')[:2]) |
這就是說,為了讓文件產生正確的版本號,你只需在你的專案的虛擬環境中簡單的需要執行$python setup.py develop即可。現在你只需擔心保持__version__為最新,因為setup.py會使用它。
html_theme
考慮到更改default到html_theme,我更喜歡原生態的東西,顯然這是一個個人喜好的問題。我之所以提出這個問題是因為Python官方文件在Python 2和Python 3將預設主題更改為Pydoc主題(後者的主題是一個自定義主題僅在CPython原始碼中可用)。對一些人來說,預設的主題使一個專案看起來“老”一些。
PyPI
PyPI,Python包索引(以前被稱為“Cheeseshop”)是一個公開可用的Python包中央資料庫。PyPI是你的專案釋出的地方。一旦你的包(及其相關的後設資料)上傳到PyPI,別人通過pip或easy_instal可以下載並安裝它。這一點得強調一下:即使你的專案託管在GitHub,直到被上傳到PyPI後你的專案才是有用的。當然,有些人可以複製你的git庫任何直接手工安裝它,但更多的人想使用pip來安裝它。
最後的一步
如果你已經完成了所有的前面部分中的步驟,你可能急著想把你的包上傳到PyPI,供其他人使用!
先別急著做上述事情,在分發你的包之前,有一個叫做cheesecake的有用的工具有助於執行最後一步。它分析你的包並指定一個分類的數字分數。它衡量你的包在打包、安裝、程式碼質量以及文件的數量和質量方面是否容易/正確。
除了作粗略衡量的“準備”,cheesecake在完整性檢查方面很優秀。你會很快看到你的setup.py檔案是否有錯或者有沒有忘記為一個檔案製作文件。我建議在上傳每個專案到PyPI之前執行一下它,而不僅只是第一個。
初始化上傳
現在,你已經確定了你的程式碼不是垃圾和當人們安裝它時不會崩潰,讓我們把你的包放到PyPI上吧!你將會通過setuptools和setup.py指令碼互動。如果這是第一次上傳到PyPI,你將首先註冊它:
1 |
$ python setup.py register |
注意:如果你還沒有一個免費的PyPI賬戶,你將需要現在去註冊一個,才能註冊這個包[@Lesus 注:註冊之後還需要到郵箱去驗證才行]。在你已使用了上面註冊之後,你就可以建立釋出包和上傳到PyPI了:
1 |
$ python setup.py sdist upload |
上面這個命令建立一個原始碼釋出版(sdist),然後上傳到PyPI.如果你的包不是純粹的Python(也就是說,你有二進位制需要編譯進去),你就需要釋出一個二進位制版,請看setuptools文件,瞭解更多。
釋出及版本號
PyPI使用發行版本模型來確定你軟體包的哪個版本是預設可用的。初次上傳後,為使你軟體包的每次更新後在PyPI可用,你需要指定一個新版本號建立一個釋出。版本號管理是一個相當複雜的課題,PEP有專門的內容:PEP 440——版本識別和依賴指定。我建議參照PEP 400指南(明顯地),但如果你選擇使用不同版本的方案,在setup.py中使用的版本比目前PyPI中的版本“高”,這樣PyPI才會認為這是一個新版本。
工作流
將你的第一個釋出版本上傳到PyPI後,基本的工作流程如下:
- 繼續在你的專案上工作 (比如修復bug,新增新特性等等)
- 確保測試通過
- 在git-flow中建立一個釋出分支“凍結”你的程式碼
- 在你專案的__init__.py檔案裡更新__version__number版本變數
- 多次測試執行setup.py,將新版本上傳到PyPI
使用者希望你保持足夠的更新頻率以修復bug。你要管理好你的版本號,不要“過於頻繁”的釋出。記住:你的使用者不會手工維護他們每個安裝模組的不同的版本。
使用TravisCI持續整合
持續整合是指一個專案中所有變化不斷整合的過程(不是週期性的批量更新)。就我們而言,這意味每次我們GitHub提交時,我們通過測試執行來發現是否有什麼異常,正如你想象的,這是一個非常有價值的實踐。不要有“忘記執行測試”的提交。如果你的提交通不過測試,你將收到一封電子郵件被告知。
TravisCI是一種使GitHub專案持續整合更容易的服務。如果你還沒有賬號到這看一下注冊一個,完成這些之後,在我們進入CI之前我們先需要建立一個簡單的檔案。
通過.travis.yml配置
在TravisCI上的不同專案通過一個.travis.yml檔案來配置,這個檔案在專案的根目錄。簡要地說,我們需要告訴Travis:
- 我們專案使用的語言是什麼
- 它使用的是語言的哪個版本
- 使用什麼命令安裝它
- 使用什麼命令執行專案的測試
這些都是很直接的東西。下面是sandman.travis.yml的內容:
1 2 3 4 5 6 7 8 9 10 11 |
language: python python: - "2.7" install: - "pip install -r requirements.txt --use-mirrors" - "pip install coverage" - "pip install coveralls" script: - "coverage run --source=sandman setup.py test" after_success: coveralls |
在列出語言和版本後,我們告訴Travis如何安裝我們的包。在install這行,確認包含下面這行:
1 |
- "pip install -r requirements.txt --use-mirrors" |
這是pip安裝我們專案的要求(如果有必要的話使用PyPI映象站點)。另外的兩行內容是sandman特有的。它使用一個額外的服務(coveralls.io)來連續監測測試用例的覆蓋率,這不是所有專案都需要的。
script:列出能執行該專案測試的命令。與上面一樣,sandman還需要做一些額外的工作。你的專案需要的只有Python的setup.py測試,after_success部分也可以一塊刪掉。
一旦你提交了這個檔案並在TravisCI中啟用了你的專案的,push到GitHub。一會兒後,你會看到一個基於你最近提交的編譯結束結果。如果成功了,你的編譯呈現“綠色”和並且狀態頁會顯示編譯通過。你可以看到你專案在任何時間的編譯歷史。這對對人開發特別有用,在歷史頁可以看到特定開發者出錯和編譯的頻率…
你還會收到一封通知你編譯成功的電子郵件。當然你也可以設定只有在出錯或錯誤被修復時才有郵件通知,但編譯輸出結果相同時也不會傳送。這是非常有用的,你在不必被無用的“編譯通過!”郵件淹沒的同時在發生改變仍會收到警示。
用ReadTheDocs做持續文件整合
儘管PyPI有一個官方文件站點(pythonhosted.org),但是ReadTheDocs提供了一個更好的體驗。為什麼?ReadTheDocs有針對GitHub非常棒的整合。當你註冊ReadTheDocs的時候,你就會看到你的所有GitHub 程式碼庫。選擇合適的程式碼庫,做一些小幅的配置,那麼你的文件就會在你每次提交到GitHub之後自動重新生成。
配置你的專案應該是一個很直觀的事情。只有一些事需要記住,儘管,這裡有一個配置欄位的列表,對應的值可能不一定是你直接用得上的:
- Repo: https://github.com/github_username/project_name.git
- Default Branch:develop
- Default Version:latest
- Python configuration file: (leave blank)
- Usevirtualenv: (checked)
- Requirements file:requirements.txt
- Documentation Type: Sphinx HTML
DRY 不要重複你自己
現在你已經完成了對於一個現存程式碼基礎的所有艱難的開源工作,你可能不會想在開始一個新專案的時候把這些事重來一遍。幸運的是,你並不需要這麼做。有Andrey Roy的Cookiecutter工具(我連結到了Python版本,儘管還有一些不同語言的版本在the main repo))
Cookiecutter是一個命令列工具能夠自動執行新建專案的一些步驟來做這篇文章裡提到的一些事情。 Daniel Greenfeld (
@pydanny )寫了一篇很好的關於它的部落格並且提到了如何與這篇文章裡提到的實踐聯絡上。你可以從這裡看看這篇文章:
Cookiecutter: Project Templates Made Easy .
結論
我們已經介紹了所有用來開源一個Python包的命令,工具和服務。當然,你可以直接把它扔到GitHub上並且說“自己安裝它”,但是沒人會這麼做。並且你僅僅是開發原始碼並不算是真正的開源軟體。
另外,你可能不會為你的專案吸引外部貢獻者。通過這裡列出的方法來設立你的專案,你就已經建立了一個容易維護的Python包並且會鼓勵大家來使用和貢獻程式碼。而這,就是開源軟體的真正精神,不是嗎?