Pythonプロジェクトをオープンソース化する正しい方法
Aug 16, 2013 by Jeff Knupp
ほとんどのPython開発者は、他の人が役に立つと思うようなツール、スクリプト、ライブラリ、フレームワークを少なくとも1つは書いています。この記事での私の目標は、既存のPythonコードをオープンソースで公開するプロセスをできる限り明確にし、痛みを伴わないようにすることです。単に「GitHub リポジトリを作成して git push し、Reddit に投稿して終わり」という意味ではありません。この記事を読み終える頃には、既存のコードベースを、使用と貢献の両方を奨励するオープンソースプロジェクトに変えることができるでしょう。
アップデート1(8月17日: pydanny さんが @audreyr さんの素晴らしいプロジェクトである Cookiecutter の存在を教えてくれたことに感謝します。この記事の最後に、Cookiecutterのセクションを追加しました。Audreyさんの素晴らしいプロジェクトをぜひご覧ください。
アップデート2(8月18日): toxのセクションを提案してくれた@ChristianHeimes(と他の人)に感謝します。ChristianはPEP 440についても思い出させてくれましたし、その他の細かい改善についても素晴らしい提案をしてくれました。
ツールとコンセプト
特に、私が便利だと感じた、あるいは必要だと感じたツールやコンセプトがいくつかあります。以下では、それぞれのトピックについて、実行する必要のある正確なコマンドや設定する必要のある構成値を含めて説明します。目指すのは、すべてのプロセスを明確かつシンプルにすることです。
1. プロジェクトのレイアウト(ディレクトリ構造)
2. setuptoolsとsetup.pyファイル
GitHubの「Issues」で以下のようなことができます。
バグトラッキング
機能リクエスト
予定されている機能
リリース/バージョン管理
プロジェクトのレイアウト
プロジェクトを立ち上げる際には、レイアウト(ディレクトリ構造)が重要になります。適切なレイアウトは、潜在的な貢献者がコードの一部を探すのに時間を費やす必要がないことを意味し、ファイルの位置は直感的にわかります。ここでは既存のプロジェクトを扱っているので、おそらくいくつかのものを移動させる必要があるでしょう。
まずはトップレベルから始めましょう。ほとんどのプロジェクトには、いくつかのトップレベルのファイルがあります(setup.py、README.md、requirements.txt など)。次に、すべてのプロジェクトが持つべき3つのディレクトリがあります。
プロジェクトのドキュメントを格納する docs ディレクトリ
実際のPythonパッケージを格納する、プロジェクト名のディレクトリ
次の2つのうち、どちらかの場所にテストディレクトリを置きます。
テストコードとリソースを含むパッケージディレクトリの下
独立したトップレベルディレクトリとして
ファイルがどのように整理されるべきかを理解するために、私のプロジェクトの1つであるsandmanのレイアウトを簡略化して紹介します。
code: bash
$ pwd
~/code/sandman
$ tree
.
|- LICENSE
|- README.md
|- TODO.md
|- docs
| |-- conf.py
| |-- generated
| |-- index.rst
| |-- installation.rst
| |-- modules.rst
| |-- quickstart.rst
| |-- sandman.rst
|- requirements.txt
|- sandman
| |-- __init__.py
| |-- exception.py
| |-- model.py
| |-- sandman.py
| |-- test
| |-- models.py
| |-- test_sandman.py
|- setup.py
ご覧のように、いくつかのトップレベルのファイル、docsディレクトリ(generatedは空のディレクトリで、sphinxが生成したドキュメントを置く場所)、sandmanディレクトリ、sandmanの下のtestディレクトリがあります。
setuptoolsとsetup.pyファイル
他のパッケージでもよく見かけるsetup.pyファイルは、Pythonパッケージをインストールする際にdistutilsパッケージによって使用されます。バージョニング、パッケージの要件、PyPIで使用されるプロジェクトの説明、あなたの名前や連絡先などの情報が含まれており、どのプロジェクトにとっても重要なファイルです。パッケージをプログラムで検索してインストールすることができ、メタデータとインストールするツールへの指示を提供します。
setuptools パッケージ (実際には distutils の機能拡張セット) は、Python パッケージの構築と配布を簡素化します。setuptools でパッケージ化された Python パッケージは、distutils でパッケージ化されたものと区別がつかないはずです。これを使わない理由はありません。
setup.py はプロジェクトのルートディレクトリに置かれるべきです。setup.pyの最も重要な部分はsetuptools.setupへの呼び出しで、ここにはパッケージに関するすべてのメタ情報が格納されています。以下は、sandmanのsetup.pyの完全な内容です。
code: python
rom __future__ import print_function
from setuptools import setup, find_packages
from setuptools.command.test import test as TestCommand
import io
import codecs
import os
import sys
import sandman
here = os.path.abspath(os.path.dirname(__file__))
def read(*filenames, **kwargs):
encoding = kwargs.get('encoding', 'utf-8')
sep = kwargs.get('sep', '\n')
buf = []
for filename in filenames:
with io.open(filename, encoding=encoding) as f:
buf.append(f.read())
return sep.join(buf)
long_description = read('README.txt', 'CHANGES.txt')
class PyTest(TestCommand):
def finalize_options(self):
TestCommand.finalize_options(self)
self.test_args = []
self.test_suite = True
def run_tests(self):
import pytest
errcode = pytest.main(self.test_args)
sys.exit(errcode)
setup(
name='sandman',
version=sandman.__version__,
license='Apache Software License',
author='Jeff Knupp',
install_requires=['Flask>=0.10.1',
'Flask-SQLAlchemy>=1.0',
'SQLAlchemy==0.8.2',
],
cmdclass={'test': PyTest},
author_email='jeff@jeffknupp.com',
description='Automated REST APIs for existing database-driven systems',
long_description=long_description,
include_package_data=True,
platforms='any',
test_suite='sandman.test.test_sandman',
classifiers = [
'Programming Language :: Python',
'Development Status :: 4 - Beta',
'Natural Language :: English',
'Environment :: Web Environment',
'Intended Audience :: Developers',
'License :: OSI Approved :: Apache Software License',
'Operating System :: OS Independent',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: Software Development :: Libraries :: Application Frameworks',
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
],
extras_require={
}
)
(read()をより慣用的にするという提案をしてくれたChristian Heimesに感謝します。私がこのコードを拝借したプロジェクトにも伝えておきます...)
ほとんどの内容は単純で、setuptoolsのドキュメントから得られるものですので、「興味深い」部分だけに触れます。sandman.__version__とlong_descriptionを取得する方法(どのプロジェクトかは覚えていませんが、他のプロジェクトのsetup.pyから引用しました)を使うことで、書かなければならない定型的なコードの量を減らすことができます。プロジェクトのバージョンを3箇所(setup.py、package.__version__を介したパッケージ自体、ドキュメント)で管理する代わりに、パッケージのバージョンを使用してsetup.pyのバージョンパラメータを設定することができます。
long_description は、PyPI がプロジェクトの PyPI ページの説明文として使用するドキュメントです。README.mdというほぼ同じ内容のファイルがあるので、pandocを使ってREADME.mdからREADME.rstを自動生成しています。したがって、単純にREADME.rstというファイルを読み込んで、それをlong_descriptionの値として使えばいいのです。
pytest(後述)には、python setup.py testが正しく動作するための特別なエントリ(class PyTest)があります。このコードスニペットは pytest のドキュメントから直接引用しています。
このファイルの他の部分は、ドキュメントに書かれているセットアップパラメータの値を設定するだけです。
その他のsetup.pyパラメータ
セットアップの引数の中には、sandmanには必要のないものもありますが、あなたのパッケージでは必要になるかもしれません。例えば、ユーザーがコマンドラインから実行できるようにしたいスクリプトを配布している場合があります。上記の例では、そのスクリプトは他のコードと一緒に通常のsite-packagesの場所にインストールされます。インストールされた後にユーザーが実行する方法はありません。
このような理由から、setup() は scripts 引数を取り、インストールされるべき Python スクリプトを指定することができます。パッケージから go_foo.py というスクリプトをインストールするには、setup() の呼び出しに次の行を含めます。
code: python
ただ、スクリプトの名前だけではなく、相対パスを記述することを忘れないでください。
(例:scripts = ['scripts/foo_scripts/go_foo.py'])また、スクリプトは "python "を含む "shebang"行で始める必要があります。
code: bash
distutils は、インストール時にこの行を現在のインタープリタの場所に自動的に置き換えます。
Gitによるソース管理とGitHubによるプロジェクト管理
「Starting a Django Project The Right Way」では、バージョン管理にはgitかmercurialを使うことを勧めています。共有したり貢献したりすることを前提としたプロジェクトでは、実際には git しか選択肢がありません。実際のところ、git の使用が必要なだけでなく、人々に実際に使ってもらいたい、貢献してもらいたいと思うのであれば、GitHub を使ってプロジェクトを管理する必要があると言ってもいいでしょう。
この言葉は決して扇動的なものではありません (多くの人がこの言葉に異議を唱えるでしょうけれど)。むしろ、良くも悪くも、git と GitHub はオープンソースプロジェクトのデファクトスタンダードとなっています。GitHub は、潜在的な貢献者が最もよく登録し、よく知っているサイトです。この点を軽視してはいけないと私は思います。
README.mdファイルの作成
GitHub上のリポジトリのプロジェクトの説明は、プロジェクトのルートディレクトリにあるファイルから引用されます。README.mdです。このファイルには、次のような情報を含める必要があります。
あなたのプロジェクトの説明
プロジェクトのReadTheDocsページへのリンク
ビルドの状態を示すTravisCIボタン
「クイックスタート」ドキュメント (プロジェクトを素早くインストールして使用する方法)
Python以外の依存関係がある場合は、そのリストとインストール方法
バカバカしいと思うかもしれませんが、これは重要なファイルです。将来のユーザや貢献者があなたのプロジェクトについて最初に読むものになる可能性が高いのです。時間をかけてわかりやすい説明を書き、GFM (GitHubFlavoredMarkdown) を使ってある程度魅力的に見えるようにしましょう。生の Markdown で文書を書くことに抵抗がある場合は、GitHub 上でライブプレビューエディターを使ってこのファイルを作成・編集することができます。
リストの2番目と3番目の項目(ReadTheDocsとTravisCI)はまだカバーしていません。これらについては後述します。
課題ページの使用
人生におけるほとんどのことがそうであるように、GitHub に力を入れれば入れるほど得られるものも大きくなります。どのみち、ユーザーはバグレポートを提出するために GitHub を使うことになるでしょうから、GitHub の 課題(Issues) ページを使って機能要求や拡張機能を追跡することは理にかなっています。
さらに重要なのは、潜在的な貢献者が自分たちが実装しそうなもののリストを見ることができ、プルリクエストのワークフローを適度にエレガントな方法で自動的に管理することができるということです。GitHub の課題とそのコメントは、コミットや自分のプロジェクトの他の課題、他のプロジェクトの課題などと相互にリンクさせることができます。そのため、"Issues" ページはバグフィックスや機能強化、機能リクエストに関連するすべての情報を保管するのに適した場所となります。
課題は常に最新の状態に保ち、新しい課題には少なくとも簡単な回答をタイムリーに行うようにしてください。コントリビューターとしては、バグを修正した後にそれが issues ページでマージされるのを待っているのを見るほどやる気をなくすことはありません。
git-flow を使った賢明な git ワークフロー
自分も貢献者も楽になるために、私は大人気の git-flow モデルを使ったブランチを提案します。
簡単な概要
develop ブランチは、あなたがほとんどの作業を行うブランチであり、次のリリースでデプロイされるコードを表すブランチでもあります。feature ブランチは、まだデプロイされていない自明でない機能や修正を表します (完了した feature ブランチは develop にマージされます)。masterの更新は、リリースの作成を通じて行われます。
インストール方法
お使いのプラットフォームに合わせた手順で、git-flowをインストールします。
インストールが完了したら、次のコマンドで既存のプロジェクトを移行します。
code: bashh
$ git flow init
ブランチの詳細
スクリプトは、いくつかの設定に関する質問をしてきます。git-flow が提案するデフォルト値をそのまま使えばいいでしょう。デフォルトのブランチが develop になっていることにお気づきでしょうか。これについては後ほど説明します。一歩下がって、git-flow・・・いや、フローをもう少し詳しく説明しましょう。そのための最も簡単な方法は、モデル内のさまざまなブランチやブランチの種類について説明することです。
マスター
master は常に「本番用」のコードです。コミットが直接masterに行われることはありません。むしろ、本番用のリリースブランチが作成され、「完成」した後に初めてmasterにコードが置かれます(後ほど詳しく説明します)。このように、master上のコードは常に本番環境にリリースできる状態にあります。また、masterは常に予測可能な状態にあるので、他のブランチが変更していないのにmaster(つまりproduction)が変更されていることを心配する必要はありません。
develpブランチ
ほとんどの作業は develop ブランチで行われます。このブランチには、完成した機能やまだリリースされていないバグフィックスがすべて含まれています。ナイトリービルド(Nightly Build)や継続的インテグレーション(CI: Continuous Integration)サーバーは、次のリリースに含まれるコードを表す develop をターゲットにする必要があります。
featureブランチ
大規模な機能については、feature ブランチを作成します。feature ブランチは develop ブランチから作成されます。featureブランチは、次のリリースに向けた小さな機能強化であったり、今すぐにでも作業を行う必要がある変更であったりします。新しい機能の作業を開始するには、次のようにします。
code: bash
$ git flow feature start <feature name>
これで、新しいブランチ feature/<feature name> が作成されます。このブランチへのコミットは、通常通り行われます。機能が完成して本番環境にリリースする準備ができたら、次のコマンドで develop にマージします。
code: bash
$ git flow feature finish <feature name>
これにより、コードが develop にマージされ、feature/<feature name> ブランチが削除されます。
リリース
本番リリースの準備が整ったら、develop ブランチからリリースブランチを作成します。以下のコマンドで作成します。
code: bash
$ git flow release start <リリース番号>
このとき、初めてリリースのバージョン番号が作成されることに注意してください。完成してリリースできる状態になった機能はすべて、すでにdevelop ブランチにある (つまり feature finish済み) 必要があります。リリースブランチが作成されたら、コードをリリースします。リリース後に必要な小さなバグフィックスは、release/<release number> ブランチに直接行います。落ち着いてきて、もうバグ修正の必要がなさそうになったら、次のコマンドを実行します。
code: bash
$ git flow release finish <リリース番号>
これで、release/<release number> での変更が master と develop の両方のブランチにマージされます。つまり、これらのブランチに本番環境での変更が欠けてしまうことを心配する必要はなくなります (おそらくちょっとしたバグ修正の結果でしょう)。
hotfix ブランチ
hotfixブランチは便利なものですが、実際にはほとんど使われていません。hotfix は master からの機能ブランチのようなものです。すでにリリースブランチを閉じてしまったものの、まだリリースしなければならない重要な変更があることに気づいた場合は、次のようにして master から hotfix ブランチを作成します (タグは $ git flow release finish <リリース番号> で作成したものです)。
code: bash
$ git flow hotfix start <リリース番号>
変更を加えてバージョン番号を変更したら、次のようにして hotfix を確定します。
code: bash
$ git flow hotfix finish <リリース番号>
これは、リリースブランチのように (本質的にはリリースブランチの一種なので)、master と develop の両方に変更をコミットします。
この方法があまり使われないのは、リリース済みのコードを変更するための仕組みがすでに存在するからです。確かに最初のうちは、チームが git flow でリリースを終えるのが早すぎて、翌日になって急な変更が必要になることもあるでしょう。しかし時間が経てば、リリースブランチをオープンしておくのに適した時間がわかってくるので、ホットフィックスブランチが必要になることはありません。Hotfixブランチが必要になるのは、新しい「機能」を開発中の変更を取り込まずに、すぐに運用する必要がある場合だけです。そのようなことは(願わくば)滅多に起こらないことだと思います。
virtualenvとvirtualenvwrapper
Ian Bicking の virtualenv ツールは、Python 環境を分離するためのデファクトスタンダードなメカニズムになっています。その目的は単純です: 1 台のマシン上にいくつかの Python プロジェクトがあり、それぞれが異なる依存関係を持っている場合 (おそらく同じパッケージの異なるバージョンに依存している場合)、1 つの Python インストールで依存関係を管理することは不可能に近いです。
distribute と pip は、pip install がシステムの Python インストールではなく virtualenv にパッケージを正しくインストールするようにインストールされます。virtualenv の間での切り替えは、1 コマンドでできます。
別のツールである Doug Hellmann の virtualenvwrapper は、複数の virtualenv の作成と管理を容易にします。さっそく両方をインストールしてみましょう。
code: bash
$ pip install virtualenvwrapper
...
Successfully installed virtualenvwrapper virtualenv virtualenv-clone stevedore
Cleaning up...\
見ての通り、後者は前者に依存しているので、単にvirtualenvwrapperをインストールするだけで十分です。なお、Python 3を使用している場合は、venvパッケージとpyvenvコマンドによってPythonが仮想環境をネイティブにサポートする PEP-405 がPython 3.3で実装されています。上述のツールではなく、PEP-405を使用してください。 virtualenvwrapperをインストールしたら、.zhsrcファイル(bashユーザーの場合は .bashrcファイル)に一行を追加する必要があります。
code: bash
$ echo "source /usr/local/bin/virtualenvwrapper.sh" >> ~/.zshrc
これにより、シェルにいくつかの便利なコマンドが追加されます(これらを初めて利用できるようにするために、.zshrc をソース化することを忘れないでください)。mkvirtualenv コマンドで直接仮想環境を作成することもできますが、通常は mkproject [OPTIONS] DEST_DIR を使って「プロジェクト」を作成する方が便利です。しかし、今回は既存のプロジェクトがあるので、単純にプロジェクト用の新しい仮想環境を作成します。これには簡単なコマンドが必要です。
code: bash
$ mkvirtualenv ossproject
New python executable in ossproject/bin/python
Installing setuptools............done.
Installing pip...............done.
(ossproject)$
シェルプロンプトの先頭に仮想環境の名前(私は「ossproject」と呼びましたが、もちろん好きな名前を使うことができます)が表示されていることに気づくでしょう。これで、pip install でインストールしたものはすべて、仮想環境の site-packages にインストールされます。
プロジェクトでの作業をやめてシステムインストールに戻るには、deactivate コマンドを使用します。シェルプロンプトの前に付加されていたvirtualenv名が消えるのを確認できます。プロジェクトの作業を再開するには、$ workon <プロジェクト名> を実行すると、仮想環境に戻ることができます。
単にプロジェクト用の仮想環境を作成するだけでなく、もう一つの目的である requirements.txt ファイルの作成にも使用します。pip は requirements ファイルと -r フラグを使用して、プロジェクトのすべての依存関係をインストールすることができます。このファイルを作成するためには、仮想環境内で以下のコマンドを実行します(あなたのコードが仮想環境で動作するようになったら、ですが)。
code: bash
(ossproject)$ pip freeze > requirements.txt
このファイルには、プロジェクトに必要なすべての要件がリストアップされており、後でsetup.pyファイルで依存関係をリストアップする際に使用できます。ここで1つ注意があります。私はよく requirements.txt の '==' を' >=' に変更して、"私が作業しているバージョン以降のこのパッケージのすべてのバージョン "としています。これをすべきかどうかはプロジェクトによって異なりますが、一応指摘しておきます。
requirements.txt を git repo にコミットします。さらに、setup.py の distutils.setup への install_requirements 引数の値として、ここに記載されているパッケージを追加することができます。こうすることで、後でパッケージをPyPIにアップロードしたときに、Pipでインストールできるようになります。自動的に依存関係が解決された状態でPipインストールされるようになります。
pytestによるテスト
Pythonの自動テストのエコシステムでは、Python標準ライブラリのunittestパッケージに代わるものとして、noseとpytestの2つの主要なものがあります。どちらも unittest を拡張して、機能を追加しながら作業を容易にしています。正直なところ、どちらを選んでも構いません。私はいくつかの理由からpytestを好んで使っています。
setuptools/distutilsプロジェクトのサポート
python setup.py testはまだ動作します。
通常の "アサート文のサポート(jUnitスタイルのアサート関数を全て覚える必要はありません)
ボイラープレートの削減
複数のテストスタイルのサポート
unittest
doctest
nose
注意点
既に自動化されたテストソリューションをお持ちの方は、このセクションを飛ばして、そのままお使いください。後のセクションでは、テストが pytest を使用して行われていると仮定しており、それが設定値に影響を与える可能性があることに注意してください。
テストのセットアップ
pytestがテストファイルを見つけるための仕組みは、デフォルトではカレントディレクトリ以下のtest_という接頭辞を持つファイルを探し、それをテストファイルとして扱います。
そのファイルに何を入れるかは、ほとんどあなた次第です。テストの記述は大きなテーマであり、この記事の範囲外です。しかし、重要なことは、テストがあなたとプロジェクトの潜在的な貢献者の両方にとって有用であるということです。各テストがどのような機能を実行しているのかを明確にしておく必要があります。テストは同じ「スタイル」で書かれるべきであり、 潜在的な貢献者が、あなたのプロジェクトで使われている 3 つのテストスタイルのうちどれを使うべきかを推測する必要がないようにします。
テストカバレッジ
自動テストのカバレッジ(coverage)は議論の多いテーマです。ある人は、自動テストカバレッジを、誤った安全性を提供する無意味な指標だと考えています。また、非常に有用であると考える人もいます。少なくとも、すでにテストを持っていてテストカバレッジをチェックしたことがない場合は、練習として今すぐチェックすることをお勧めします。
pytestを使えば、Ned Batchelder氏のcoverageツールを利用することができます。そのためには、 $ pip install pytest-cov でインストールする必要があります。
これまでテストを次のように実行していた場合は、いくつかの追加フラグを渡すことでテストのカバレッジレポートを生成できます。
code: bash
$ pytest
以下はsandmanの実行例です。
code: bash
$ py.test --cov=path/to/package
$ py.test --cov=path/to/package --cov-report=term --cov-report=html
======================================= test session starts ========================================
platform darwin -- Python 2.7.5 -- pytest-2.3.5
plugins: cov
collected 23 items
sandman/test/test_sandman.py .......................
------------------------- coverage: platform darwin, python 2.7.5-final-0 --------------------------
Name Stmts Miss Cover
--------------------------------------------------
sandman/__init__ 5 0 100%
sandman/exception 10 0 100%
sandman/model 48 0 100%
sandman/sandman 142 0 100%
sandman/test/__init__ 0 0 100%
sandman/test/models 29 0 100%
sandman/test/test_sandman 114 0 100%
--------------------------------------------------
TOTAL 348 0 100%
Coverage HTML written to dir htmlcov
==================================== 23 passed in 1.14 seconds =======================================
確かに、私のプロジェクトのすべてがテストカバレッジ100%ではありません(実際、これを読んでいる間に、sandmanはもう100%のカバレッジを持っていないかもしれません)。しかし、100%を達成することは有益なことでした。他の方法では気づかなかったようなバグやリファクタリングの機会を発見することができました。
また、テスト自体については、継続的インテグレーションの一環として、テストカバレッジレポートを自動的に生成することができます。そうすることを選択した場合、現在のテストカバレッジを示すバッジを表示することで、プロジェクトに少しの透明性を加えることができます(そして、高い数値は他の人の貢献を促すこともあります)。
Toxによるテストの標準化
Pythonプロジェクトのメンテナが直面する問題の一つに互換性があります。もしあなたのゴールが Python 2.x と Python 3.x の両方をサポートすることだとしたら (そして、もしあなたが現在 Python 2.x のみをサポートしているのであれば、そうすべきでしょう)、あなたがサポートしていると言っているすべてのバージョンに対して、あなたのプロジェクトが実際に動作することをどのようにして確認するのでしょうか?結局のところ、あなたがテストを実行するとき、あなたはテストを実行するために使用される特定のインタープリタのバージョンをテストしているだけです。あなたが行った変更が、Python 2.7.5では問題なく動作するが、2.6や3.3では壊れてしまうということはよくあることです。
幸運なことに、この問題を解決するためのツールがあります。 toxは「Pythonの標準化されたテスト」を提供し、単に複数のバージョンのインタープリタでテストを実行するだけではありません。toxは、あなたのパッケージとその要件がインストールされ、テストされる完全なサンドボックス化された環境を作ります。もしあなたが行った変更が、直接テストされたときには問題なく動作するが、その変更が不注意にもインストールを壊してしまった場合、toxを使えばそれを発見することができます。
toxの設定は、tox.iniというINI形式ファイルで行います。このファイルの設定は非常にシンプルです。以下は、toxのドキュメントから抜粋した最小限の tox.ini ファイルです。
code: tox.ini
# content of: tox.ini , put in same dir as setup.py
envlist = py26,py27
deps=pytest # install pytest in the venvs
commands=py.test # or 'nosetests' or ...
envlist にpy26 と py27 を設定することで、toxはこれらのバージョンのインタープリタに対してテストを実行すべきだと認識します。toxは異なるバージョンや設定でのテストを可能にします。このような素晴らしいツールを使うためだけに、複数のバージョンをサポートしないのは犯罪です。
deps は、あなたのパッケージの依存関係のリストです。toxには、すべての依存関係を、別のPyPI URLからインストールするように指示することもできます。明らかに、このプロジェクトにはかなりの思考と作業が投入されています。
すべての環境ですべてのテストを実行するには、次の4つのキーストロークが必要です。
code: bash
$ tox
もっと複雑な設定
私の著書である「Writing Idiomatic Python3」は、実際には一連のPythonモジュールとdocstringsとして書かれています。これは、すべてのコードサンプルが意図したとおりに動作することを確認するためです。私はビルドプロセスの一環として、新しいイディオムのコードが正しく動作するかどうかを確認するためにtoxを実行します。また、時々テストカバレッジをチェックして、テスト中に誤ってスキップされたイディオムがないかどうかを確認しています。そのため、私の tox.ini は上記のものよりも少し複雑になっています。見てみてください。 code: tox.ini
envlist=py27, py34
deps=
pytest
coverage
pytest-cov
setenv=
PYTHONWARNINGS=all
adopts=--doctest-modules
python_files=*.py
python_functions=test_
norecursedirs=.tox .git
commands=
py.test --doctest-module
commands=
py.test --doctest-module
basepython=python
commands=
py.test --doctest-module --cov=. --cov-report term
basepython=python3.4
commands=
py.test --doctest-module --cov=. --cov-report term
このコンフィグファイルでさえ、非常に簡単なものです。そして結果は?
code: bash
(idiom)~/c/g/idiom git:master >>> tox
GLOB sdist-make: /home/jeff/code/github_code/idiom/setup.py
py27 inst-nodeps: /home/jeff/code/github_code/idiom/.tox/dist/Writing Idiomatic Python-1.0.zip
py27 runtests: commands0 | py.test --doctest-module /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.
from compiler import parse, ast, pycodegen
========================================= test session starts =======================================
platform linux2 -- Python 2.7.5 -- pytest-2.3.5
plugins: cov
collected 150 items
...
======================================== 150 passed in 0.44 seconds =================================
py33 inst-nodeps: /home/jeff/code/github_code/idiom/.tox/dist/Writing Idiomatic Python-1.0.zip
py33 runtests: commands0 | py.test --doctest-module ======================================== test session starts ========================================
platform linux -- Python 3.3.2 -- pytest-2.3.5
plugins: cov
collected 150 items
...
========================================= 150 passed in 0.62 seconds ================================
_________________________________________ summary ___________________________________________________
py27: commands succeeded
py33: commands succeeded
congratulations :)
(出力から実行されるすべてのテストのリストを切り取っています)。ある環境に対する自分のテストのカバレッジを見たいときは、単純に実行します。
code: bash
$ tox -e py33verbose
----------------------------- coverage: platform linux, python 3.3.2-final-0 ------------------------
Name Stmts Miss Cover
-----------------------------------------------------------------------------------------------------
control_structures_and_functions/a_if_statement/if_statement_multiple_lines 11 0 100%
control_structures_and_functions/a_if_statement/if_statement_repeating_variable_name 10 0 100%
control_structures_and_functions/a_if_statement/make_use_of_pythons_truthiness 20 3 85%
control_structures_and_functions/b_for_loop/enumerate 10 0 100%
control_structures_and_functions/b_for_loop/in_statement 10 0 100%
control_structures_and_functions/b_for_loop/use_else_to_determine_when_break_not_hit 31 0 100%
control_structures_and_functions/functions/2only/2only_use_print_as_function 4 0 100%
control_structures_and_functions/functions/avoid_list_dict_as_default_value 22 0 100%
control_structures_and_functions/functions/use_args_and_kwargs_to_accept_arbitrary_arguments 39 31 21%
control_structures_and_functions/zexceptions/aaa_dont_fear_exceptions 0 0 100%
control_structures_and_functions/zexceptions/aab_eafp 22 2 91%
control_structures_and_functions/zexceptions/avoid_swallowing_exceptions 17 12 29%
general_advice/dont_reinvent_the_wheel/pypi 0 0 100%
general_advice/dont_reinvent_the_wheel/standard_library 0 0 100%
general_advice/modules_of_note/itertools 0 0 100%
general_advice/modules_of_note/working_with_file_paths 39 1 97%
general_advice/testing/choose_a_testing_tool 0 0 100%
general_advice/testing/separate_tests_from_code 0 0 100%
general_advice/testing/unit_test_your_code 1 0 100%
organizing_your_code/aa_formatting/constants 16 0 100%
organizing_your_code/aa_formatting/formatting 0 0 100%
organizing_your_code/aa_formatting/multiple_statements_single_line 17 0 100%
organizing_your_code/documentation/follow_pep257 6 2 67%
organizing_your_code/documentation/use_inline_documentation_sparingly 13 1 92%
organizing_your_code/documentation/what_not_how 24 0 100%
organizing_your_code/imports/arrange_imports_in_a_standard_order 4 0 100%
organizing_your_code/imports/avoid_relative_imports 4 0 100%
organizing_your_code/imports/do_not_import_from_asterisk 4 0 100%
organizing_your_code/modules_and_packages/use_modules_where_other_languages_use_object 0 0 100%
organizing_your_code/scripts/if_name 22 0 100%
organizing_your_code/scripts/return_with_sys_exit 32 2 94%
working_with_data/aa_variables/temporary_variables 12 0 100%
working_with_data/ab_strings/chain_string_functions 10 0 100%
working_with_data/ab_strings/string_join 10 0 100%
working_with_data/ab_strings/use_format_function 18 0 100%
working_with_data/b_lists/2only/2only_prefer_xrange_to_range 14 14 0%
working_with_data/b_lists/3only/3only_unpacking_rest 16 0 100%
working_with_data/b_lists/list_comprehensions 13 0 100%
working_with_data/ca_dictionaries/dict_dispatch 23 0 100%
working_with_data/ca_dictionaries/dict_get_default 10 1 90%
working_with_data/ca_dictionaries/dictionary_comprehensions 21 0 100%
working_with_data/cb_sets/make_use_of_mathematical_set_operations 25 0 100%
working_with_data/cb_sets/set_comprehensions 12 0 100%
working_with_data/cb_sets/use_sets_to_remove_duplicates 34 6 82%
working_with_data/cc_tuples/named_tuples 26 0 100%
working_with_data/cc_tuples/tuple_underscore 15 0 100%
working_with_data/cc_tuples/tuples 12 0 100%
working_with_data/classes/2only/2only_prepend_private_data_with_underscore 43 43 0%
working_with_data/classes/2only/2only_use_str_for_human_readable_class_representation 18 18 0%
working_with_data/classes/3only/3only_prepend_private_data_with_underscore 45 2 96%
working_with_data/classes/3only/3only_use_str_for_human_readable_class_representation 18 0 100%
working_with_data/context_managers/context_managers 16 7 56%
working_with_data/generators/use_generator_expression_for_iteration 16 0 100%
working_with_data/generators/use_generators_to_lazily_load_sequences 44 1 98%
-----------------------------------------------------------------------------------------------------
TOTAL 849 146 83%
====================================== 150 passed in 1.73 seconds ===================================
______________________________________ summary ______________________________________________________
py33verbose: commands succeeded
congratulations :)
それはとても素晴らしいことです。
setuptoolsの統合
tox は setuptools と統合することができ、python setup.py test で tox のテストを実行することができます。次のスニペットは、あなたの setup.py ファイルに記述されるべきもので、tox のドキュメントから直接引用されています。
code: setup.py
from setuptools.command.test import test as TestCommand
import sys
class Tox(TestCommand):
def finalize_options(self):
TestCommand.finalize_options(self)
self.test_args = []
self.test_suite = True
def run_tests(self):
#import here, cause outside the eggs aren't loaded import tox
errcode = tox.cmdline(self.test_args)
sys.exit(errcode)
setup(
cmdclass = {'test': Tox},
)
これで python setup.py test はtoxをダウンロードして実行します。本当にクールです。そしてとても時間の節約になります。
Sphinxによるドキュメント作成
Sphinx は、pocoo の人々によるツールです。これは、Pythonの公式ドキュメントや、他のほとんどの人気Pythonパッケージのドキュメントを生成するために使用されます。Sphinxは、PythonコードからHTMLドキュメントを自動生成することを可能な限り簡単にするというアイデアで書かれています。
ツールに仕事をさせる
SphinxはPythonプログラムや、そこからドキュメントを抽出する方法についての暗黙の知識は持ち合わせていません。SphinxはreStructured Textファイルのみを翻訳することができます。つまり、Sphinxが作業を行うためには、コードのドキュメントのreStructured Textバージョンが利用可能である必要があります。
しかし、すべての.pyファイル(関数やクラスの実際の本体を除いて)のreStructured Textバージョンを維持することは、明らかに不可能です。
訳注: Sphinx は recommonmark エクステンションを追加することで markdown を翻訳することができます。
これには、他のPython パッケージと同様の手順でインストールすることができます: pip install recommonmark
幸運なことに、Sphinxにはjavadocのようなautodocと呼ばれる拡張機能があり、コードのdocstringからreStructured Textを抽出することができます。Sphinxとautodocの力を十分に活用するためには、docstringを特定の方法でフォーマットする必要があります。特に、SphinxのPythonディレクティブを利用すると良いでしょう。ここでは、reStructured Textディレクティブを使用してドキュメント化された関数の例を紹介します。これによって、結果的にHTMLドキュメントがより美しくなります。
code: python
def _validate(cls, method, resource=None):
"""Return True if the the given *cls* supports the HTTP *method* found
on the incoming HTTP request.
:param cls: class associated with the request's endpoint
:type cls: :class:sandman.model.Model instance
:param string method: HTTP method of incoming request
:param resource: *cls* instance associated with the request
:type resource: :class:sandman.model.Model or None
:rtype: bool
"""
if not method in cls.__methods__:
return False
class_validator_name = 'validate_' + method
if hasattr(cls, class_validator_name):
class_validator = getattr(cls, class_validator_name)
return class_validator(resource)
return True
ドキュメント作成には多少の手間がかかりますが、その分、ユーザーにとっては価値のあるものになります。優れたアクセス可能なドキュメントは、使い勝手の良いプロジェクトと不満の多いプロジェクトを分けます。
Sphinxのautodocエクステンションは、docstringから自動的にドキュメントを生成する多くのディレクティブにアクセスすることができます。
インストール方法
ドキュメントはプロジェクトの中でバージョン管理された成果物になるので、Sphinxを仮想環境にインストールすることを忘れないでください。Sphinxのバージョンが異なると、異なるHTMLを生成する可能性があります。virtualenvにインストールすることで、ドキュメントを制御された方法で「アップグレード」することができます。
私たちのドキュメントはdocsディレクトリに、生成されたドキュメントは docs/generated ディレクトリに置いておきます。docstringsからreStructured Textのドキュメントファイルを自動生成するには、プロジェクトのルートディレクトリで以下のコマンドを実行します。
code: bash
$ sphinx-apidoc -F -o docs <package name>
これにより、多くのドキュメントファイルを含むdocsディレクトリが作成されます。さらに、ドキュメントの設定を行う conf.py ファイルも作成されます。また、Makefileも作成されます。これは、HTMLドキュメントを1つのコマンドで作成するのに便利なファイルです(コマンド:$ make html)。
ドキュメントを実際に作成する前に、パッケージがローカルにインストールされていることを確認してください。($ python setup.py develop が最新の状態に保つ最も簡単な方法ですが、pipを使用することもできます。) さもなければ、sphinx-apidocはあなたのパッケージを見つけることができません。
設定: conf.py
作成された conf.py ファイルは、生成されるドキュメントの多くの側面をコントロールしています。 conf.py 自体にも十分なドキュメントが用意されていますので、ここでは2つの項目について簡単に触れてみたいと思います。
バージョンとリリース
まず、バージョンとリリースの値を最新のものにしてください。これらの数値は、生成されたドキュメントの一部として表示されますので、実際の値とずれてしまうことは避けたいものです。
ドキュメントと setup.py ファイルの両方でバージョンを最新に保つ最も簡単な方法は、パッケージの__version__属性から読み取ることです。以下のsandman用の conf.py コードは Flaskの conf.py から 拝借 しました。
code: conf.py
import pkg_resources
try:
release = pkg_resources.get_distribution('sandman').version
except pkg_resources.DistributionNotFound:
print 'To build the documentation, The distribution information of sandman'
print 'Has to be available. Either install the package into your'
print 'development environment or run "setup.py develop" to setup the'
print 'metadata. A virtualenv is recommended!'
sys.exit(1)
del pkg_resources
version = '.'.join(release.split('.'):2) つまり、ドキュメントが正しいバージョン番号を生成するためには、プロジェクトの virtualenv で $ python setup.py develop を実行するだけでよいのです。これで、setup.py が同様に使用するので、__version__を最新の状態に保つことだけを心配すればよいことになります。
html_theme
html_theme をデフォルトから変更してみましょう。私は自然が好きなのですが、これは明らかに個人的な好みの問題です。私がこの点を指摘する理由は、Pythonの公式ドキュメントがPython 2とPython 3の間でテーマをdefaultからpydocthemeに変更したからです(後者のテーマはcPythonソースでのみ利用可能なカスタムテーマです)。人によっては、デフォルトのテーマを見るとプロジェクトが「古い」と感じてしまうのです。
PyPI
PyPI(the Python Package Index) http://pypi.python.org/pypi (以前は "the Cheeseshop" として知られていました) は、一般に公開されている Python パッケージの中央データベースです。PyPI はあなたのプロジェクトのリリースが "生きている "場所です。あなたのパッケージ(とそれに付随するメタデータ)がPyPIにアップロードされると、他の人はpipやeasy_installを使ってそれをダウンロードしてインストールすることができます。繰り返しになりますが、たとえあなたのプロジェクトがGitHubで公開されていたとしても、PyPIにリリースがアップロードされるまでは、あなたのプロジェクトは有用ではありません。もちろん、あなたの git リポジトリをクローンして手動でインストールすることもできますが、pip でインストールしたいと考える人のほうが圧倒的に多いのです。 最後のステップ
前のセクションのすべてのステップを完了したのなら、あなたはパッケージをバンドルしてPyPIにアップロードし、それを世界に公開したいと思っていることでしょう。
しかし、そうする前に、パッケージを配布する前の最後のステップとして、cheesecakeという便利なツールを実行することができます。このツールは、パッケージを分析し、いくつかのカテゴリーで「スコア」を割り当てます。パッケージのパッケージングやインストールがどれだけ簡単で正しいか、コードの質、ドキュメントの質と量を測定します。
準備状況」の粗い尺度として、cheesecakeは健全性のチェックに最適です。setup.py ファイルに問題がないか、ファイルをドキュメント化するのを忘れていないかをすぐに確認することができます。最初のアップロードだけでなく、PyPIへの各アップロードの前に実行することをお勧めします。
初回アップロード
あなたのコードがゴミではなく、人々がインストールしようとしても壊れないことを確認したので、パッケージをPyPIにアップロードしましょう! PyPIとのやりとりは、setuptoolsとsetup.pyスクリプトで行います。もしこのパッケージが初めてPyPIにアップロードされるのであれば、まず登録する必要があります。
code: bash
$ python setup.py register
注意: まだ無料のPyPIアカウントを持っていない場合は、パッケージを登録するために今すぐアカウントを作成する必要があります。登録画面の指示に従った後、配布可能なパッケージを作成してPyPIにアップロードする準備ができました。
code: bash
$ python setup.py sdist upload
上記のコマンドは、ソースディストリビューション(sdist)をビルドし、PyPIにアップロードします。パッケージが純粋なPythonではない場合(つまり、ビルドが必要なバイナリがある場合)、バイナリの配布を行う必要があります。詳しくは setuptools のドキュメントを参照してください。
リリースとバージョン番号
PyPI はリリースバージョンモデルを使用して、どのバージョンのパッケージをデフォルトで利用するかを決定します。最初にアップロードした後、更新したパッケージをPyPIで利用できるようにするためには、毎回新しいバージョン番号のリリースを作成する必要があります。バージョン番号の管理は、実際にはかなり複雑なトピックであり、そのためのPEPがあるほどです。PEP 440 -- Version Identification and Dependency Specification. もちろんPEP 400のガイドラインに従うことをお勧めしますが、もし別のバージョン管理方式を使用する場合は、setup.pyで使用されるバージョンが、現在PyPIにあるものよりも「高い」バージョンでなければ、PyPIはパッケージを新しいバージョンとみなしません。 ワークフロー
最初のリリースを PyPI にアップロードした後の基本的なワークフローは以下の通りです。
パッケージに何らかの作業を行う (バグの修正、機能の追加など)
テストの合格を確認する
git-flow でリリースブランチを作成し、コードを "凍結(Freeze)" します。
パッケージの __init__.py ファイルの__version__の番号を更新する
python setup.py sdist upload を実行して、新しいバージョンのパッケージを PyPI にアップロードします。
ユーザーは、あなたがバグフィックスを出すために頻繁にリリースすることを期待しています。バージョン番号を適切に管理している限り、「頻繁にリリースしすぎる」ということはありません。覚えておいてほしいのは、ユーザーはインストールしたすべてのPythonパッケージの異なるバージョンを手動で管理しているわけではないということです。
TravisCIによる継続的インテグレーション
継続的インテグレーションとは、(定期的な一括更新ではなく)プロジェクトのすべての変更を継続的に統合するプロセスのことです。ここでは、GitHub にコミットするたびにテストを実行して、そのコミットが何かを壊していないかどうかを確認することを意味します。ご想像のとおり、これは非常に価値のある行為です。コミットやプッシュの前に「テストを実行するのを忘れる」ということはもうありません。テストが壊れるようなコミットをプッシュした場合は、その旨のメールが届きます。
TravisCI は、GitHub プロジェクトの継続的インテグレーションを恥ずかしいほど簡単にしてくれるサービスです。まだアカウントをお持ちでない方は、TravisCI にアクセスしてアカウントを作成しましょう。アカウントを作成したら、あとは簡単なファイルを作成してCIの世界に入りましょう。 .travis.ymlによる設定
TravisCI上の個々のプロジェクトの設定は、プロジェクトのルートディレクトリにある .travis.yml というファイルで行います。簡単に言うと、Travisに次のことを伝える必要があります。
プロジェクトがどの言語で書かれているか
その言語のどのバージョンを使用するか
プロジェクトのインストールに使用するコマンドは何か
プロジェクトのテストを実行するために使用するコマンド
これはとても簡単なことです。以下は、sandmanの .travis.yml ファイルの内容です。
code: .travis.yml
language: python
python:
- "2.7"
install:
- "pip install -r requirements.txt --use-mirrors"
- "pip install coverage"
- "pip install coveralls"
script:
- "coverage run --source=sandman setup.py test"
after_success:
coveralls
言語とバージョンをリストアップした後、Travisにパッケージのインストール方法を伝えます。install: の下に、次の行があることを確認します。
code: .travis.yml 抜粋
- "pip install -r requirements.txt --use-mirrors"
この pip は私たちのプロジェクトの必要なものをインストールします(必要に応じて PyPI ミラーを使用します)。インストールの他の2行はsandmanに特有のものです。テストケースのカバレッジを継続的に監視するために追加のサービス(coveralls.io)を使用していますが、すべてのプロジェクトで必要なわけではありません。
script: プロジェクトのテストを実行するのに必要なコマンドを列挙します。ここでもsandmanが余計なことをしてくれています。あなたのプロジェクトに必要なのは python setup.py test だけです。また、after_success の部分はすべて削除しても構いません。
このファイルをコミットし、TravisCI でプロジェクトのリポジトリを有効にしたら、GitHub にプッシュします。しばらくすると、TravisCI 上で最新のコミットに基づいてビルドが開始されるのがわかります。すべてが成功すれば、ビルドは「グリーン」になり、ステータスページにはビルドが成功したことが表示されます。プロジェクトのすべてのビルドの履歴をいつでも見ることができるようになります。これは、複数の開発者がいるプロジェクトでは特に便利で、履歴ページを使って、特定の開発者がどれだけ頻繁にビルドを壊しているかを知ることができます...
また、ビルドが成功したことを知らせるメールが届くはずです。そうしないと、ビルドが壊れたり修正されたりしたときにのみメールが送られてきますが、コミットの結果がその前のビルドと同じになったときには送られてきません。これは非常に便利で、"the build passed!"(ビルドが成功しました!)という無駄なメールが氾濫することはありませんが、何か変化があったときにはアラートが表示されます。
ドキュメントの継続的な統合のための ReadTheDocs
PyPI には公式のドキュメントサイト (pythonhosted.org) がありますが、ReadTheDocs はより良い体験を提供します。なぜか?ReadTheDocsはGitHubとの統合に優れているからです。ReadTheDocsに登録すると、あなたのGitHubリポジトリがすべて表示されます。適切なレポを選択し、いくつかのマイナーな設定を行えば、GitHubにコミットするたびにドキュメントが自動的に再生成されます。 プロジェクトの設定は簡単にできます。しかし、いくつか覚えておくべきことがあります。ここでは、設定項目とその値の一覧を示します。これらは、すぐにはわからないかもしれません。
デフォルトのブランチ:develop
デフォルトのバージョン: latest
Python設定ファイル。(空欄のまま)
virtualenvを使う。(チェック)
要件ファイル: requirements.txt
ドキュメントの種類 Sphinx HTML
DRY(Don't Repeat Yourself) / 自分自身で繰り返さない
せっかく既存のコードベースをオープンソース化したのに、新しいプロジェクトを始めるときに、そのすべてを繰り返さなければならないのは嫌でしょう。幸いなことに、その必要はありません。Audrey Roy氏の Cookiecutter ツールです(ここではPythonバージョンにリンクしていますが、メインレポ には様々な言語のバージョンがあります)。 Cookiecutterは、プロジェクトを開始するプロセスを自動化するコマンドラインツールで、この記事で取り上げたことを簡単に行うことができます。Daniel Greenfeld(@pydanny)が、このツールとこの記事で紹介した内容との関連性について、素晴らしいブログ記事を書いています。ぜひチェックしてみてください。Cookiecutter: Project Templates Made Easy.
まとめ
ここまでで、既存のPythonパッケージをオープンソース化するためのコマンド、ツール、サービスの全てを網羅しました。確かに、GitHubに放り込んで「自分でインストールしてください」と言うこともできたでしょうが、誰もそうしなかったでしょう。また、オープンソースソフトウェアではなく、単に「フリーコード」になってしまいます。
さらに、自分のプロジェクトに外部からの貢献者を集めることもできなかったでしょう。ここで説明した方法であなたのプロジェクトをセットアップすることで、あなたは使用と貢献の両方を奨励する簡単にメンテナンスできるPythonパッケージを作成しました。そしてそれこそが、オープンソースソフトウェアの真の精神ではないでしょうか。