Building Your Own Python Project
最近有同事花了一點時間 survey、並開了個讀書會分享如何建立一個完整的 Python project 環境,乾脆就趁此機會寫篇文章整理一下,希望可以改善我 project 環境一向亂搞的情況(XD)。這篇筆記是基於這場讀書會的內容,略微修改成我自己喜歡的版本。
這篇筆記會包含以下幾個部分:
- 如何組織一個 Python project
- 如何建立程式的 unit test 環境
- 如何用 docstring 生成 document
- 如何包裝 Python package
但這篇筆記不會教你:
- 如何寫 Python (?
- 如何寫 unit test
- 如何寫 document
不過筆記中都會附上相關的文件連結,有興趣瞭解可以自己點開進去看。
Step 1. Creating Project
首先,讓我們開個新的 project 資料夾,就叫做 demo 吧:
$ mkdir demo
$ cd demo
Step 1-1. Virtual Environment
在開始開發 project 之前,我習慣先用 virtualenv 建立一個獨立的 Python 環境,避免不同 project 或 global 環境的 Python packages 不同版本混亂的問題。
如果你的系統裡還沒有 virtualenv,可以直接利用 pip
來安裝:
$ pip install virtualenv
接著就可以直接在 project 目錄下建立專屬的 virtual environment:
$ virtualenv .venv
New python executable in .venv/bin/python2.7
Also creating executable in .venv/bin/python
Installing setuptools, pip...done.
在這裡我建立了一個 Python 2.7 的 virtual environment,.venv
則是它的名稱。我個人是習慣都這樣取,你也可以自己換成喜歡的名字。
實際上,這個名稱就是存放這個 virtual environment 的資料夾名稱:
$ ls -a
. .. .venv
接著,我們要啟動這個 virtual environment:
$ source .venv/bin/activate
上面的指令是給 bash 的使用者用的。不過如果你像我一樣比較喜歡用 fish 的話,則要改成:
$ . .venv/bin/activate.fish
然後我們就得到一個乾淨的 Python 的環境了:
$ which python
/Users/jason2506/Projects/demo/.venv/bin/python
$ which pip
/Users/jason2506/Projects/demo/.venv/bin/pip
$ pip list
pip (6.0.6)
setuptools (8.2.1)
Step 1-2. Let's Coding
現在可以來開發 project 了。首先,先在目錄下建立 demo/
資料夾當作 top-level package,並在底下建立 __init__.py
與 demo.py
。
demo/__init__.py
Top-level 的 __init__.py
通常是拿來放一些 metadata:
# -*- coding: utf-8 -*-
__version__ = '0.0.1'
__author__ = 'Chi-En Wu'
__email__ = '[email protected]'
__license__ = 'BSD'
__copyright__ = 'Copyright (c) 2015, Chi-En Wu.'
demo/demo.py
接著,讓我們在 demo.py
裡頭寫一個抓取網頁 title 的無用小 function:
# -*- coding: utf-8
from __future__ import print_function
import requests
from lxml import html
def get_title(url):
page = requests.get(url)
root = html.fromstring(page.text)
return root.findtext('.//title')
if __name__ == '__main__':
print(get_title('http://www.google.com/'))
requirements.txt
由於我們有用到 requests 跟 lxml 這兩個額外的 3rd-party packages,我的習慣是會在主目錄下的 requirements.txt
寫下 dependencies:
requests==2.5.1
lxml==3.4.1
詳細的 requirements.txt
檔案格式可以參考 pip 的文件。
setup.py
一個 Python project 最核心的部分就是 setup.py
了:
# -*- coding: utf-8 -*-
"""
Demo
~~~~
Just a simplest project to show you how to build your own project.
:copyright: (c) 2015 by Chi-En Wu <[email protected]>.
:license: BSD.
"""
import uuid
from pip.req import parse_requirements
from setuptools import setup, find_packages
import demo
def requirements(path):
return [str(r.req) for r in parse_requirements(path, session=uuid.uuid1())]
setup(
name='demo',
version=demo.__version__,
author=demo.__author__,
author_email=demo.__email__,
url='http://your.host.name/demo',
description='Just a simplest project to show you how to build your own project.',
long_description=__doc__,
packages=find_packages(),
install_requires=requirements('requirements.txt'),
classifiers=[
'Development Status :: 1 - Planning',
'Environment :: Console',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: Implementation :: PyPy',
'Topic :: Documentation',
'Topic :: Software Development :: Libraries :: Python Modules',
]
)
基本上就是從 setuptools
import setup
,然後呼叫這個 function。其中 name
是 project 的名字,version
、author
、author_email
這三個欄位則是直接用剛剛寫在 demo/__init__.py
裡的 metadata。url
是 project 網站首頁的網址。
description
與 long_description
分別是 project 的簡單與完整描述。long_description
我就直接用 __doc__
引用寫在檔案開頭的 docstring 了,而這個 docstring 是用 reStructuredText 的格式撰寫的。之所以用這個格式,是因為無論之後是上傳到 PyPI,或是用 docstring 產生文件,系統都是只認這種格式的。(雖然我個人其實比較喜歡 markdown,但也只能將就了。)
packages
代表 project 涵蓋的 packages,這裡直接用 setuptools.find_packages()
來自動列出 package 清單。當然你要寫成 packages=['demo']
這樣,自己列出所有 packages 也是可以的。
install_requires
代表 project 的 dependencies,列在這裡的 packages 會在你的 project 安裝的時候同時被 pip 安裝進去。這裡我們則是藉助 pip.req.parse_requirements()
來讀出我們寫在 requirements.txt
的 dependencies。同樣的,你想要自己寫成 install_requires=['requests==2.5.1', 'lxml==3.4.1']
也是可以的。
最後 classifiers
是描述 project 的分類。基本上裡頭列的每一個 classifier 都對應到 PyPI 裡的一個分類,有興趣可以自己到 PyPI 的網站上研究看看。
其餘的參數還有很多,這裡就不一一細講了,要看完整的說明就自己翻文件吧。
MANIFEST.in
呃,因為 setup.py
預設不會把 packages 跟 README 以外的東西發佈出去。所以我們還需要寫個 MANIFEST.in
(同樣附上文件),明確指定我們需要 requirements.txt
。
include requirements.txt
Step 1-3. Try It Out
到目前為止,目錄應該長這樣:
$ tree
.
├── MANIFEST.in
├── demo
│ ├── __init__.py
│ └── demo.py
├── requirements.txt
└── setup.py
現在用 setup.py
來把 project 安裝進系統吧:
$ python setup.py develop
develop
這個命令會使用 development mode 來安裝你的 project,這可以避免你每次修改 code 都要 re-install 你的 project(詳細的說明請參考這裡)。此外,如同前面所說的,這個指令會一併把 install_requires
列的 dependencies 安裝進環境中:
$ pip list
demo (0.0.1)
lxml (3.4.1)
pip (6.0.6)
requests (2.5.1)
setuptools (11.0)
可以看到 requests、lxml 還有我們的 demo 都已經被安裝進我們的環境中了。
然後來試試看剛剛寫的 function:
$ python
Python 2.7.9 (default, Jan 9 2015, 20:15:09)
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.56)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from demo import demo
>>> demo.get_title('http://www.google.com/')
'Google'
>>>
Step 2. Testing
Project 寫好之後,我們還想要測試程式的功能是否都正確。
Step 2-1. Writing Tests
Python 的 unit test packages 很多,像官方的 unittest、unittest2、nose、pytest 等等。這裡我們選擇的是 pytest(暫且沒什麼理由,同事最近推薦我用這套,就先用用看了)。
一樣先利用 pip 安裝:
$ pip install pytest
接著在主目錄建立 tests/
資料夾,project 中的所有 test code 都會放在這裡頭。然後建立一個 test script,叫做 test_demo.py
(名稱隨你喜歡,只要 test_
開頭就可以了)。
tests/test_demo.py
在 test_demo.py
裡頭,直接用 assert
判斷結果是否正確就可以了。
# -*- coding: utf-8 -*-
from demo import demo
def test_get_title():
result = demo.get_title('http://www.google.com/')
expect = 'Google'
assert result == expect
由於 pytest 會自己找到 tests/
底下 test_*.py
裡頭所有 test_
開頭的 function 作為 test cases(詳細的搜尋規則請參考文件),因此只要執行
$ python -m pytest
=================== test session starts ====================
platform darwin -- Python 2.7.9 -- py-1.4.26 -- pytest-2.6.4
collected 1 items
tests/test_demo.py .
================= 1 passed in 0.44 seconds =================
就可以完成所有的測試了。
如果我們故意把答案改成錯的(譬如說 expect = 'google'
):
$ python -m pytest
=================== test session starts ====================
platform darwin -- Python 2.7.9 -- py-1.4.26 -- pytest-2.6.4
collected 1 items
tests/test_demo.py F
========================= FAILURES =========================
---------------------- test_get_title ----------------------
def test_get_title():
result = demo.get_title('http://www.google.com/')
expect = 'google'
> assert result == expect
E assert 'Google' == 'google'
E - Google
E ? ^
E + google
E ? ^
tests/test_demo.py:9: AssertionError
================= 1 failed in 0.38 seconds =================
Step 2-2. Test Coverage
光是測試還不夠,我們還想知道到底我們的測試涵蓋多少程式碼。這時候就需要用到 pytest-cov 這個 pytest 的 plug-in 來幫我們做到這件事:
$ pip install pytest-cov
然後只要在指令後面加上 --cov [MODULE_NAME]
就可以測試 coverage 了:
$ python -m pytest --cov demo
=================== test session starts ====================
platform darwin -- Python 2.7.9 -- py-1.4.26 -- pytest-2.6.4
plugins: cov
collected 1 items
tests/test_demo.py .
----- coverage: platform darwin, python 2.7.9-final-0 ------
Name Stmts Miss Cover
-----------------------------------
demo/__init__ 5 0 100%
demo/demo 9 1 89%
-----------------------------------
TOTAL 14 1 93%
================= 1 passed in 0.87 seconds =================
如果想知道程式有哪幾行沒測到,也可以在指令加上 --cov-report term-missing
:
$ python -m pytest --cov-report term-missing --cov demo
=================== test session starts ====================
platform darwin -- Python 2.7.9 -- py-1.4.26 -- pytest-2.6.4
plugins: cov
collected 1 items
tests/test_demo.py .
----- coverage: platform darwin, python 2.7.9-final-0 ------
Name Stmts Miss Cover Missing
---------------------------------------------
demo/__init__ 5 0 100%
demo/demo 9 1 89% 16
---------------------------------------------
TOTAL 14 1 93%
================= 1 passed in 0.91 seconds =================
Step 2-3. Testing Against Multiple Python Versions
測試是寫好了,但我們實際上只確保當前的 Python 版本可以運作。偏偏 Python 還有 Python 2、Python 3 跟 PyPy 等等不同的版本,所以我們需要每次都一個一個手動切換跑測試嗎?
當然不需要,因為我們有 tox!
$ pip install tox
安裝完成之後,只要在主目錄下寫個 tox.ini
就可以完成設定。
tox.ini
[tox]
envlist = py27, py34, pypy
[testenv]
deps =
-r{toxinidir}/requirements.txt
pytest
pytest-cov
commands =
python -m pytest --cov-report term-missing --cov demo
我們在 tox.ini
用 envlist
指定了三個測試環境:Python 2.7、Python 3.4 跟 PyPy。接著只要執行 tox
,tox 就會自動在當前目錄的 .tox/
建立多個 virtual environment,並分別在裝完 deps
指定的 dependencies 之後執行 commands
指定的指令。
比較需要一提的是 -r{toxinidir}/requirements.txt
這行。{toxinidir}
是 tox.ini
所在的目錄,整行的意思實際上就是代表去讀 tox.ini
所在目錄的 requirements.txt
內的 dependencies。
於是:
$ tox
GLOB sdist-make: /Users/jason2506/Projects/demo/setup.py
py27 create: /Users/jason2506/Projects/demo/.tox/py27
py27 installdeps: -r/Users/jason2506/Projects/demo/requirements.txt, pytest, pytest-cov
py27 inst: /Users/jason2506/Projects/demo/.tox/dist/demo-0.0.1.zip
py27 runtests: PYTHONHASHSEED='2621270020'
py27 runtests: commands[0] | python -m pytest --cov-report term-missing --cov demo
=================== test session starts ====================
platform darwin -- Python 2.7.9 -- py-1.4.26 -- pytest-2.6.4
plugins: cov
collected 1 items
tests/test_demo.py .
----- coverage: platform darwin, python 2.7.9-final-0 ------
Name Stmts Miss Cover Missing
---------------------------------------------
demo/__init__ 5 0 100%
demo/demo 9 1 89% 16
---------------------------------------------
TOTAL 14 1 93%
================= 1 passed in 0.75 seconds =================
py34 create: /Users/jason2506/Projects/demo/.tox/py34
py34 installdeps: -r/Users/jason2506/Projects/demo/requirements.txt, pytest, pytest-cov
py34 inst: /Users/jason2506/Projects/demo/.tox/dist/demo-0.0.1.zip
py34 runtests: PYTHONHASHSEED='2621270020'
py34 runtests: commands[0] | python -m pytest --cov-report term-missing --cov demo
=================== test session starts ====================
platform darwin -- Python 3.4.2 -- py-1.4.26 -- pytest-2.6.4
plugins: cov
collected 1 items
tests/test_demo.py .
----- coverage: platform darwin, python 3.4.2-final-0 ------
Name Stmts Miss Cover Missing
---------------------------------------------
demo/__init__ 5 0 100%
demo/demo 9 1 89% 16
---------------------------------------------
TOTAL 14 1 93%
================= 1 passed in 0.77 seconds =================
pypy create: /Users/jason2506/Projects/demo/.tox/pypy
pypy installdeps: -r/Users/jason2506/Projects/demo/requirements.txt, pytest, pytest-cov
pypy inst: /Users/jason2506/Projects/demo/.tox/dist/demo-0.0.1.zip
pypy runtests: PYTHONHASHSEED='2621270020'
pypy runtests: commands[0] | py.test --cov-report term-missing --cov demo
=================== test session starts ====================
platform darwin -- Python 2.7.8[pypy-2.4.0-final] -- py-1.4.26 -- pytest-2.6.4
plugins: cov
collected 1 items
tests/test_demo.py .
----- coverage: platform darwin, python 2.7.8-final-42 -----
Name Stmts Miss Cover Missing
---------------------------------------------
demo/__init__ 5 0 100%
demo/demo 9 1 89% 16
---------------------------------------------
TOTAL 14 1 93%
================= 1 passed in 2.48 seconds =================
------------------------- summary --------------------------
py27: commands succeeded
py34: commands succeeded
pypy: commands succeeded
congratulations :)
可以看到 tox 在 .tox/
底下分別為不同的 Python 版本建立獨立的 virtual environment,並在裝完 dependencies 之後自動跑完所有的測試。
Step 2-4. Coverage Threshold
讓我們再龜毛一點,我們希望 coverage 在 95% 以下的話,就讓整個 test fail。
但這裡有個小問題:pytest-cov 雖然有 --cov-min [MIN_COVERAGE]
這個 option 可以做這件事,但在目前的最新穩定版(1.8.1)這個功能還無法使用(根據它 github 的 issue 來看,要到 2.0 版以後才會正式支援)。
沒辦法,我們只好用 coverage 來幫我們做到這件事。由於在執行 py.test --cov
的時候實際上會在主目錄擺一個 .coverage
來存放 coverage 的結果。又因為 pytest-cov 實際上就是建構在 coverage 之上,所以在執行完 py.test --cov
之後,可以直接用 coverage report
來讀取並印出 coverage 的結果:
; tox.ini
; ...
commands =
python -m pytest --cov-report "" --cov demo
coverage report --show-missing --fail-under 95
coverage
的 --show-missing
等同於 pytest-cov 的 --cov-report term-missing
,後面的 --fail-under 95
則指定了 test 會在 coverage 小於 95% 的時候 fail。
由於印出 coverage report 這項重責大任我們已經委任給 coverage
了,所以我在 py.test
後面加上 --report ""
以防止這行指令重複印出 report。
$ tox
...
ERROR: InvocationError: '/Users/jason2506/Projects/demo/.tox/py27/bin/coverage report --show-missing --fail-under 95'
...
ERROR: InvocationError: '/Users/jason2506/Projects/demo/.tox/py34/bin/coverage report --show-missing --fail-under 95'
...
ERROR: InvocationError: '/Users/jason2506/Projects/demo/.tox/pypy/bin/coverage report --show-missing --fail-under 95'
------------------------- summary --------------------------
ERROR: py27: commands failed
ERROR: py34: commands failed
ERROR: pypy: commands failed
因為我們的 coverage 只有 93%,所以可以看到我們的測試 fail 了。
如果你想直接用 coverage 來跑 pytest,這樣寫也是可以的:
; tox.ini
; ...
[testenv]
deps =
-r{toxinidir}/requirements.txt
pytest
coverage
commands =
coverage run --source demo -m pytest
coverage report --show-missing --fail-under 95
這個的執行結果與上面完全相同,但不需要額外安裝 pytest-cov。
Step 2-5. Excluding Code from Coverage
此外,可以在程式中用註解明確指定某個部分不要列入 coverage 的計算:
# demo/demo.py
# ...
if __name__ == '__main__': # pragma: no cover
print(get_title('http://www.google.com/'))
然後再跑一次看看:
$ python -m pytest --cov demo --cov-report term-missing
=================== test session starts ====================
platform darwin -- Python 2.7.9 -- py-1.4.26 -- pytest-2.6.4
plugins: cov
collected 1 items
tests/test_demo.py .
----- coverage: platform darwin, python 2.7.9-final-0 ------
Name Stmts Miss Cover Missing
---------------------------------------------
demo/__init__ 5 0 100%
demo/demo 7 0 100%
---------------------------------------------
TOTAL 12 0 100%
================= 1 passed in 0.40 seconds =================
$ tox
...
------------------------- summary --------------------------
py27: commands succeeded
py34: commands succeeded
pypy: commands succeeded
congratulations :)
可以發現 coverage 變成 100%,也因此可以通過 coverage threshold 的測試了。
但請注意:這個步驟不是用來無條件衝高 coverage 的,而是為了不要讓過多較無意義的程式片段(如上例的 if __name__ == '__main__'
、或是 debug 用的 code)拉低整體的 coverage。
Step 2-6. Packaging Tests
前面也提到了,setup.py
預設不會把 packages 跟 README 以外的東西發佈出去。所以我們要在 MANIFEST.in
裡請 setup.py
一併發佈 tox.ini
跟 tests/
底下所有的 .py
檔:
include requirements.txt
include tox.ini
recursive-include tests *.py
此外,我們可以讓 setup.py
支援跑 tox 測試的操作:
# setup.py
# ...
from pip.req import parse_requirements
from setuptools import setup, find_packages
from setuptools.command.test import test as TestCommand
# ...
class Tox(TestCommand):
def finalize_options(self):
TestCommand.finalize_options(self)
self.test_args = ['-v', '-epy']
self.test_suite = True
def run_tests(self):
import tox
tox.cmdline(self.test_args)
setup(
# ...
tests_require=['tox'],
cmdclass={'test': Tox},
# ...
)
之後就可以用 setup.py
的 test
命令來跑 tox 測試了:
$ python setup.py test
到目前為止,檔案結構長這樣(扣掉自動產生的檔案):
$ tree
.
├── MANIFEST.in
├── demo
│ ├── __init__.py
│ └── demo.py
├── requirements.txt
├── setup.py
├── tests
│ └── test_demo.py
└── tox.ini
Step 3. Documentation
程式寫好了,測試也過了,但是說好的文件呢?
這裡我們採用從 docstring 產生文件的方式。而在 Python 社群中,最有名的 document generator 大概非 sphinx 莫屬了。
$ pip install sphinx
Step 3-1. Quickstart
安裝完成之後可以透過 sphinx-quickstart
來自動生成一些基礎的設定檔:
$ sphinx-quickstart
然後按照指示設定就可以了。以下是我自己的設定:
> Root path for the documentation [.]: docs
這個代表我們要把 document 相關的設定擺在 docs/
這個資料夾。
> Separate source and build directories (y/N) [n]: n
如果這個選項選 y
,程式會在 docs/
底下再創造一個獨立的資料夾擺 document 的原始碼。但因為我們已經把文件放在獨立的 docs/
資料夾裡了,這裡選 n
也不會讓檔案結構變得太混亂。
> Name prefix for templates and static dir [_]: <ENTER>
templates 跟 static 是用來擺放 document 頁面的 template 跟 CSS/JS 之類的。這個選項只是用來設定這兩個資料夾名稱的 prefix,一般來說維持預設即可。
> Project name: Demo
> Author name(s): Chi-En Wu
> Project version: 0.0.1
> Project release [0.0.1]: <ENTER>
設定 project 的基本資訊。
> Source file suffix [.rst]: <ENTER>
> Name of your master document (without suffix) [index]: <ENTER>
設定 document 原始碼的附檔名,以及 document 主頁的檔案名稱。基本上維持預設即可。
> Do you want to use the epub builder (y/n) [n]: n
設定是否要支援產生 epub 格式的文件。
> autodoc: automatically insert docstrings from modules (y/N) [n]: y
設定是否要利用 docstring 來自動生成文件(請參考 sphinx.ext.autodoc)。基本上我們主要需要的就是這個功能,當然選 y
囉。
> doctest: automatically test code snippets in doctest blocks (y/N) [n]: y
設定是否要測試 docstring 裡頭的 examples(請參考 sphinx.ext.doctest)。
> intersphinx: link between Sphinx documentation of different projects (y/N) [n]: y
設定是否能夠連結不同 project 的 Sphinx 文件(請參考 sphinx.ext.intersphinx)。如果有用到其它 Python project,這個功能很方便。
> todo: write “todo” entries that can be shown or hidden on build (y/N) [n]: y
設定是否要在文件中顯示標為 todo 的項目(請參考 sphinx.ext.todo)。
> coverage: checks for documentation coverage (y/N) [n]: y
設定是否要支援計算 document coverage 的功能(請參考 sphinx.ext.coverage)。
> pngmath: include math, rendered as PNG images (y/N) [n]: n
> mathjax: include math, rendered in the browser by MathJax (y/n) [n]: n
如果要在文件中寫數學式,可以把其中一個改成 y
(請參考 Math support in Sphinx)。基本上我都不會用到,所以選 n
。
> ifconfig: conditional inclusion of content based on config values (y/N) [n]: n
設定是否能夠用 config 變數決定文件是否要不要包含特定內容(請參考 sphinx.ext.ifconfig)。
> viewcode: include links to the source code of documented Python objects (y/n) [n]: y
設定是否能夠在文件中連結 Python 的 source code(請參考 sphinx.ext.viewcode)。
> Create Makefile? (Y/n) [y]: y
> Create Windows command file? (Y/n) [y]: n
是否產生 Makefile 跟 Windows 批次檔。由於我不是用 Windows,直接用 Makefile 來產生 document 就可以了。
完成之後會在主目錄下產生一個 docs/
,結構應該長這樣:
$ tree
.
│ ...
├── docs
│ ├── Makefile
│ ├── _build/
│ ├── _static/
│ ├── _templates/
│ ├── conf.py
│ └── index.rst
│ ...
接著對 docs/conf.py
做一點小修改。
首先,為了讓 sphinx 能找到 project 的 source code,我們要將正確的位置加到搜尋路徑中。也就是把
#sys.path.insert(0, os.path.abspath('.'))
改成
sys.path.insert(0, os.path.abspath('..'))
此外,如果 project 中的不同 module 可能是由不同人撰寫的話,也可以設定為獨立顯示每個 module 的作者:
#show_authors = False
改成
show_authors = True
Step 3-2. Writing Docstring
現在我們要在原始的程式碼加上 docstring。這些加上去的內容會自動被 sphinx 抓出來編成文件。
demo/__init__.py
# -*- coding: utf-8 -*-
"""An example module.
.. moduleauthor:: Chi-En Wu <[email protected]>
"""
from __future__ import print_function
import requests
from lxml import html
def get_title(url):
"""Get the title of a web page.
:param url: url of web page.
:type url: str
:returns: title of web page.
:rtype: str
>>> from demo import demo
>>> demo.get_title('http://www.google.com/')
'Google'
.. versionadded:: 0.0.1
"""
page = requests.get(url)
root = html.fromstring(page.text)
return root.findtext('.//title')
如同前面說過的,這裡的 docstring 是用 reStructuredText 的格式撰寫的。parameters 跟 return value 的寫法可以參考這裡。
Step 3-3. API Document
接著,這裡要用官方提供的 sphinx-apidoc
來自動產生 API 文件:
$ sphinx-apidoc -o docs/api demo
Creating file docs/api/demo.rst.
Creating file docs/api/modules.rst.
可以看到這個命令在 docs/api
目錄下自動為每個 sub-module 產生 .rst
檔。
然後,我們就可以用 Makefile 來產生 HTML document。
$ cd docs
$ make html
sphinx-build -b html -d _build/doctrees . _build/html
Making output directory...
Running Sphinx v1.2.3
loading pickled environment... done
building [html]: targets for 3 source files that are out of date
updating environment: 0 added, 1 changed, 0 removed
reading sources... [100%] api/demo
looking for now-outdated files... none found
pickling environment... done
checking consistency... /Users/jason2506/Projects/demo/docs/api/modules.rst:: WARNING: document isn't included in any toctree
done
preparing documents... done
writing output... [100%] index
writing additional files... (1 module code pages) _modules/index genindex py-modindex search
copying static files... done
copying extra files... done
dumping search index... done
dumping object inventory... done
build succeeded, 1 warning.
Build finished. The HTML pages are in _build/html.
docs/
目錄下所有的 .rst
都會產生一個對應的 HTML 檔案。生成的 HTML document 擺在 _build/html/
裡頭。
打開 index.html
右上角的 modules 或 index 連結之後,點開 demo
這個 module 的頁面應該就可以看到剛剛的註解變成文件了。不過我懶得截圖了,各位看倌就自己編來看看吧(XD)。
但要注意的是,如果有增刪 modules 的話,docs/api
裡不會自動更新。到時候還是要重新用 sphinx-apidoc
來重新產生 *.rst
。
Step 3-4. Doctest
前面有提到我們可以測試 docstring 裡頭的 examples(>>>
開頭的那幾行)。要用到這項功能請先確定 quickstart 時的 doctest 選項是否有選 y
。如果沒有的話,可以修改 docs/conf.py
,找到 extensions = [...]
,並在裡頭加入 'sphinx.ext.doctest'
。
然後移動到 docs/
裡打 make doctest
就可以了:
$ cd docs
$ make doctest
sphinx-build -b doctest -d _build/doctrees . _build/doctest
Running Sphinx v1.2.3
loading pickled environment... done
building [doctest]: targets for 3 source files that are out of date
updating environment: 0 added, 1 changed, 0 removed
reading sources... [100%] api/demo
looking for now-outdated files... none found
pickling environment... done
checking consistency... /Users/jason2506/Projects/demo/docs/api/modules.rst:: WARNING: document isn't included in any toctree
done
running tests...
Document: api/demo
------------------
1 items passed all tests:
2 tests in default
2 tests in 1 items.
2 passed and 0 failed.
Test passed.
Doctest summary
===============
2 tests
0 failures in tests
0 failures in setup code
0 failures in cleanup code
build succeeded, 1 warning.
Testing of doctests in the sources finished, look at the results in _build/doctest/output.txt.
這個功能可以用來隨時檢查文件中的 examples 是否正確。
Step 3-5. Documentation Coverage
test code 可以算 coverage,document 當然也可以(XD)。
首先,要先確定 quickstart 時的 coverage 選項是否有選 y
。如果沒有的話,可以修改 docs/conf.py
,找到 extensions = [...]
,並在裡頭加入 'sphinx.ext.coverage'
。
接著修改 docs/Makefile
,在最底下加上
coverage:
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
@echo
@cat $(BUILDDIR)/coverage/python.txt
然後找到 .PHONY:
這行,在最後面加上 coverage
。
接著只要 make coverage
就可以看到沒有被列在 document 的 Python objects(classes, methods, functions, ...etc)了:
$ cd docs
$ make coverage
sphinx-build -b coverage -d _build/doctrees . _build/coverage
Making output directory...
Running Sphinx v1.2.3
loading pickled environment... done
building [coverage]: coverage overview
updating environment: 0 added, 0 changed, 0 removed
looking for now-outdated files... none found
build succeeded.
Undocumented Python objects
===========================
注意,Undocumented Python objects 是沒有被列在 document 的 Python objects,不見得是沒有 docstring 的 Python objects。如果有用 :undoc-members:
把沒有 docstring 的 Python objects 加到 document 裡面,還是會被當成 documented Python objects。
Step 3-6. Packaging Docs
同樣的,我們要在 MANIFEST.in
裡請 setup.py
一併發佈 docs/
底下的 documents:
include requirements.txt
include tox.ini
recursive-include tests *.py
graft docs
prune docs/_build
另外,由於 setuptools 也支援 build_sphinx
這個擴充命令,所以我們也可以加入一個 setup.cfg
,把 sphinx 的設定寫進去。
setup.cfg
[build_sphinx]
source-dir = docs
build-dir = docs/_build
all_files = 1
之後就可以用 build_sphinx
命令來生成文件了:
$ python setup.py build_sphinx
Step 4. Deployment
費盡千辛萬苦,終於可以把 project 發佈出去了。首先我們要先用 setup.py
的 sdist
命令來包裝整個 project。
$ python setup.py sdist
running sdist
running egg_info
writing requirements to demo.egg-info/requires.txt
writing demo.egg-info/PKG-INFO
writing top-level names to demo.egg-info/top_level.txt
writing dependency_links to demo.egg-info/dependency_links.txt
reading manifest file 'demo.egg-info/SOURCES.txt'
reading manifest template 'MANIFEST.in'
writing manifest file 'demo.egg-info/SOURCES.txt'
warning: sdist: standard file not found: should have one of README, README.rst, README.txt
running check
creating demo-0.0.1
creating demo-0.0.1/demo
creating demo-0.0.1/demo.egg-info
creating demo-0.0.1/docs
creating demo-0.0.1/docs/api
creating demo-0.0.1/tests
making hard links in demo-0.0.1...
hard linking MANIFEST.in -> demo-0.0.1
hard linking requirements.txt -> demo-0.0.1
hard linking setup.cfg -> demo-0.0.1
hard linking setup.py -> demo-0.0.1
hard linking tox.ini -> demo-0.0.1
hard linking demo/__init__.py -> demo-0.0.1/demo
hard linking demo/demo.py -> demo-0.0.1/demo
hard linking demo.egg-info/PKG-INFO -> demo-0.0.1/demo.egg-info
hard linking demo.egg-info/SOURCES.txt -> demo-0.0.1/demo.egg-info
hard linking demo.egg-info/dependency_links.txt -> demo-0.0.1/demo.egg-info
hard linking demo.egg-info/requires.txt -> demo-0.0.1/demo.egg-info
hard linking demo.egg-info/top_level.txt -> demo-0.0.1/demo.egg-info
hard linking docs/Makefile -> demo-0.0.1/docs
hard linking docs/conf.py -> demo-0.0.1/docs
hard linking docs/index.rst -> demo-0.0.1/docs
hard linking docs/api/demo.rst -> demo-0.0.1/docs/api
hard linking docs/api/modules.rst -> demo-0.0.1/docs/api
hard linking tests/test_demo.py -> demo-0.0.1/tests
copying setup.cfg -> demo-0.0.1
Writing demo-0.0.1/setup.cfg
creating dist
Creating tar archive
removing 'demo-0.0.1' (and everything under it)
可以確認一下是不是 tox.ini
、tests/
、docs/
之類該包的檔案都包進去了。
都包好之後,就可以自行用
$ python setup.py upload
上傳到官方或是其它自己架的 PyPI 上面。像我們公司用的是 devpi,可以參考這裡。
Summary
這篇筆記基本上只是介紹如何從無到有建立一個包含 test code 與 document 的 Python project。由於篇幅已經不短了(再加上我懶得寫,哈哈哈),關於如何寫 unit test 跟 sphinx document 就不在這篇細講。上面的每個部分我都有擺上對應的文件連結,如果想確實把每一個步驟做好,還是建議自己仔細地把相關文件讀完。
File Structure
$ tree
.
├── MANIFEST.in
├── [PACKAGE_NAME]
│ ├── __init__.py
│ └── ...
├── docs
│ ├── Makefile
│ ├── _build/
│ ├── _static/
│ ├── _templates/
│ ├── conf.py
│ └── index.rst
├── requirements.txt
├── setup.cfg
├── setup.py
├── tests
│ ├── test_*.py
│ └── ...
└── tox.ini
Tools
- Virtual Environment:virtualenv
- Packaging:setuptools
- Testing:pytest + pytest-cov(coverage) + tox
- Documentation:sphinx
Commands
- Package:
python setup.py sdist
- Test:
tox
orpython setup.py test
- Generate Document:
python setup.py build_sphinx