以正確的方式開源 Python 項目

以正確的方式開源 Python 項目

大多數Python開發者至少都寫過一個像工具、腳本、庫或框架等對其餘人也有用的工具。我寫這篇文章的目的是讓現有Python代碼的開源過程儘量清 晰和無痛。我不是簡單的指——「建立一個GitHub庫,提交,在Reddit上發佈,天天調用它」。在本文的結尾,你能夠把現有的代碼轉換成一個可以鼓 勵他人使用和貢獻的開源項目。html

然而每個項目都是不一樣的,但其中將現有代碼開源的流程對全部的Python項目都是相似的。在另外一個受歡迎的文章系列裏我寫了「以正確方式開始一個Django項目」,我將概述在開源Python項目我發現的有必要的步驟。java

更新 (8月17號): 感謝@pydann提醒我Cookiecutter的存在,@audreyr的一個不起的項目。我在文章結尾添加了其中的一段。看一下Audrey的項目吧!node

更新 2 (8月18號):感謝@ChristianHeimes(和其餘人)關於ontox這一段。Christian也讓我想起了PEP 440和其餘一些都已實現很棒的改進建議。python

工具和概念

特別是,我發現一些工具和概念十分有用或者說是必要的。下面我就會談及這方面主題,包括須要運行的精確的命令和須要設置的配置值。其終極目標就是讓整個流程簡單明瞭。 linux

  1. 項目佈局(目錄結構)
  2. setuptools 和 setup.py文件
  3. git版本控制
  4. GitHub 項目管理
    1. GitHub的"Issues" 以下做用:
      1. bug跟蹤
      2. 請求新特性
      3. 計劃好的新特性
      4. 發佈或者版本管理
  5. git-flow git工做流
  6. py.test 單元測試
  7. tox 標準化測試
  8. Sphinx 自動生成HTML文檔
  9. TravisCI 持續測試集成
  10. ReadTheDocs 持續文檔集成
  11. Cookiecutter  爲開始下一個項目自動生成這些步驟

項目佈局

當準備一個項目時,正確合理的佈局(目錄結構)是十分重要的。一個合理的佈局意味着想參與開發者沒必要花時間來尋找某些代碼的位置; 憑直覺就能夠找到文件的位置。由於咱們在處理一個項目,就意味着可能須要處處移動一些東西。 git

讓咱們從頂層開始。大多數項目都有不少頂層文件(如setup.py, README.md, requirements等等)。每一個項目至少應該有下面三個目錄: github

  1. doc目錄,包括項目文檔
  2. 項目目錄,以項目命名,存儲實際的Python包
  3. test目錄,包含下面兩部分
    1. 在這個目錄下包括了測試代碼和資源
    2. 做爲一個獨立頂級包

爲了更好理解文件該如何組織,這裏是一個個人簡單項目:sandman 佈局快照。 shell

01 $ pwd
02 ~/code/sandman
03 $ tree
04 .
05 |- LICENSE
06 |- README.md
07 |- TODO.md
08 |- docs
09 |   |-- conf.py
10 |   |-- generated
11 |   |-- index.rst
12 |   |-- installation.rst
13 |   |-- modules.rst
14 |   |-- quickstart.rst
15 |   |-- sandman.rst
16 |- requirements.txt
17 |- sandman
18 |   |-- __init__.py
19 |   |-- exception.py
20 |   |-- model.py
21 |   |-- sandman.py
22 |   |-- test
23 |       |-- models.py
24 |       |-- test_sandman.py
25 |- setup.py

如你所看到那樣,這裏有一些頂層文件,一個docs目錄(創建一個空目錄,由於sphinx會將生成的文檔放到這裏),一個sandman目錄,以及一個在sandman目錄下的test目錄。數據庫

setuptools 和 setup.py文件

setup.py文件,你可能已經在其它包中看 到過,被distuils包用來安裝Python包的。對於任何一個項目,它都是一個很重要的文件,由於它包含了版本,包依賴信息,PyPi須要的項目描 述,你的名字和聯繫信息,以及其它一些信息。它容許以編程的方式搜索安裝包,提供元數據和指令說明讓工具如何作。 express

