Python プロジェクトを構成して CI 連携 & PyPI 登録

Python 絡みのドキュメントは個人的に割とハードに感じることが多く、プロジェクト構成を作るところからよく理解できていなかった。

そのため、Python で何か作ろうと思って少し調べてみても、以前はこんな感じだった。

  • requirements.txt に依存モジュール書いても自動的にインストールされないし何なの……
  • 開発中のパッケージの動作確認時、ローカル環境への再インストールが面倒すぎる
  • 動作用の依存パッケージと開発用の依存パッケージの管理を分けたい

今回少し試行錯誤をしてみて、上記の謎を解いて CI 連携も行って、まあまあ満足できる構成ができた。 あまり公式的な作法に則っていないかもしれないがメモしておく。

以下では、基本的なファイルレイアウトから、開発中パッケージのローカル環境へのインストール方法、依存ライブラリの管理方法、PyPI への登録方法について説明する。 なお OSS として PyPI に公開しない場合であっても、同じ作法でプライベートな VCS 上にリポジトリを作っておけば pip でその URL を指定してインストールできるようになるので、インストール時の依存パッケージ管理を楽にする意味でもこのような構成で作っておくといい。

説明に使うリポジトリhttps://github.com/keik/subarg にある。

基本のファイルレイアウト

依存パッケージがない場合、PyPI に準拠した基本的なファイルレイアウトは以下のような感じ。

.
├── setup.py
├── subarg
│   └── __init__.py
└── tests
    └── test_subarg.py

これに、以降で説明するもろもろを追加していく。

依存パッケージがない場合の setup.py

PyPI にパッケージを登録するためのスクリプトsetup.py というファイル名で作成する。

Bundler における Gemfile や npm における package.json のように専用のフォーマットで情報を列挙する方式と違って、setup.py ファイルは setuptools.setup 関数を使って以下のように Python で実装する。

from setuptools import setup, find_packages

setup(name='subarg',
      version='1.0.0',
      description="""Parse sub-arguments in `[` and `]` recursively""",
      author='keik',
      author_email='k4t0.kei@gmail.com',
      url='https://github.com/keik/subarg',
      license='MIT',
      classifiers=[
          'License :: OSI Approved :: MIT License',
          'Programming Language :: Python',
          'Programming Language :: Python :: 2',
          'Programming Language :: Python :: 2.7',
          'Programming Language :: Python :: 3',
          'Programming Language :: Python :: 3.5',
      ],
      packages=find_packages())

setuptools.setup 関数の classifiers 引数に指定する値は https://pypi.python.org/pypi?%3Aaction=list_classifiers から選択して列挙する。

ただしこの classifiers 引数、かなりモヤモヤ感がある。指定する値のリストにある Development Status :: 4 - Beta といった値は version 引数のサフィックスでも表現できるし、License :: OSI Approved :: MIT License といった値は license 引数でも表現できるし、Programming Language :: Python といった値は「そりゃそうだろ」感しかない。

とはいえ PyPI にはここに書かれたラベル情報が登録されるし、バッジを生成する shields.io もここの情報を参照するものがあるので、面倒でも上記の例くらいは書いておいたほうがよさそう。

開発時のローカル環境へのインストール

開発中は実際に動作させながらソースを編集していきたいが、ソース変更の度にパッケージを再インストールするのは面倒。

そこで、pip で setup.py があるディレクトリを --editable オプション指定でインストールする (下の例は . でカレントを指定)。

pip install --editable .

すると /Library/Python/2.7/site-packages などの pip 管理下の場所に実際のリソースがコピーされることなく、そのかわりに開発中パッケージの場所情報を持つ subarg.egg-link といったファイルが作られる。開発中パッケージのソースを書き換えても再インストールする必要はない。

動作に依存するパッケージを管理する

setuptools.setupinstall_requires 引数に依存するパッケージ名を文字列のリストで指定する。

install_requires = [
    'numpy',
    'matplotlib'
]

setup(name='subarg',
    ...,
    install_requires=install_requires)

これで pip でパッケージをインストールする際に自動的に依存ライブラリもインストールされるようになる。

パッケージ名を列挙した requirements.txt というファイル (Requirements ファイル) を見たことあるかもしれないが、このファイル単独ではこのような自動的な依存管理システムに作用することはない。このファイルは、pip install -r requirements.txt で依存パッケージをバッチでインストールするために使用されるファイルに対して慣例的につけられる名前というだけであって、予約されたファイル名ではない。多分。

開発時に必要なパッケージを管理する

pytest など開発時のテストにのみ必要なライブラリは、配布したパッケージのインストール時についてきてほしくない。

setuptools.setup 関数にもテスト関連の tests_require 引数とかなんとかがあるが、テスティングフレームワークとの統合がめんどくさそうだったりオプション渡すのも工夫がいるよう なので、もはや setuptools のシステムを使わないで済ませたほうが楽。

そこで Makefile および上述した Requirements ファイルの出番となる。

requirements-dev.txt には開発時に必要なパッケージを列挙する。

flake8
pytest
pytest-cov

これを用いて Makefile にテストの手続きを定義する。

SELF="subarg"
DEV_DEPS="requirements-dev.txt"

test: init
    py.test tests --cov subarg --verbose

init: uninstall
    pip install --upgrade -r $(DEV_DEPS)
    pip install --upgrade --editable .

uninstall:
    - pip uninstall --yes -r $(DEV_DEPS) 2>/dev/null

これで make test でテスト時に必要な依存ライブラリがインストールされてテストが実行できる。

Python のエコシステムを脱して Makefile なの? と思うかもしれないが httpie で採っている方法 でもあるので、悪くない方法なのだと思う。

Travis CI と Coveralls 連携

あらかじめ Travis CI と Coveralls のウェブサイトからリポジトリを有効化しておく。あとは .travis.yml を用意してリポジトリにプッシュ。

language: python
python:
  - 2.7
  - 3.5
script:
  - make
after_success:
  - pip install python-coveralls && coveralls

Coveralls でカバレッジを計測するため、after_success で Coveralls 用のパッケージ coveralls のインストールと実行をする。 これでテスト時に生成されたカバレッジファイルが自動的に Coveralls に送信され、測定結果の記録やバッジ機能が有効になる。

PyPI への登録

パッケージを PyPI に登録する際は以下のコマンドを実行する。

python setup.py register
python setup.py sdist upload

事前に PyPI へのアカウント登録が必要となる。上の register 時にログイン情報が求められる。

README.md にバッジを貼る

一番楽しい時間。Coveralls でカバレッジを記録したし PyPI にも登録したら、あとはバッジを貼ってちゃんとしている感を演出する。

shields.io には PyPI 登録情報に基づくバッジがある。またフラットデザインのバッジを生成できるので、バッジのデザインを統一したいなら shields.io のバッジで統一して使うといい。