簡介
這本 Python 備忘錄內容包含 Python 2.x 和 Python 3.x .
作業系統是 Mac OS X 10.10.5 .
在沒有啟用任何虛擬環境的情況下, 預設的 python 指令是 python2, 預設的 pip 指令是 pip2, 預設的 idle 指令是 idle2 .
不論是否啟用虛擬環境 , Vim 編輯器連結的環境是 Python2 .
目錄結構
~/Desktop/
python-projects/
.gitignore # .gitignore 檔案範本
README.txt # README.txt 檔案範本
__init__.py # __init__.py 檔案範本
__main__.py # __main__.py 檔案範本
cmdcopy.py # cmdcopy.py 檔案範本
cmdls.py # cmdls.py 檔案範本
cmdsum.py # cmdsum.py 檔案範本
create-project.sh
helloworld.py # helloworld.py 檔案範本
requirements.txt # requirements.txt 檔案範本
setup.py # setup.py 檔案範本
start-idle.py
test_lib.py # test_lib.py 檔案範本
test_lib_cmdsum_case1.txt # test_lib_cmdsum_case1.txt 檔案範本
test_lib_cmdsum_case2.txt # test_lib_cmdsum_case2.txt 檔案範本
test_lib_cmdsum_case3.txt # test_lib_cmdsum_case3.txt 檔案範本
test_ut_lib.py # test_ut_lib.py 檔案範本
tox.ini # tox.ini 檔案範本
project1/ # 專案名稱
py2/ # 虛擬環境名稱
src/ # 本地 Git Repository 名稱
.gitignore # 預設是來自檔案範本
README.txt # 預設是來自檔案範本
requirements.txt # 預設是來自檔案範本
setup.py # 預設是來自檔案範本 , 替換內容中關於專案名稱和直譯器版本資訊
tox.ini # 預設是來自檔案範本 , 替換內容中關於專案名稱
project1/ # Top Package 名稱
__init__.py # 預設是空白檔案
__main__.py # 預設是來自檔案範本
helloworld.py # 預設是來自檔案範本
lib/ # Sub Package 名稱
__init__.py # 預設是來自檔案範本
cmdcopy.py # 預設是來自檔案範本
cmdsum.py # 預設是來自檔案範本
cmdls.py # 預設是來自檔案範本
tests/ # pytest 測試腳本目錄
test_lib.py # 預設是來自檔案範本 , 替換內容中關於專案名稱
test_lib_cmdsum_case1.txt # 預設是來自檔案範本 , 替換內容中關於專案名稱
test_lib_cmdsum_case2.txt # 預設是來自檔案範本 , 替換內容中關於專案名稱
test_lib_cmdsum_case3.txt # 預設是來自檔案範本 , 替換內容中關於專案名稱
unittests/ # unittest 測試腳本目錄
test_ut_lib.py # 預設是來自檔案範本 , 替換內容中關於專案名稱
project2/
py2/ # 可以在 Python2 上執行
project3/
py3/ # 可以在 Python3 上執行
project4/
py6/ # 可以在 Python2 和 Python3 上執行
關於 .gitignore 範本
*.pyc
MANIFEST
dist/
*.egg-info/
build/
.cache/
.tox/
關於 README.txt 範本
關於 __init__.py
範本
from . import cmdcopy
from . import cmdsum
from . import cmdls
關於 __main__.py
範本
# -*- coding: utf-8 -*-
import sys
import argparse
from . import helloworld
from . import lib
def main():
# add parser
parser = argparse.ArgumentParser(description='ADD YOUR DESCRIPTION HERE')
parser.add_argument('-v', '--verbose',
help='increase output verbosity',
required=False, action="count", default=0)
parser.add_argument('-d', '--debug',
help='turn on debug mode',
required=False, action="store_true")
# add subparsers
subparsers = parser.add_subparsers(title='subcommands')
# add subcommand - copy
parser_copy = subparsers.add_parser('copy', help='copy file')
parser_copy.add_argument('-i', '--input', help='input file name',
required=True)
parser_copy.add_argument('-o', '--output', help='output file name',
required=True)
parser_copy.set_defaults(func=lib.cmdcopy.do_copy)
# add subcommand - sum
parser_sum = subparsers.add_parser('sum',
help='calculate the sum of numbers')
parser_sum.add_argument('number', metavar='N', type=int, nargs='+',
help='a list of numbers for sum operation')
parser_sum.set_defaults(func=lib.cmdsum.do_sum)
# add subcommand - ls
parser_ls = subparsers.add_parser('ls',
help='list files or folders')
parser_ls.add_argument('file', metavar='F', type=str, nargs='+',
help='a list of files or folders for ls operation')
parser_ls.set_defaults(func=lib.cmdls.do_ls)
# add subcommand - helloworld
parser_helloworld = subparsers.add_parser('helloworld',
help='print hello world')
parser_helloworld.set_defaults(func=helloworld.show)
# parse
args = parser.parse_args()
if len(sys.argv) == 1:
parser.print_help()
else:
if args.debug:
print "Turning on debug mode ..."
print sys.argv
print args
else:
print "Turning off debug mode ..."
if args.verbose >= 2:
print "Turning on verbose mode II ..."
elif args.verbose == 1:
print "Turning on verbose mode I ..."
else:
print "Turning off verbose mode ..."
args.func(args)
if __name__ == "__main__":
main()
關於 cmdcopy.py 範本
def do_copy(args):
print "Copying", args.input, args.output
from shutil import copyfile
copyfile(args.input, args.output)
關於 cmdls.py 範本
def do_ls(args):
print "Listing", args.file
import subprocess
print subprocess.check_output(['ls'] + args.file)
關於 cmdsum.py 範本
def do_sum(args):
"""Calculate the sum of numbers.
...
>>> import argparse
>>> args = argparse.Namespace()
>>> args.number = []
>>> do_sum(args)
Calculating the sum of []
0
0
>>> args.number = [1]
>>> do_sum(args)
Calculating the sum of [1]
1
1
>>> args.number = [1,2]
>>> do_sum(args)
Calculating the sum of [1, 2]
3
3
"""
print "Calculating the sum of", args.number
print sum(args.number)
return sum(args.number)
if __name__ == "__main__":
import doctest
doctest.testmod()
關於 create-project.sh
#!/bin/bash
# Usage ./create-project [name]
#
# Environment Variable
# - PP_PYTHON_VERSION : 2 | 3 , default 2
# - PP_GITHUB : ON | OFF , default OFF
# _ PP_PYPI : ON | OFF , default OFF
# set variable $ID
if [ $# -lt 1 ]; then
ID=pp$(date +%Y%m%d%H%M%S) # pp is short for python project
else
ID=$1
fi
# set variable $PP_PYTHON_VERSION
[ -z $PP_PYTHON_VERSION ] && PP_PYTHON_VERSION=2
# set variable $PP_GITHUB
[ -z $PP_GITHUB ] && PP_GITHUB=OFF
# set variable $PP_PYPI
[ -z $PP_PYPI ] && PP_PYPI=OFF
# create project with name $ID
# - create local project directory
# - create github repository (optional)
# - create pipy package (optional)
if [ -d $ID ]; then
echo "Error: project $ID already exists."
exit 1
else
if [ $PP_PYTHON_VERSION == 2 ]; then
_python=python2
_venv_name=$ID/py2
else
_python=python3
_venv_name=$ID/py3
fi
_src_name=$_venv_name/src
_top_pkg_name=$_src_name/$ID
_sub_pkg_name=$_top_pkg_name/lib
_test_name=$_src_name/tests
_unittest_name=$_src_name/unittests
mkdir $ID &&
(
if [ $PP_PYTHON_VERSION == 2 ]; then
virtualenv $_venv_name
else
python3 -m venv $_venv_name
fi
) &&
(
mkdir -p $_sub_pkg_name &&
mkdir $_test_name &&
mkdir $_unittest_name
) &&
(
touch $_src_name/{.gitignore,README.txt,requirements.txt,setup.py,tox.ini} &&
touch $_top_pkg_name/{__init__.py,__main__.py,helloworld.py} &&
touch $_sub_pkg_name/{__init__.py,cmdcopy.py,cmdls.py,cmdsum.py} &&
touch $_test_name/{test_lib.py,test_lib_cmdsum_case1.txt,test_lib_cmdsum_case2.txt,test_lib_cmdsum_case3.txt} &&
touch $_unittest_name/test_ut_lib.py
) &&
(
cp .gitignore $_src_name/.gitignore &&
cp README.txt $_src_name/README.txt &&
cp requirements.txt $_src_name/requirements.txt &&
sed 's/PROJECT-ID/'$ID'/g' setup.py | sed 's/PROJECT-PYTHON-VERSION/'$PP_PYTHON_VERSION'/g' > $_src_name/setup.py &&
sed 's/PROJECT-ID/'$ID'/g' tox.ini > $_src_name/tox.ini &&
cp __main__.py $_top_pkg_name/__main__.py &&
cp helloworld.py $_top_pkg_name/helloworld.py &&
cp __init__.py $_sub_pkg_name/__init__.py &&
cp cmdcopy.py $_sub_pkg_name/cmdcopy.py &&
cp cmdls.py $_sub_pkg_name/cmdls.py &&
cp cmdsum.py $_sub_pkg_name/cmdsum.py &&
sed 's/PROJECT-ID/'$ID'/g' test_lib.py > $_test_name/test_lib.py &&
sed 's/PROJECT-ID/'$ID'/g' test_lib_cmdsum_case1.txt > $_test_name/test_lib_cmdsum_case1.txt &&
sed 's/PROJECT-ID/'$ID'/g' test_lib_cmdsum_case2.txt > $_test_name/test_lib_cmdsum_case2.txt &&
sed 's/PROJECT-ID/'$ID'/g' test_lib_cmdsum_case3.txt > $_test_name/test_lib_cmdsum_case3.txt &&
sed 's/PROJECT-ID/'$ID'/g' test_ut_lib.py > $_unittest_name/test_ut_lib.py
) &&
(
cd $_src_name && git init . && git add . && git commit -m "first commit" && cd -
) &&
(
if [ $PP_GITHUB == "ON" ]; then
curl -i -u jinsenglin -d '{"name":"'$ID'"}' -X POST https://api.github.com/user/repos &&
cd $_src_name && git remote add github [email protected]:jinsenglin/$ID.git && git push -u github master && cd -
fi
) &&
(
if [ $PP_PYPI == "ON" ]; then
cd $_src_name && $_python setup.py register && $_python setup.py sdist upload && cd -
fi
)
fi
關於 helloworld.py 範本
# -*- coding: utf-8 -*-
def show(args):
print(__name__)
關於 requirements.txt 範本
pluggy==0.3.1
py==1.4.31
pytest==2.9.1
tox==2.3.1
virtualenv==15.0.1
關於 setup.py 範本
# -*- coding: utf-8 -*-
from setuptools import setup
long_desc = """Programming Exercise for Python is a collection of sample code
for demostration of concept and usage of Python Language."""
setup(name='PROJECT-ID',
version='0.0.1',
packages=['PROJECT-ID', 'PROJECT-ID.lib'],
entry_points={'console_scripts': [
'PROJECT-ID = PROJECT-ID.__main__:main',
]},
author='Jim Lin',
author_email='[email protected]',
url='https://github.com/jinsenglin/PROJECT-ID/',
description='Programming Exercise for Python',
long_description=long_desc,
license="Apache 2.0",
classifiers=[
'Programming Language :: Python :: PROJECT-PYTHON-VERSION'
],
install_requires=[]
)
為了使用 entry_points 參數 , 所以使用 from setuptools from setup 而非 from distutils.core import setup
關於 start-idle.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from idlelib.PyShell import main
if __name__ == '__main__':
main()
關於 test_lib.py 範本
from PROJECT-ID import lib
def test_do_sum():
import argparse
args = argparse.Namespace()
args.number = []
assert lib.cmdsum.do_sum(args) == 0
args.number = [1]
assert lib.cmdsum.do_sum(args) == 1
args.number = [1, 2]
assert lib.cmdsum.do_sum(args) == 3
def test_do_copy():
pass
def test_do_ls():
pass
關於 test_lib_cmdsum_case1.txt 範本
>>> import argparse
>>> args = argparse.Namespace()
>>> args.number = []
>>> from PROJECT-ID import lib
>>> lib.cmdsum.do_sum(args)
Calculating the sum of []
0
0
關於 test_lib_cmdsum_case2.txt 範本
>>> import argparse
>>> args = argparse.Namespace()
>>> args.number = [1]
>>> from PROJECT-ID import lib
>>> lib.cmdsum.do_sum(args)
Calculating the sum of [1]
1
1
關於 test_lib_cmdsum_case3.txt 範本
>>> import argparse
>>> args = argparse.Namespace()
>>> args.number = [1,2]
>>> from PROJECT-ID import lib
>>> lib.cmdsum.do_sum(args)
Calculating the sum of [1, 2]
3
3
關於 test_ut_lib.py 範本
import unittest
from PROJECT-ID import lib
class TestLib(unittest.TestCase):
def test_do_sums(self):
import argparse
args = argparse.Namespace()
args.number = []
assert lib.cmdsum.do_sum(args) == 0
args.number = [1]
assert lib.cmdsum.do_sum(args) == 1
args.number = [1, 2]
assert lib.cmdsum.do_sum(args) == 3
def test_do_copy(self):
pass
def test_do_ls(self):
pass
if __name__ == '__main__':
unittest.main()
關於 tox.ini 範本
[tox]
envlist = py27
[testenv]
deps = -rrequirements.txt
commands = py.test {posargs:./PROJECT-ID ./tests}
建立專案
cd ~/Desktop/python-projects/ && ./create-project.sh
# e.g., 專案一
source ~/Desktop/python-projects/project1/py2/bin/activate
cd ~/Desktop/python-projects/project1/py2/src && pip install -r requirements.txt
編輯專案 (使用 IDLE)
# e.g., 專案一
source ~/Desktop/python-projects/project1/py2/bin/activate
cd ~/Desktop/python-projects/project1/py2/src && ~/Desktop/python-projects/start-idle.py
# e.g., 專案三
source ~/Desktop/python-projects/project1/py3/bin/activate
cd ~/Desktop/python-projects/project1/py3/src && ~/Desktop/python-projects/start-idle.py
編輯專案 (使用 PyCharm)
# e.g., 專案一
Open ~/Desktop/python-projects/project1/py2
測試專案
# e.g., 專案一
source ~/Desktop/python-projects/project1/py2/bin/activate
cd ~/Desktop/python-projects/project1/py2/src && python -m project1
測試專案 (使用 PyCharm)
# e.g., 專案一
# Run Configuration
# Name = project1
# Script = project1
# Script parameters = sum 1 2 3
# (預設) Environment variables = PYTHONUNBUFFERED=1
# (預設) Python 2.7.11 virtualenv at ~/Desktop/python-projects/project1/py2
# Interpreter options = -m
# Working directory = /Users/cclin/Desktop/python-projects/project1/py2/src
# 勾選 Add content roots to PYTHONPATH
# 勾選 Add source roots to PYTHONPATH
測試專案 (使用 doctest)
# e.g., 專案一
source ~/Desktop/python-projects/project1/py2/bin/activate
# 當 cmdsum.py 內使用 doctest.testmod()
python ~/Desktop/python-projects/project1/py2/src/project1/lib/cmdsum.py -v
# 當 cmdsum.py 內沒有使用 doctest.testmod()
python -m doctest ~/Desktop/python-projects/project1/py2/src/project1/lib/cmdsum.py -v
測試專案 (使用 unittest)
# e.g., 專案一
source ~/Desktop/python-projects/project1/py2/bin/activate
# 當 test_ut_lib.py 內使用 unittest.main()
export PYTHONPATH=~/Desktop/python-projects/project1/py2/src && python ~/Desktop/python-projects/project1/py2/src/unittests/test_ut_lib.py
# 當 test_ut_lib.py 內沒有使用 unittest.main() , 或當一次測試多個案例時
export PYTHONPATH=~/Desktop/python-projects/project1/py2/src && python -m unittest discover ~/Desktop/python-projects/project1/py2/src/unittests
測試專案 (使用 py.test)
# e.g., 專案一
source ~/Desktop/python-projects/project1/py2/bin/activate
cd ~/Desktop/python-projects/project1/py2/src/tests && export PYTHONPATH=~/Desktop/python-projects/project1/py2/src && py.test
測試專案 (使用 tox)
# e.g., 專案一
source ~/Desktop/python-projects/project1/py2/bin/activate
cd ~/Desktop/python-projects/project1/py2/src && tox
更新專案
cd ~/Desktop/python-projects/project1/py2/src
pip freeze > requirements.txt
git add .
git commit -m "0.0.2" # 在 setup.py 先變更版號
git push -u github master
python setup.py sdist upload
測試專案 (上線階段)
# 測試安裝套件
pip install $ID
# 測試安裝套件後的命令列
$ID
$ID -h
#ID copy -h
$ID copy -i input.json -o output.json
$ID -d copy -i input.json -o output.json
$ID -d -v copy -i input.json -o output.json
$ID -d -vv copy -i input.json -o output.json
$ID -vv copy -i input.json -o output.json
$ID -v copy -i input.json -o output.json
# 測試安裝套件後的命令列
python -m $ID
python -m $ID -h
python -m $ID copy -h
python -m $ID copy -i input.json -o output.json
python -m $ID -d copy -i input.json -o output.json
python -m $ID -d copy -v -i input.json -o output.json
python -m $ID -d copy -vv -i input.json -o output.json
python -m $ID -vv copy -i input.json -o output.json
python -m $ID -v copy -i input.json -o output.json
# 測試安裝套件後的自定函式
from $ID import helloworld
helloworld.show()
目錄結構再版
- Convention 1: 專案根目錄本身是一個 git repository
- 使用 github.com 提供的 Python.gitignore
- 預設的虛擬環境目錄名稱是 venv 或 ENV
- 使用 github.com 提供的 Python.gitignore
- Convention 2: 專案根目錄名稱與套件根目錄名稱一致
- 支援 python -m <套件名稱> 的執行方式
- Convention 3: 專案所依賴的套件列在 requirements.txt 和 requirements-dev.txt
- requirements-dev.txt 有時候也叫做 test-requirements.txt, 例如 OpenStack Horizon 專案
- 使用我整理的樣板檔案 requirements.txt
- 使用我整理的樣板檔案 requirements-dev.txt
- Convention 4: 使用套件 setuptools 的 setup.py 和 setup.cfg 和 MANIFEST.in 和 README.rst 來描述該專案的可散佈套件資訊
- Convention 5: 使用套件 tox 來管理虛擬環境和執行開發任務包含自動測試和自動產生文件
- 自動測試規範目前主流套件是 pep8 (其次是 pylint, flake8)
- 自動測試框架目前主流套件是 pytest (其次是 nose, nose2)
- 自動產生文件目前主流套件是 sphinx
- Convention 6: 依據 pytest 所建議的測試目錄結構
- 建議一 an extra directory outside your actual application code (我選這個)
- 建議二 inlining test directories into your application package
範例
專案根目錄 e.g., pysperm/
# convention 1
工具 git 自動產生的目錄 i.g., .git/
工具 git 的組態檔案 i.e., .gitignore
專案虛擬環境目錄 i.e., venv/
# convention 2
套件根目錄 e.g., pysperm/
python 套件目錄必要檔案 i.e., __init__.py
python 套件目錄必要檔案 i.e., __main__.py # 為了支援 python -m <套件名稱>
# convention 3
專案執行階段所依賴的套件清單 i.e., requirements.txt
專案開發階段所依賴的套件清單 i.e., requirements-dev.txt
# convention 4
可散佈套件資訊描述檔案一 setup.py
可散佈套件資訊描述檔案二 setup.cfg
可散佈套件資訊描述檔案三 (用於 python setup.py sdist) MANIFEST.in
可散佈套件資訊描述檔案四 (被 setup.cfg 引用) README.rst
# convention 5
套件 tox 的組態檔案 i.e., tox.ini
套件 tox 自動產生的目錄 i.e., .tox/
# convention 6
測試套件根目錄 i.e., tests/
python 套件目錄必要檔案 i.e., __init__.py
py.test 第一個測試案例 e.g., test_.py
步驟 (第一開發人員)
mkdir pysperm && cd pysperm
# convention 1
git init
wget https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore -O .gitignore
virtualenv --no-site-packages venv
# convention 2
mkdir pysperm && touch pysperm/__init__.py
wget https://raw.githubusercontent.com/jinsenglin/snippet/master/python/__main__.py -O pysperm/__main__.py
# convention 3
wget https://raw.githubusercontent.com/jinsenglin/snippet/master/python/requirements.txt
wget https://raw.githubusercontent.com/jinsenglin/snippet/master/python/requirements-dev.txt
source venv/bin/activate && pip install -r requirements.txt -r requirements-dev.txt && deactivate
# convention 4
wget https://raw.githubusercontent.com/jinsenglin/snippet/master/python/setup.py
wget https://raw.githubusercontent.com/jinsenglin/snippet/master/python/setup.cfg
touch MANIFEST.in
touch README.rst
# convention 5
# 簡易版 source venv/bin/activate && echo -e '1\npy.test\n\n' | tox-quickstart && deactivate
source venv/bin/activate && echo -e '4\nn\ny\nn\nn\nn\ny\nn\nn\npy.test\n\n' | tox-quickstart && deactivate
echo 'changedir = tests' >> tox.ini
# convention 6
mkdir tests && touch tests/__init__.py
wget https://raw.githubusercontent.com/jinsenglin/snippet/master/python/test_.py -O tests/test_.py
# run test
# 如果 tests/ 目錄內沒有任何測試案例也會引發錯誤
source venv/bin/activate && tox ; deactivate
步驟 (其他開發人員)
git clone <pysperm> && cd pysperm
virtualenv --no-site-packages venv && source venv/bin/activate && pip install -r requirements.txt -r