setuptools包(實際上就是對distutils的加強)簡單化了創建發佈python包。使用setuptools給python包打包,和distutils打包沒什麼區別。這實在是沒有任何理由不使用它。

setup.py應該放在你的項目的根目錄。setup.py中最重要的一部分就是調用setuptools.setup,這裏麪包含了此包所需的全部元信息。這裏就是sandman的setup.py的全部內容

01 from __future__ import print_function
02 from setuptools import setup, find_packages
03 from setuptools.command.test import test as TestCommand
04 import io
05 import codecs
06 import os
07 import sys
08  
09 import sandman
10  
11 here = os.path.abspath(os.path.dirname(__file__))
12  
13 def read(*filenames, **kwargs):
14     encoding = kwargs.get('encoding', 'utf-8')
15     sep = kwargs.get('sep', '\n')
16     buf = []
17     for filename in filenames:
18         with io.open(filename, encoding=encoding) as f:
19             buf.append(f.read())
20     return sep.join(buf)
21  
22 long_description = read('README.txt', 'CHANGES.txt')
23  
24 class PyTest(TestCommand):
25     def finalize_options(self):
26         TestCommand.finalize_options(self)
27         self.test_args = []
28         self.test_suite = True
29  
30     def run_tests(self):
31         import pytest
32         errcode = pytest.main(self.test_args)
33         sys.exit(errcode)
34  
35 setup(
36     name='sandman',
37     version=sandman.__version__,
38     url='http://github.com/jeffknupp/sandman/',
39     license='Apache Software License',
40     author='Jeff Knupp',
41     tests_require=['pytest'],
42     install_requires=['Flask>=0.10.1',
43                     'Flask-SQLAlchemy>=1.0',
44                     'SQLAlchemy==0.8.2',
45                     ],
46     cmdclass={'test': PyTest},
47     author_email='jeff@jeffknupp.com',
48     description='Automated REST APIs for existing database-driven systems',
49     long_description=long_description,
50     packages=['sandman'],
51     include_package_data=True,
52     platforms='any',
53     test_suite='sandman.test.test_sandman',
54     classifiers = [
55         'Programming Language :: Python',
56         'Development Status :: 4 - Beta',
57         'Natural Language :: English',
58         'Environment :: Web Environment',
59         'Intended Audience :: Developers',
60         'License :: OSI Approved :: Apache Software License',
61         'Operating System :: OS Independent',
62         'Topic :: Software Development :: Libraries :: Python Modules',
63         'Topic :: Software Development :: Libraries :: Application Frameworks',
64         'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
65         ],
66     extras_require={
67         'testing': ['pytest'],
68     }
69 )

