先日の記事で Python 2.x/3.x の互換性に関するツールをいくつか紹介したけど、詳しい使い方までは書くことができなかった。 今回は、その中のひとつ tox の使い方について紹介してみる。
なんで tox が必要なのか
プロジェクトを複数のバージョンの Python に対応させるには自動化されたテストが必要になる。 本当にそのバージョンでソースコードが動作するかは、実際に動かしてみないと分からないため。
ただ、テストがあったとしても複数のバージョンでそれを実行するのを手動でやっていては手間もかかるしミスも出る。 tox は複数のバージョンで一度にテストを実行してくれるため、その手間とミスを大幅に減らすことができる。
記事の全体の流れ
今回は tox を実際にサンプルのプロジェクトを用意して使ってみることにする。 流れは次の通り。
- pyenv で複数のバージョンの Python をインストールする
- テスト対象と nose で書かれたテストを用意する
- 上記を setuptools でインストールできるパッケージにする
- パッケージを tox を使ってテストする
尚、環境には CentOS7 を使った。
$ cat /etc/redhat-release CentOS Linux release 7.1.1503 (Core) $ uname -r 3.10.0-229.11.1.el7.x86_64
pip をインストールする
まずは、あらかじめ Python のパッケージマネージャ pip をインストールしておく。 同時にパッケージング用のライブラリである setuptools もインストールされる。
$ sudo yum -y install wget $ wget -O - https://bootstrap.pypa.io/get-pip.py | sudo python
pyenv をインストールする
次に、複数のバージョンの Python を管理するのに便利な pyenv をインストールするために Git のリポジトリをチェックアウトする。
$ sudo yum -y install git
$ git clone https://github.com/yyuu/pyenv.git ~/.pyenv
pyenv にパスを通すためにシェルの起動スクリプトに設定を追加する。 bash 以外のシェルを使う場合には、起動スクリプトの名前をそのシェルのものに変える。
$ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bash_profile $ echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bash_profile $ echo 'eval "$(pyenv init -)"' >> ~/.bash_profile
シェルに設定を反映すると pyenv コマンドが使えるようになる。
$ source ~/.bash_profile $ which pyenv ~/.pyenv/bin/pyenv
そして Python のビルドに必要な依存パッケージをインストールしておく。
$ sudo yum -y install zlib-devel bzip2 bzip2-devel readline-devel sqlite sqlite-devel openssl-devel patch
pyenv で Python をインストールする
pyenv install サブコマンドにお目当てのバージョンを指定することで Python をインストールできる。
$ pyenv install 3.4.3
とりあえず今のライブラリなら 3.4 3.3 2.7 2.6 あたりをサポート対象にすれば良いんじゃないかなーと思うのでひと通りインストールしておく。
$ for i in 3.3.6 2.7.10 2.6.9; do pyenv install $i; done;
使用するパッチバージョンを指定するため pyenv の設定ファイルに書き出しておく。
$ cat << EOF > ~/.pyenv/version system 3.4.3 3.3.6 2.7.10 2.6.9 EOF
これでマイナーバージョンまでの指定で特定のバージョンの Python が使えるようになった。
$ python3.4 --version Python 3.4.3 $ for i in 3.3 2.7 2.6; do python${i} --version;done; Python 3.3.6 Python 2.7.5 Python 2.6.9
テスト対象のパッケージを作る
ここからはテスト対象のパッケージを作っていく。 といってもサンプルなのでシンプルに。 まずは Python のパッケージとして認識させるために sample という名前でディレクトリを作った上で __init__.py というファイルを作っておく。
$ mkdir sample $ touch sample/__init__.py
実際に動作するコードとしては calc というモジュールの中に add() という名前でふたつの引数を足し算するだけの関数を作っておく。
$ cat << EOF > sample/calc.py #!/usr/bin/env python # -*- coding: utf-8 -*- def add(a, b): return a + b EOF
テストを作る
次に、先ほど作った calc モジュールに対するテストを作る。 sample パッケージの中に、更に tests パッケージを作った上で test_calc というモジュールを用意する。 今回は nose というサードパーティ製のテスティングフレームワークを使う想定でテストを書いた。
$ mkdir sample/tests $ touch sample/tests/__init__.py $ cat << EOF > sample/tests/test_calc.py # -*- coding: utf-8 -*- import nose from nose.tools import assert_equal from sample.calc import add class Test_sample(object): def test(self): assert_equal(add(1, 1), 2) if __name__ == '__main__': nose.main(argv=['nosetests', '-s', '-v'], defaultTest=__file__) EOF
ここまででディレクトリの構成は次のようになっている。
$ find sample/ sample/ sample/__init__.py sample/calc.py sample/tests sample/tests/__init__.py sample/tests/test_calc.py
nose でテストを実行する
上記で作ったテストを nose で実行する。
まずは pip コマンドで nose をインストールしよう。
$ sudo pip install nose
nose をインストールすると nosetests コマンドが使えるようになる。 これは指定したディレクトリ以下にあるテストを自動的に探してきて実行してくれる便利なやつ。 テストの探し方は test なんちゃらって名前になってるファイル名やクラス名、メソッド名を手がかりにする。
$ nosetests -v sample/ test_calc.Test_sample.test ... ok ---------------------------------------------------------------------- Ran 1 test in 0.002s OK
まずは、システムにインストールされているデフォルトの Python でちゃんとテストがパスすることが確認できた。
パッケージをインストールできるようにする
tox でテストするにはパッケージがインストールできる形式になっている必要がある。 なぜかというと、tox は Python 仮想環境を作った上でそこにテスト対象のパッケージを展開して実行するという動作原理になっているため。
ということでインストールに必要なセットアップスクリプトをささっと書く。 最近の Python パッケージでは、依存しているサードパーティ製のライブラリをテキストファイルに記述しておくのがデファクトスタンダードになりつつあるので、それを読み込むように作った。
$ cat << EOF > setup.py #!/usr/bin/env python # -*- coding: utf-8 -*- from setuptools import setup from setuptools import find_packages def _requirements(): return [ name.rstrip() for name in open('requirements.txt').readlines() ] def _test_requirements(): return [ name.rstrip() for name in open('test-requirements.txt').readlines() ] def main(): setup( name='sample', version='0.0.1', description='Sample package', author='momijiame', author_email='momijiame@example.jp', packages=find_packages(), install_requires=_requirements(), test_require=_test_requirements(), test_suite = 'nose.collector', zip_safe=False, include_package_data=True, ) if __name__ == '__main__': main() EOF
上記で読み込んでいる依存ライブラリを記述したテキストファイルを用意する。 インストールに必要なものはひとつもないけど、テストに必要なものについては先ほどの nose がある。
$ touch requirements.txt $ cat << EOF > test-requirements.txt nose EOF
Python のソースコード以外をパッケージに含める場合は MANIFEST.in というファイルも必要になるので書いておく。
$ cat << EOF > MANIFEST.in include requirements.txt include test-requirements.txt EOF
virtualenv で動作を確認する
作成したパッケージを実際にインストールしてみよう。 とはいえシステムに入れてしまうのはイマイチなので virtualenv を使う。 virtualenv はシステムから独立した Python 実行環境 (Python 仮想環境と呼ばれる) を作ることができるサードパーティ製のライブラリで tox もこれを使っている。
$ sudo pip install virtualenv
Python 仮想環境を作ってアクティベートする。
$ virtualenv /tmp/sample-env New python executable in /tmp/sample-env/bin/python Installing setuptools, pip, wheel...done. $ source /tmp/sample-env/bin/activate
すると、システムにインストールされていたパッケージは見えなくなり、独立した実行環境の中にいることがわかる。
(sample-env)$ pip list pip (7.1.2) setuptools (18.2) wheel (0.24.0)
作成したパッケージをインストールする。
(sample-env)$ python setup.py install (sample-env)$ pip list pip (7.1.2) sample (0.0.1) setuptools (18.2) wheel (0.24.0)
そして add() 関数をワンライナーで使ってみる。
(sample-env)$ python -c "from sample.calc import add; print(add(1, 1))" 2
ばっちり。
ひと通り確認できたら、ひとまず仮想環境から抜ける。
$ deactivate
tox を使って各バージョンでテストを実行する
さて、やっとここからが本題。 tox をインストールする。
$ sudo pip install tox
tox の設定ファイルを用意する。 tox セクションの envlist にはテスト対象のバージョンを羅列する。 testenv セクションの deps には依存ライブラリのインストール方法を指定する。 同じく commands には、準備が整った上で実行したいコマンドを指定する。 ここでは nosetests でテストを実行しているが、例えば pep8 や pylint などを実行してももちろん構わない。
$ cat << EOF > tox.ini [tox] envlist = py26,py27,py33,py34 [testenv] deps = -U -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = nosetests -v EOF
設定ファイルが用意できたら tox コマンドを実行する。 すると各バージョンの Python 仮想環境上にパッケージが展開された上で指定したコマンドが実行される。
$ tox GLOB sdist-make: /home/vagrant/setup.py py26 inst-nodeps: /home/vagrant/.tox/dist/sample-0.0.1.zip py26 installed: argparse==1.3.0,nose==1.3.7,pep8==1.6.2,sample==0.0.1,wheel==0.24.0 py26 runtests: PYTHONHASHSEED='3203418270' py26 runtests: commands[0] | nosetests -v test_calc.Test_sample.test ... ok ---------------------------------------------------------------------- Ran 1 test in 0.003s OK py27 create: /home/vagrant/.tox/py27 py27 installdeps: -U, -r/home/vagrant/requirements.txt, -r/home/vagrant/test-requirements.txt py27 inst: /home/vagrant/.tox/dist/sample-0.0.1.zip py27 installed: nose==1.3.7,sample==0.0.1,wheel==0.24.0 py27 runtests: PYTHONHASHSEED='3203418270' py27 runtests: commands[0] | nosetests -v test_calc.Test_sample.test ... ok ---------------------------------------------------------------------- Ran 1 test in 0.003s OK py33 create: /home/vagrant/.tox/py33 py33 installdeps: -U, -r/home/vagrant/requirements.txt, -r/home/vagrant/test-requirements.txt py33 inst: /home/vagrant/.tox/dist/sample-0.0.1.zip py33 installed: nose==1.3.7,sample==0.0.1,wheel==0.24.0 py33 runtests: PYTHONHASHSEED='3203418270' py33 runtests: commands[0] | nosetests -v test_calc.Test_sample.test ... ok ---------------------------------------------------------------------- Ran 1 test in 0.006s OK py34 create: /home/vagrant/.tox/py34 py34 installdeps: -U, -r/home/vagrant/requirements.txt, -r/home/vagrant/test-requirements.txt py34 inst: /home/vagrant/.tox/dist/sample-0.0.1.zip py34 installed: nose==1.3.7,sample==0.0.1,wheel==0.24.0 py34 runtests: PYTHONHASHSEED='3203418270' py34 runtests: commands[0] | nosetests -v test_calc.Test_sample.test ... ok ---------------------------------------------------------------------- Ran 1 test in 0.004s OK _____________________________________________________ summary _____________________________________________________ py26: commands succeeded py27: commands succeeded py33: commands succeeded py34: commands succeeded congratulations :)
ばっちり。
最終的なディレクトリ構成はこんなかんじ。 (ビルドなどの際に生成されるディレクトリやファイルは除外している)
$ find .
.
./MANIFEST.in
./requirements.txt
./sample
./sample/__init__.py
./sample/calc.py
./sample/tests
./sample/tests/__init__.py
./sample/tests/test_calc.py
./setup.py
./test-requirements.txt
./tox.ini
いじょう。
まとめ
今回は tox を使ってテストを実行するまでの手順を、実際にサンプルとなるパッケージを動作確認を交えつつ作った上で実施してみた。
スマートPythonプログラミング: Pythonのより良い書き方を学ぶ
- 作者: もみじあめ
- 発売日: 2016/03/12
- メディア: Kindle版
- この商品を含むブログを見る