(感謝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 $ pip install `virtualenvwrapper`
2 ...
3 Successfully installed `virtualenvwrapper` `virtualenv` `virtualenv`-clone stevedore
4 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 $ mkvirtualenv ossproject
2  
3 New python executable in ossproject/bin/python
4 Installing setuptools............done.
5 Installing pip...............done.
6 (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標準單元測試包(頗有用)的替代品:nosepy.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的一個例子:

01 $ py.test --cov=path/to/package
02 $ py.test --cov=path/to/package --cov-report=term --cov-report=html
03 ====================================================== test session starts =======================================================
04 platform darwin -- Python 2.7.5 -- pytest-2.3.5
05 plugins: cov
06 collected 23 items
07  
08 sandman/test/test_sandman.py .......................
09 ---------------------------------------- coverage: platform darwin, python 2.7.5-final-0 -----------------------------------------
10 Name                           Stmts   Miss  Cover
11 --------------------------------------------------
12 sandman/__init__                   5      0   100%
13 sandman/exception                 10      0   100%
14 sandman/model                     48      0   100%
15 sandman/sandman                  142      0   100%
16 sandman/test/__init__              0      0   100%
17 sandman/test/models               29      0   100%
18 sandman/test/test_sandman        114      0   100%
19 --------------------------------------------------
20 TOTAL                            348      0   100%
21 Coverage HTML written to dir htmlcov
22  
23 =================================================== 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 # content of: tox.ini , put in same dir as setup.py
2 [tox]
3 envlist = py26,py27
4 [testenv]
5 deps=pytest       # install pytest in the venvs
6 commands=py.test  # or 'nosetests' or ...

經過設置envlist爲py26和py27,tox知道須要在這兩種版本環境下運行測試。tox大約支持十幾個「默認」的環境沙箱,包括jython和pypy。tox這個強大的工具使用不一樣的版本進行測試,在不支持多版本時可配置警示。

deps是你的包依賴列表。你甚至可讓tox從PyPI地址安裝全部或一些你依賴包。顯然,至關多的想法和工做已融入了項目。

實際在你的全部環境下運行測試如今只須要四個按鍵:

1 $ tox

 

一個更復雜的設置

 


個人書——「寫地道的Python」, 實際上寫的是一系列的Python模塊和代碼。這樣作是爲了確保全部的示例代碼按預期工做。做爲個人構建過程的一部分,我運行tox來確保任何新的語法代 碼能正常運行。我偶爾也看看個人測試覆蓋率,以確保沒有語法在測試中被無心跳過。所以,個人tox.ini比上面的複雜一些,一塊兒來看一看:

01 [tox]
02 envlist=py27, py34
03  
04 [testenv]
05 deps=
06     pytest
07     coverage
08     pytest-cov
09 setenv=
10     PYTHONWARNINGS=all
11  
12 [pytest]
13 adopts=--doctest-modules
14 python_files=*.py
15 python_functions=test_
16 norecursedirs=.tox .git
17  
18 [testenv:py27]
19 commands=
20     py.test --doctest-module
21  
22 [testenv:py34]
23 commands=
24     py.test --doctest-module
25  
26 [testenv:py27verbose]
27 basepython=python
28 commands=
29     py.test --doctest-module --cov=. --cov-report term
30  
31 [testenv:py34verbose]
32 basepython=python3.4
33 commands=
34     py.test --doctest-module --cov=. --cov-report term

這個配置文件依舊比較簡單。而結果呢?

01 (idiom)~/c/g/idiom git:master >>> tox
02 GLOB sdist-make: /home/jeff/code/github_code/idiom/setup.py
03 py27 inst-nodeps: /home/jeff/code/github_code/idiom/.tox/dist/Writing Idiomatic Python-1.0.zip
04 py27 runtests: commands[0] | py.test --doctest-module
05 /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.
06 from compiler import parse, ast, pycodegen
07 =============================================================== test session starts ================================================================
08 platform linux2 -- Python 2.7.5 -- pytest-2.3.5
09 plugins: cov
10 collected 150 items
11 ...
12 ============================================================ 150 passed in 0.44 seconds ============================================================
13 py33 inst-nodeps: /home/jeff/code/github_code/idiom/.tox/dist/Writing Idiomatic Python-1.0.zip
14 py33 runtests: commands[0] | py.test --doctest-module
15 =============================================================== test session starts ================================================================
16 platform linux -- Python 3.3.2 -- pytest-2.3.5
17 plugins: cov
18 collected 150 items
19 ...
20 ============================================================ 150 passed in 0.62 seconds ============================================================
21 _____________________________________________________________________ summary ______________________________________________________________________
22 py27: commands succeeded
23 py33: commands succeeded
24 congratulations :)

(我從輸出列表裏截取了一部分)。若是想看個人測試對一個環境的覆蓋率,只需運行:

01 $ tox -e py33verbose
02 -------------------------------------------------- coverage: platform linux, python 3.3.2-final-0 --------------------------------------------------
03 Name                                                                                           Stmts   Miss  Cover
04 ------------------------------------------------------------------------------------------------------------------
05 control_structures_and_functions/a_if_statement/if_statement_multiple_lines                       11      0   100%
06 control_structures_and_functions/a_if_statement/if_statement_repeating_variable_name              10      0   100%
07 control_structures_and_functions/a_if_statement/make_use_of_pythons_truthiness                    20      3    85%
08 control_structures_and_functions/b_for_loop/enumerate                                             10      0   100%
09 control_structures_and_functions/b_for_loop/in_statement                                          10      0   100%
10 control_structures_and_functions/b_for_loop/use_else_to_determine_when_break_not_hit              31      0   100%
11 control_structures_and_functions/functions/2only/2only_use_print_as_function                       4      0   100%
12 control_structures_and_functions/functions/avoid_list_dict_as_default_value                       22      0   100%
13 control_structures_and_functions/functions/use_args_and_kwargs_to_accept_arbitrary_arguments      39     31    21%
14 control_structures_and_functions/zexceptions/aaa_dont_fear_exceptions                              0      0   100%
15 control_structures_and_functions/zexceptions/aab_eafp                                             22      2    91%
16 control_structures_and_functions/zexceptions/avoid_swallowing_exceptions                          17     12    29%
17 general_advice/dont_reinvent_the_wheel/pypi                                                        0      0   100%
18 general_advice/dont_reinvent_the_wheel/standard_library                                            0      0   100%
19 general_advice/modules_of_note/itertools                                                           0      0   100%
20 general_advice/modules_of_note/working_with_file_paths                                            39      1    97%
21 general_advice/testing/choose_a_testing_tool                                                       0      0   100%
22 general_advice/testing/separate_tests_from_code                                                    0      0   100%
23 general_advice/testing/unit_test_your_code                                                         1      0   100%
24 organizing_your_code/aa_formatting/constants                                                      16      0   100%
25 organizing_your_code/aa_formatting/formatting                                                      0      0   100%
26 organizing_your_code/aa_formatting/multiple_statements_single_line                                17      0   100%
27 organizing_your_code/documentation/follow_pep257                                                   6      2    67%
28 organizing_your_code/documentation/use_inline_documentation_sparingly                             13      1    92%
29 organizing_your_code/documentation/what_not_how                                                   24      0   100%
30 organizing_your_code/imports/arrange_imports_in_a_standard_order                                   4      0   100%
31 organizing_your_code/imports/avoid_relative_imports                                                4      0   100%
32 organizing_your_code/imports/do_not_import_from_asterisk                                           4      0   100%
33 organizing_your_code/modules_and_packages/use_modules_where_other_languages_use_object             0      0   100%
34 organizing_your_code/scripts/if_name                                                              22      0   100%
35 organizing_your_code/scripts/return_with_sys_exit                                                 32      2    94%
36 working_with_data/aa_variables/temporary_variables                                                12      0   100%
37 working_with_data/ab_strings/chain_string_functions                                               10      0   100%
38 working_with_data/ab_strings/string_join                                                          10      0   100%
39 working_with_data/ab_strings/use_format_function                                                  18      0   100%
40 working_with_data/b_lists/2only/2only_prefer_xrange_to_range                                      14     14     0%
41 working_with_data/b_lists/3only/3only_unpacking_rest                                              16      0   100%
42 working_with_data/b_lists/list_comprehensions                                                     13      0   100%
43 working_with_data/ca_dictionaries/dict_dispatch                                                   23      0   100%
44 working_with_data/ca_dictionaries/dict_get_default                                                10      1    90%
45 working_with_data/ca_dictionaries/dictionary_comprehensions                                       21      0   100%
46 working_with_data/cb_sets/make_use_of_mathematical_set_operations                                 25      0   100%
47 working_with_data/cb_sets/set_comprehensions                                                      12      0   100%
48 working_with_data/cb_sets/use_sets_to_remove_duplicates                                           34      6    82%
49 working_with_data/cc_tuples/named_tuples                                                          26      0   100%
50 working_with_data/cc_tuples/tuple_underscore                                                      15      0   100%
51 working_with_data/cc_tuples/tuples                                                                12      0   100%
52 working_with_data/classes/2only/2only_prepend_private_data_with_underscore                        43     43     0%
53 working_with_data/classes/2only/2only_use_str_for_human_readable_class_representation             18     18     0%
54 working_with_data/classes/3only/3only_prepend_private_data_with_underscore                        45      2    96%
55 working_with_data/classes/3only/3only_use_str_for_human_readable_class_representation             18      0   100%
56 working_with_data/context_managers/context_managers                                               16      7    56%
57 working_with_data/generators/use_generator_expression_for_iteration                               16      0   100%
58 working_with_data/generators/use_generators_to_lazily_load_sequences                              44      1    98%
59 ------------------------------------------------------------------------------------------------------------------
60 TOTAL                                                                                            849    146    83%
61  
62 ============================================================ 150 passed in 1.73 seconds ============================================================
63 _____________________________________________________________________ summary ______________________________________________________________________
64 py33verbose: commands succeeded
65 congratulations :)

結果很可怕啊。

setuptools整合

tox能夠和setuptools整合,這樣python的setup.py測試能夠運行你的tox測試。將下面的代碼段放到你的setup.py文件裏,這段代碼是直接從tox的文檔裏拿來的:

01 from setuptools.command.test import test as TestCommand
02 import sys
03  
04 class Tox(TestCommand):
05     def finalize_options(self):
06         TestCommand.finalize_options(self)
07         self.test_args = []
08         self.test_suite = True
09     def run_tests(self):
10         #import here, cause outside the eggs aren't loaded
11         import tox
12         errcode = tox.cmdline(self.test_args)
13         sys.exit(errcode)
14  
15 setup(
16     #...,
17     tests_require=['tox'],
18     cmdclass = {'test': Tox},
19     )

如今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文檔更漂亮:

01 def _validate(cls, method, resource=None):
02 """Return ``True`` if the the given *cls* supports the HTTP *method* found
03 on the incoming HTTP request.
04  
05 :param cls: class associated with the request's endpoint
06 :type cls: :class:`sandman.model.Model` instance
07 :param string method: HTTP method of incoming request
08 :param resource: *cls* instance associated with the request
09 :type resource: :class:`sandman.model.Model` or None
10 :rtype: bool
11  
12 """
13 if not method in cls.__methods__:
14     return False
15  
16 class_validator_name = 'validate_' + method
17  
18 if hasattr(cls, class_validator_name):
19     class_validator = getattr(cls, class_validator_name)
20     return class_validator(resource)
21  
22 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:

01 import pkg_resources
02 try:
03     release = pkg_resources.get_distribution('sandman').version
04 except pkg_resources.DistributionNotFound:
05     print 'To build the documentation, The distribution information of sandman'
06     print 'Has to be available.  Either install the package into your'
07     print 'development environment or run "setup.py develop" to setup the'
08     print 'metadata.  A virtualenv is recommended!'
09     sys.exit(1)
10 del pkg_resources
11  
12 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後,基本的工做流程以下:

  1. 繼續在你的項目上工做 (好比修復bug,添加新特性等等)
  2. 確保測試經過
  3. 在git-flow中建立一個發佈分支「凍結」你的代碼
  4. 在你項目的__init__.py文件裏更新__version__number版本變量
  5. 屢次測試運行setup.py,將新版本上傳到PyPI

用戶但願你保持足夠的更新頻率以修復bug。你要管理好你的版本號,不要「過於頻繁」的發佈。記住:你的用戶不會手工維護他們每一個安裝模塊的不一樣的版本。

使用TravisCI持續集成

持續集成是指一個項目中全部變化不斷整合的過程(不是週期性的批量更新)。就咱們而言,這意味每次咱們GitHub提交時,咱們經過測試運行來發現是否有什麼異常,正如你想象的,這是一個很是有價值的實踐。不要有「忘記運行測試」的提交。若是你的提交通不過測試,你將收到一封電子郵件被告知。

TravisCI是一種使GitHub項目持續集成更容易的服務。若是你尚未帳號到這看一下注冊一個,完成這些以後,在咱們進入CI以前咱們先須要建立一個簡單的文件。

經過.travis.yml配置

在TravisCI上的不一樣項目經過一個.travis.yml文件來配置,這個文件在項目的根目錄。簡要地說,咱們須要告訴Travis:

  1. 咱們項目使用的語言是什麼
  2. 它使用的是語言的哪一個版本
  3. 使用什麼命令安裝它
  4. 使用什麼命令運行項目的測試

這些都是很直接的東西。下面是sandman.travis.yml的內容:

01 language: python
02 python:
03     - "2.7"
04 install:
05     - "pip install -r requirements.txt --use-mirrors"
06     - "pip install coverage"
07     - "pip install coveralls"
08 script:
09     - "coverage run --source=sandman setup.py test"
10 after_success:
11     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包而且會鼓勵你們來使用和貢獻代碼。而這,就是開源軟件的真正精神,不是嗎?

相關文章
相關標籤/搜索