箱が…

Amazon箱ストラクチャーが崩れてきそうです。ダンボー作ろうかな。

Ginkgoでテストを書く

みんな大好きgeventをベースにした、daemonを書くためのフレームワークGinkgo(repo)でテストを書く方法について。

Ginkgoについての軽い説明

0.5.0devのドキュメントを読めばわかるんですが、Ginkgo(ギンコ?)は纏まった1つの機能を Service と呼び、Service の中には複数の子Serviceを持てるようになっています。
Service はこんな感じで書きます。

# 1秒毎にコンソールに Hello World と表示し続けるサービス
# http://ginkgo.readthedocs.org/en/latest/user/quickstart.html#hello-world-service からのコピペです
from ginkgo import Service

class HelloWorld(Service):
    def do_start(self):
        self.spawn(self.hello_forever)

    def hello_forever(self):
        while True:
            print "Hello World"
            self.async.sleep(1)

こいつを実行するには、Ginkgoをインストールすると生成されるコマンド ginkgo または ginkgoctl を使います。(上記のコードを hello.py とする)

$ ginkgo hello.HelloWorld

設定ファイル

daemonを作るフレームワークなので、設定ファイルを扱う仕組みが既にあります。
詳しくはドキュメントを読んでいただくとして、下記のコードをみてください。

# service.py
from ginkgo import Service, Setting

class HelloWorld(Service):
    message = Setting("message", default="Hello World",
        help="Message to print out while running")

    def do_start(self):
        self.spawn(self.message_forever)

    def message_forever(self):
        while True:
            print self.get_message()
            self.async.sleep(1)

    def get_message(self):
        return self.message + '!'
# 設定ファイル(といいつつ普通のPythonコード)
# service.conf.py
message = "Services all the way down."
service = "service.HelloWorld"

以下のコマンドで実行

$ ginkgoctl service.conf.py start

上記のコード中の Setting は(ドキュメントに書かれていませんが)シングルトンになっていて、Service が複数あったとしても Setting の第1引数が同じであれば全ての Service (子Serviceも含む)から設定値が参照できます。

で、実は Setting は ginkgo.settings から設定値を取ってくるためのラッパーなので、設定ファイルを介さずに設定値を弄りたいときは ginkgo.settings をつつけばどうとでもなるというわけです。

ここ重要なんで覚えといてください。

テストを書く

ginkgo.settings を直接弄ると「設定ファイルから設定値を読む」処理の下準備をしなくて済むのでとても楽です。

# service_test.py
from unittest import TestCase
from ginkgo import settings

from service import HelloWorld

class HelloWorldTest(TestCase):
    def tearDown(self):
        # .load() で設定値をデフォルトで上書き
        # 与えられた辞書のkeyをもとに settings を更新するので
        # 空の辞書を与えてまっさらに、とはいかない
        settings.load({'message': "Hello World"})

    def test_message(self):
        # 設定値をセット
        settings.load({'message': 'spam'})
        service = HelloWorld()
        assert service.get_message() == 'spam!'

こんな感じで。

Buildoutを使う 設定ファイル編

前回のセットアップ編ではBuildoutをインストールし buildout コマンドで環境を自動構築するところまで書きました。
ここまで読めば、誰かが書いたBuildoutの設定ファイル(buildout.cfg)を貰ってきて(Zopeとかで)Webアプリケーションの開発ができるかもしれません。

今回は buildout.cfg についての説明と、Buildout環境下でのパッケージの開発について書きます。

前回出てきた buildout.cfg の解説

まずは、セットアップ編の最後に出てきた buildout.cfg の解説をします。

buildout.cfg の構造は前回書きましたが、軽く説明しておきます。
[](大括弧)でくくられた部分が「セクション」の始まりで、セクションは次のセクションが始まるまで続きます。
セクション内には「key = value」の形で値を設定します。また、keyの種類によっては値を改行(またはスペース)区切りで列挙できるものと、1つしか設定できないものがあります。

さて、ここからが今回の内容です。

コメント

多くの設定ファイルと同じように、buildout.cfg にもコメントが書けます。

# 前回貼った buildout.cfg
[buildout]
parts =
    codehl

# このセクションは今回は使わないので後で消えます
[codehl]
recipe = zc.recipe.egg
eggs = 
    pygments

「#」の後がコメントになります。
ただし「#」は行頭に書かないといけないようで、例えば以下のように書くと

[codehl]
recipe = zc.recipe.egg # download and install eggs
eggs = 
    pygments

Buildoutは「zc.recipe.egg # download and install eggs」という名前のEggをPyPIに探しに行こうとするのでアウトです。

buildoutセクション

前回でも少し触れましたが buildout.cfg において「buildoutセクション」は特別で、Buildout全体の設定を記述するためのセクションです。
サンプルに書かれた「parts」というkey(ラベル?)はbuildoutセクションにのみ存在する設定項目で、Buildoutはここに列挙されたセクションだけを実行対象(Buildoutによる環境構築に参加できる)とします。
言い換えれば、たとえ buildout.cfg にセクションが記述されていても、partsに列挙されていなければBuildoutには見つけてもらえないということです。

[buildout]
parts =
    codehl

parts = の後に改行とスペースによるインデントが入っていますが、これは単に見やすいからそうしているだけです。
parts = の後にそのままセクション名を書いても何ら問題はありません。

# 上記と意味は同じ
[buildout]
parts = codehl

なお parts はbuildoutセクションに必須です。

レシピ

Buildoutにおけるレシピとは、セクションの「eggs」に書かれたEggをどう料理するのかを指示するMakefileみたいなものです。
レシピの実体はBuildout専用のPythonのパッケージで、buildout.cfg にレシピの識別子を書いておけば Buildout がPyPIからレシピを自動でダウンロード・インストールし、その指示に従ってセクションをいい感じに料理してくれるというわけです。

レシピは以下のサンプルのように、「recipe = [レシピの識別子]」の形で記述します。

# buildout セクションは略
[the-sample]
recipe = zc.recipe.egg
eggs =
    pyyaml
    pytz

zc.recipe.egg」はPythonのパッケージ(配布物)をダウンロード・インストールするレシピです。できることはそれだけではないんですが、今はこの理解でいいと思います。
このセクションをpartsに追加してbuildoutコマンドを実行すると、zc.recipe.egg によってpyyaml(YAMLを扱うライブラリ)とpytz(Pythonの超煩雑なタイムゾーン定義を代わりに提供してくれる素晴らしいライブラリ)がBuildout環境にインストールされます。

レシピは各セクションに1つ、必ず設定してください。書き忘れると Error: Missing option: the-sample:recipe などと言われてBuildoutが止まります。

recipe には値を1つだけ設定できます。

まだ出てきていない buildout.cfg の要素

ここからが本番です。

開発中のEgg

Buildoutは、開発中でまだ .egg として固めていないパッケージであっても扱うことができます。
それには、Egg基礎編の最後に書いたEggのdevelop installの仕組みを使います。
setup.py を使ってPythonのsite-packagesディレクトリにdevelop installするときは以下のようにしましたが

$ python setup.py develop

Buildoutで(意味的に)同じことをするには、buildout.cfg にこのように書きます。

[buildout]
parts = 
develop = /home/stackbox/code/python/deepblue/deepblue_sources

特に何もしていないので実用性はありませんが、簡単のためにこれで勘弁してください。
上記の例では、ディレクトリ・ファイル構成はこうなっています。

(以下は /home/stackbox/code/python/deepblue/ から見た構成)
+ buildout
    + bin
        - buildout
    - buildout.cfg
    + (多いので他のディレクトリは略)
+ deepblue_sources
    - setup.py
    + deepblue
        - __init__.py
        - answer.py

Buildout環境に開発中のEggをdevelop installするには、buildoutセクションの「develop」に開発中のEggの入っているディレクトリ(直下にsetup.pyを含む)へのパスを指定します。
これによって、普通のセクションの「eggs」に、このパッケージの識別子(setup.pyで定義した)である「deepblue」を指定できるようになりました。

develop には値が複数列挙できるので、開発中のEggが複数必要なときにはそれぞれのディレクトリを指定することになるでしょう。

上記の例ではdevelopをフルパスで指定しましたが、相対パスでも問題はないようです。
ただ、どのディレクトリを基準とした相対パスなのかは私は知りません。

他のセクションの値の参照

実は、partsに列挙しないセクションを buildout.cfg に書く意味はちゃんとあります。
以下の buildout.cfg を見てください。

[buildout]
parts =
    twitter-bot
    irc-bot
develop-basedir = /home/stackbox/code/python/bots
develop = 
    ${buildout:develop-basedir}/twitter
    ${buildout:develop-basedir}/irc

[libs]
recipe = zc.recipe.egg
eggs =
    pyyaml
    pytranslate

[twitter-bot]
recipe = zc.recipe.egg
eggs =
    ${libs:eggs}
    my.twitterbot

[irc-bot]
recipe = zc.recipe.egg
eggs =
    ${libs:eggs}
    my.ircbot

えーと、まず my.twitterbot と my.ircbot は私のでっち上げなのでこの buildout.cfg は嘘っぱちです。
ですが、ちゃんと似たような用途のBOTを2つ作るつもりで考えてみました。以下に解説を書きます。

まず、my.twitterbotとmy.ircbotは開発中のEggです。そのためbuildoutセクションのdevelopに、それぞれのパッケージの入っているディレクトリを指定しています。
その際に、私が勝手に作った「develop-basedir」にパスの共通部分を書いて、「develop」から「develop-basedir」を参照し、2つのBOTのパスを記述しました。値の参照は「${セクション名:参照したいkey名}」と書きます。

2つのBOTは「TwitterまたはIRC(の特定のチャンネル)を監視して『pytranslate』で日本語に翻訳し『pyyaml』でYAMLに出力する」という想定なので、共通するライブラリをlibsセクションに記述して、それぞれのBOTのセクション(partsに列挙されている)から参照しています。

自信満々な感じで書きましたが、libsセクションにまとめるところは「一応こんな使い方もある」という程度に覚えておいて頂ければと思います。
Buildoutの想定外の使い方かもしれないですからね!

解説の途中にさらっと書いてしまいましたが、あるセクション内の値を参照したいときは

foobar = ${セクション名:参照したいkey名}

というフォーマットで記述します。

buildoutセクションの特別な値

buildoutセクションには、参照および値の変更が可能な隠された(?)key名があります。
なんだか公式ドキュメントにもちゃんと載っていないようですし、Undocumentedなんだと思いますが、多用している buildout.cfg を見たことがあるのでここに載せてしまいます。

参照方法 内容の説明
${buildout:directory} buildout.cfgのあるディレクトリ(※1)
${buildout:bin-directory} bin ディレクトリ(コマンド等が生成されるところ)
${buildout:parts-directory} parts ディレクトリ(partsに列挙された各セクションのレシピが使う)
${buildout:eggs-directory} eggs ディレクトリ(DLされたEggがキャッシュされるところ)
${buildout:develop-eggs-directory} develop-eggs ディレクトリ(開発中のEggへのリンクを入れるところ)

※1:Buildoutのコードを読んだところ、buildout.cfgが読み込まれてから値が(Buildout内部で)設定されているようなので、Read Onlyなkeyとして見るべきだと思います。

特に理由がない限り、これらの値を変更しない方がきっと無難でしょう。

また、こういう隠蔽されている(?)keyは他のセクションには存在しないと思われます。

私がこれらのkeyを見つけたときは、buildout.cfg に書かれた値がレシピによって変更(設定した相対パス絶対パスに置き換わる等)されることもあるのでは、などと色々と疑心暗鬼になりましたが、そういうことをするレシピはまだ見たことがありません。
変更されないと信じたいです。

ちゃんとした例

最後に、ちゃんと動き、ちゃんと意味のあるコードと buildout.cfg の例を示します。
もちろん解説付きで。

buildout.cfg

[buildout]
parts =
    run-test
    make-interpreter
# ここだけdeepblueのsetup.pyが入っているディレクトリに書き換えてください
develop = /path/to/deepblue_sources

# deepblue が import できるように調整されたPythonインタープリタを作る
[make-interpreter]
recipe = zc.recipe.egg:script
eggs =
    deepblue
# Pythonインタープリタのファイル名を指定する
# これがないとセクション名が流用されてしまう
interpreter = earth

# Pythonのテストフレームワーク nose のコマンド
# nosetests が deepblue にアクセスできるようにする
# Buildout環境の特殊な事情により、nosetests を使うにはこのレシピが必要
[run-test]
recipe = pbp.recipe.noserunner
eggs =
    deepblue
# nosetests を実行するときに、ここに一時的にcdする
working-directory = ${buildout:develop}

この buildout.cfg の意味は以下のとおりです

インストール手順:

  1. テストや新機能を追加した deepblue 0.1.0 のアーカイブをダウンロードして展開しましょう
  2. Buildout環境を用意(新規でも使い回しでも可)したら、上記の buildout.cfg をローカルの buildout.cfg に上書きします
  3. ローカルの buildout.cfg の「develop」のところだけ修正してください
  4. 「Buildoutディレクトリ」で、コマンド bin/buildout を実行し環境を構築

環境ができたら、とりあえずテストを実行してみましょう。

$ ls
bin          buildout.cfg eggs
bootstrap.py develop-eggs parts
$ bin/run-test
..
----------------------------------------------------------------------
Ran 2 tests in 0.381s

テストは2つしか書いていないので、全てのテストが成功しました。

いよいよPythonインタープリタ(ファイル名:earth)から実行します。

$ bin/earth

>>> import deepblue
>>> deepblue.get_answer()
'The Answer to life, the universe, and everything: 42'
>>> deepblue.get_answer(from_google=True)
'The Answer to life, the universe, and everything: 42'

2つめの関数呼び出しはGoogle電卓にクエリを投げて、帰ってきたHTMLから『答え』をBeautifulSoupで切り出し、文字列にして返しています。
あまり連続で実行すると怒られそうなのでほどほどに。

おわりに

ここまでで私が知っている Buildout の情報とノウハウはほとんど出し尽くしました。
過剰なくらいに Why 分をマシマシにしたので、Buildoutがさっぱりわからない方でも何とかなるんじゃないでしょうか?

余談ですが、半年前の自分にプレゼントしたらテンション上がって走り回るくらいの内容になったと思います。
タイムマシンをお持ちの方はご連絡お待ちしております。


# それにしても…今回は長すぎるだろ…常識的に考えて…

Buildoutを使う セットアップ編

前回のEgg基礎編ではPython Eggsについて書きました。
今回はいよいよBuildoutをインストールして、設定ファイルにちょっとだけ触れたりします。

Buildoutのインストール

さてインストールするわけですが、早速はまりポイントの登場です。
インストールの方法が二通りあることが原因なので、まずはインストール方法を2つご説明しましょう。

Pythonにインストールした後に任意のディレクトリに展開

Pythonに」とはオペレーティングシステムに入っているPythonに、という意味です。
すなわち、コマンド easy_install や、それよりもいい感じの pip を使って普段通りにパッケージをインストールする方法です。
Buildoutのパッケージ名はzc.buildoutなので、これを指定してインストールします。

# easy_install zc.buildout

pipの場合はこうなります

# pip install zc.buildout

なお、はまりポイント的には「Pythonにインストールする」という点が重要なので、試してませんがユーザー毎のsite-packagesディレクトリにインストールしても同じだと思います。
コマンドを実行し、特に問題がなければPythonへのインストールは完了です。

次に、Buildoutのファイル群を展開したい任意のディレクトリに移動し、コマンドを実行します。

$ buildout init

ファイルやディレクトリが展開されたらBuildoutのディレクトリへのインストールも完了です。
今回参考にした zc.buildout 1.5.2 では、この方法で任意のディレクトリにBuildoutをインストールすると distribute が自動でインストールされました。

任意のディレクトリに直接インストール

Buildoutをダウンロード・インストールする専用のスクリプトを使って、任意のディレクトリ下に直接インストールする方法です。
このスクリプトによってインストールされるBuildoutは、Pythonのシステムから独立しているので環境を全く汚しません。
手順は、Buildoutをインストールしたいディレクトリに移動し

$ mkdir buildout
$ cd buildout

インストールスクリプトをダウンロードし

$ wget "https://raw.github.com/buildout/buildout/master/bootstrap/bootstrap.py"

実行します。
ここで使ったバージョンのPythonが以降のこのBuildout環境でずっと使われることになるようです。また、オプション -d はBuildout環境でEgg基礎編で触れたdistributeを使えという指示です。
最後の init はBuildoutの初期化(必要なファイル・ディレクトリの展開)の指示です。

(追記)2013年1月現在、どうやら古い bootstrap.py は実行時に失敗するようなので、GitHubにある新しい bootstrap.py を使いましょう(上記URL)。新しい方はオプションなしで実行します。

$ python bootstrap.py
はまりポイント

前者の方法でPythonにBuildoutをインストールしたことを忘れて、後者のスクリプトで任意のディレクトリにインストールしようとすると、解りづらいエラーが出てスクリプトが死んだ記憶があります。
面倒くさいので再現と検証はしませんが、Pythonに入れた方のBuildoutを(pipで)アンインストールしたらエラーは出なくなりました。
エラーが出る場合ははまっていないか疑ってみましょう。

Buildoutの設定ファイル

以降、Buildoutに必要なファイル・ディレクトリを展開したディレクトリのことを「Buildoutディレクトリ」と呼ぶことにします。

ここまでで、Buildoutディレクトリに「buildout.cfg」というファイルが生成されていると思います。それが設定ファイルのスケルトンです。
Buildoutディレクトリ/bin/ にある buildout コマンドは、カレントディレクトリにある「buildout.cfg」というファイルを設定ファイルと見なして読み込みます。読み込まれる設定ファイルのファイル名はオプションで変更できますが、いちいち指定するのが面倒なら buildout.cfg のままで使うのが良いでしょう。

以前書いた buildout.cfg をコピーするなどして、buildout init する前にBuildoutディレクトリに設定ファイルが存在する場合、buildout init しても buildout.cfg の生成だけスキップされるので上書きの心配はありません。

設定ファイルの構造

buildout.cfg の構造について説明します。

[buildout]
parts =
    codehl

[codehl]
recipe = zc.recipe.egg
eggs = 
    pygments

buildout.cfg 内の最も大きな要素は上記のサンプルの [buildout][codehl] など[]でくくられた部分を先頭とするカタマリです。
これはセクション(section)と呼ばれていて、1つのセクションは次のセクションが始まるまで続きます。
言うまでもないかも知れませんが、[buildout]の「buildout」、[codehl]の「codehl」の部分がセクション名です。
セクションの中でも buildout セクションは特別で、Buildout全体の設定を記述します。

セクションの中には「key = value」の形でラベルと値を書いていきます。「値」の部分はkeyによって、改行(またはスペース)で区切って複数設定(列挙)できるものと、1つしか指定できないものがあるので注意しましょう。

Buildoutで環境を自動構築してみる

今はここまでわかれば充分でしょう。
上記の buildout.cfg のサンプルを、Buildoutディレクトリの buildout.cfg に上書きしたら以下のコマンドを実行してください。
カレントディレクトリは、Buildoutディレクトリです。

$ bin/buildout

何やらメッセージが出てくると思いますが、要はPyPI – the Python Package Index · PyPIから buildout.cfg に書かれた「pygments」というパッケージをダウンロードしてBuildout環境下にインストールしたということです。
パッケージのダウンロード元のPyPIがたまーーに死んでる場合があるので、失敗したときは時間を空けてリトライするのが優しさとか大人の余裕とかいうものじゃないでしょうかねー

インストールしたPygmentsで遊ぶ

先の bin/buildout コマンドによって、Pygmentsというパッケージがインストールされました。PygmentsはPython製のSyntax highlighterです。
Pygmentsをインストールすると pygmentize というコマンドも同時にインストールされます(Buildoutの機能によるものではない)
この pygmentize でコードに色をつけてみましょう。

ディレクトリが以下のようになっているとして(一部省略してます)

+ buildout
    + bin
        - buildout
        - pygmentize
    - buildout.cfg
+ deepblue_sources
    - setup.py
    + deepblue
        - __init__.py
        - answer.py

Buildoutディレクトリで(ここである必要性は全くありません)以下のコマンドを実行します。

$ bin/pygmentize ../deepblue_sources/deepblue/answer.py

実行結果:

Windowsの方はcmd.exeで上記のコマンドを実行すると、大量のゴミが混入したすごい画面になるので諦めてHTMLに出力しましょう。

> bin\pygmentize -O full,style=colorful -o out.html ..\deepblue_sources\deepblue\answer.py

結果(ブラウザで表示):

おわりに

最後は脱線しましたが、buildout.cfg さえあればどこにでも同じ環境が自動で構築できることがお分かりいただけたかと思います。

次回は設定ファイル編ということにして、buildout.cfg をもっとちゃんと書きます。

Builtoutを使う Egg基礎編

前回の概要編では訳文的なノリで書いてみましたが、なかなか上手くいかないものですね

今回はPython Eggsを作るために必要なものについて書きます。

とはいったものの、Buildout(zc.buildout)を使うのに何故Eggの話を挟む必要があるのか、と思っている方もいると思います。
その理由は簡単です。BuildoutはPyPIやローカルにあるEgg(.eggまたはEggにする前のソースコード)を組み合わせてアプリケーション(のための環境)を構築するシステムだからです。
つまりBuildoutはEggがベースになっているので、あなたが今から書こうとしているアプリケーションもEggである必要があるのです。

そういうわけなので、この記事を読んでEggを作れるようになりましょう。

念のためご存じない方のために説明しますが、Python EggsとはJavaでいうところのJAR(Java ARchive)にあたるもので、PythonのパッケージのファイルをまとめてZIPで固めたファイル(拡張子は.egg)のことです。
また、Eggに必要なファイル(setup.py)を含んだPythonのパッケージで、まだEggとして固められていないものも便宜上Eggと呼ぶことにします。

Pythonのパッケージについて

Pythonのチュートリアルにも書かれていますが、パッケージとはPythonのモジュール(*.py)のコレクションのことです。
具体的には、あるディレクトリ直下に__init__.pyという名前のファイルがあれば、それはパッケージです。ファイル1つだとパッケージにする意味は薄いですが。
__init__.pyが存在すれば中身は空でも問題ありませんが、パッケージの初期化コードを書いたりドキュメンテーション文字列(docstring)にパッケージの簡単な説明を書いたりすることが多いようです。

setuptools

setuptoolsとはPythonのパッケージをダウンロード/ビルド/インストールが自動で、アンインストールが(頑張れば)手動でできるようになるパッケージマネージャです。
Pythonには元々標準ライブラリにdistutilsという配布物の構築・インストールができる仕組みが備わっていますが、setuptoolsはこれを強化する目的で作られたようです。
Python関係の記事でよく出てくる easy_install というコマンドはsetuptoolsが由来です。

ところが、setuptoolsの開発が滞ってきたおりに上位互換性をもつdistributeというパッケージマネージャが現れ、現在ではこのdistributeを使う方がよいとされています。
つまり distutils < setuptools < distribute の順でよりモダンであり、かついい感じということのようです。
なので、まだsetuptoolsを使っている方は distribute · PyPI を今すぐ手に入れましょう

パッケージの構成

ここからは実際にPythonのパッケージを作って、Eggに仕立ててみましょう。
Pythonのパッケージは、だいたい以下のようなディレクトリ・ファイル構成になります。

+ deepblue_sources
    - setup.py
    + deepblue
        - __init__.py
        - answer.py

一応パッケージの実物を置いておきます。

setup.pyを書く

Eggにはsetup.pyが欠かせません。Eggの全てのメタデータはこのsetup.pyに定義します。
setup.pyの中身はこんな感じになります。

# -*- coding: utf-8 -*-

from setuptools import setup

setup(name='deepblue',
      version='0.0.1',
      packages=['deepblue'])

関数 setup() がsetup.pyの本体で、これにメタデータを食わせます。

引数name

パッケージの名前であり、正式な識別子です。

引数version

パッケージのバージョンで、一応一定のフォーマットで記述しましょうということになっています

引数packages

Pythonのパッケージのディレクトリ(直下に__init__.pyを含む)を指定します。
詳細はhttp://www.python.jp/doc/release/distutils/setupscript.html#listing-packagesを参照してください

setup.pyを使う

setup.py が完成したら、コマンドラインから実行してみましょう。
--help オプションを与えると setup.py はヘルプメッセージを出力して終了します。

$ python setup.py --help
Common commands: (see '--help-commands' for more)

  setup.py build      will build the package underneath 'build/'
  setup.py install    will install the package

Global options:
  --verbose (-v)      run verbosely (default)
  --quiet (-q)        run quietly (turns verbosity off)
(長すぎるので略)

$ python setup.py --name
deepblue

$ python setup.py --version
0.0.1

ではいよいよEggを作ってみたいと思います。

$ python setup.py bdist_egg
running bdist_egg
running egg_info
creating deepblue.egg-info
writing deepblue.egg-info/PKG-INFO
(やっぱり長すぎるので以下略)

コマンドを実行するといくつかのディレクトリが作られ、その中のdistディレクトリ下にEgg(*.egg)ができているはずです

$ ls
build             deepblue.egg-info setup.py
deepblue          dist

$ ls dist
deepblue-0.0.1-py2.7.egg

Eggができました!

setup.py その他のコマンド

setup.py --help にも書かれていますが、bdist_eggの他にも便利なコマンドがいくつもあるので少し触れておきます。
というかBuildoutを扱うなら知っておいた方がいいと思います。

setup.py install

言わずと知れたパッケージのインストールコマンド
デフォルトではPythonのsite-packagesにパッケージをインストールする

setup.py develop

そのへんのディレクトリに置いてある開発中のパッケージへのリンク(symlink等ではない)をsite-packagesに張ることで、パッケージを擬似的にインストールする。
develop install とか呼ばれるらしい。
-u オプションでリンクを解除できる。

おわりに

今回長かった…

次回、Buildoutセットアップ編ではセットアップの方法とかはまりポイントとか書きます。

Buildoutを使う 概要編

Buildout(zc.buildout)Pythonで書かれた、アプリケーションのビルドシステムです。
任意の場所にオペレーティングシステムにインストールされたPythonから隔離された箱庭環境を作り、そこに設定ファイルに書かれたパーツと呼ばれる構成要素を組み合わせて、そのアプリケーションのためだけの環境を構築します。

同じく箱庭環境を作るvirtualenvというものもありますが、virtualenvがシェルから必要なPythonのパッケージを手動でインストールしなければならないのに対し、Buildoutは設定ファイルに従って必要なパッケージを自動でインストールしてくれます。

Buildoutのいいところは、設定ファイルとわずかな時間さえあれば全く同じ箱庭環境を、2つか3つのコマンドを叩くだけで再現できるところでしょう。

wget "http://svn.zope.org/*checkout*/zc.buildout/trunk/bootstrap/bootstrap.py"
python bootstrap.py init
bin/buildout

例にwgetコマンドが出てきましたが、Linuxだけではなく*BSDなど他のUNIX系のOSでもBuildoutは動作します。
WindowsUNIX文化を実践している鋼の心をお持ちの方にも朗報です。BuildoutはWindowsでも問題なく動作します!

しかし残念なことに、Buildoutを使ってPythonのアプリケーションを開発するにはEggについての知識が不可欠です。Eggの書き方さえ理解すれば、Buildoutの環境下で開発を始めるのにさほど苦労はしないでしょう。

次回のEgg基礎編ではこのEggを使ったり、作ったりするのに必要なsetuptoolsやsetup.pyについて書きたいと思います。

SQLAlchemyのdeclarativeなモデルの継承

SQLAlchemy というPythonのORMライブラリがあります。

こいつでテーブルを定義するには二通りの方法があって、1つは従来の「テーブル定義とそれを表現するオブジェクト(のクラス)を定義した後、2つをマッピングする」もの、もう1つは「declarativeという機能を使って、1つのクラス定義だけでテーブル定義とマッピングができる」ものがあります。

後者(declarative)の方が楽なので好きなんですが、あんまり日本語情報がないのでメモも兼ねてdeclarativeのモデルの継承についてちょっと書いてみたいと思います。

今回使ったSQLAlchemyのバージョンは0.7.5です

declarative_base() で生成したクラスオブジェクトについて

結論から言うと、モデルを書くときはテーブル名・主キーを書かないとSQLAlchemyに怒られます。

以下のコードは必要なものが足りないので怒られます

# -*- coding: utf-8 -*-

from sqlalchemy import Column, String, Integer
from sqlalchemy.ext import declarative

Base = declarative.declarative_base()

class User(Base):
    # __tablename__ = 'users'  # 必要なもの
    id = Column(Integer, primary_key=True) # 主キーもないと怒られる
    name = Column(String(64), nullable=False)

エラーメッセージはこんな感じ

sqlalchemy.exc.InvalidRequestError: Class <class '__main__.User'> does not have a __table__ or __tablename__ specified and does not inherit from an existing table-mapped class.

これはどうやらPythonのabcで提供されているメタクラス、ABCMetaと同じような抽象メンバの再定義を強制する機能を実装しているようです

ただしabcは使っておらず、また __metaclass__ も使ってはいないようです

from sqlalchemy.ext import declarative
Base = declarative.declarative_base()
import inspect
print inspect.getmembers(Base, lambda m: inspect.isclass(m) and issubclass(m, type))
[('__class__', <class 'sqlalchemy.ext.declarative.DeclarativeMeta'>)]

なので、テーブル名・主キーを定義した上でなら、ABCMetaを使った抽象クラスが書けるみたいです(試してない)

モデルの継承

クラスを継承してモリモーフィックにDBアクセスしたいときは、モデルを少し弄ってやらないといけません

サンプルとして以下にコードと出力結果を貼ります

# -*- coding: utf-8 -*-

from sqlalchemy import Column, String, Integer, ForeignKey, create_engine
from sqlalchemy.orm import sessionmaker, relation
from sqlalchemy.ext import declarative

engine = create_engine('sqlite:///:memory:')
Session = sessionmaker(bind=engine)
session = Session()

Base = declarative.declarative_base()

class MailAddress(Base):
    __tablename__ = 'mail_addresses'
    id = Column(Integer, primary_key=True)
    addr = Column(String(256), nullable=False)
    user_id = Column(Integer, ForeignKey('users.id'))

    def __init__(self, addr):
        self.addr = addr

class Person(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(String(64), nullable=False)

    _discriminator = Column(String(32))
    __mapper_args__ = {'polymorphic_on': _discriminator,
                       'polymorphic_identity': 'user'}

    def __init__(self, name):
        self.name = name

class Engineer(Person):
    __mapper_args__ = {'polymorphic_identity': 'engineer'}
    mail_addresses = relation('MailAddress', backref='engineer')

    def __init__(self, name, addr):
        super(Engineer, self).__init__(name)
        self.mail_addresses.append(MailAddress(addr))

Base.metadata.create_all(engine)

john = Person('John Doe')
jane = Engineer('Jane Doe', 'jane@example.com')
session.add_all([john, jane])
session.commit()

def get_mail_addresses(person):
    if hasattr(person, 'mail_addresses'):
        return [a.addr for a in person.mail_addresses]
for p in session.query(Person).all():
    print '%s\'s mail addresses: %s' % \
            (p.name, get_mail_addresses(p))
John Doe's mail addresses: None
Jane Doe's mail addresses: [u'jane@example.com']

クラス Person のインスタンスを取得しようとクエリを投げているにも関わらず、Personのサブクラスである Engineer のインスタンスも同時に取得できています

ここで属性 __mapper_args__ が出てきましたが、これは従来のテーブル・クラスマッピング定義におけるmapper()関数の引数に相当します

Person のpolymorphic_onにはpolymorphic_identityを入れる属性を指定し、
polymorphic_identityには型を区別するための識別子の文字列を指定します

このへんの(declarativeでない方法による)詳しい説明はドキュメントの和訳か、本家のdeclarativeのドキュメントに書かれているので参考にしてください

WinでTitanium Studio

ちょっと(楽して)作ってみようって人にはハードル高すぎだろこの環境。
というわけでWinに入れたTitanium Studioがそのままではテンプレートで生成されたデフォルトのコードすら実行できない問題の解決方法とか書きます。

なお私はAndroidでやりましたので、(いるのかわかりませんが)BlackBerryの人は他を当たってください

あとTitanium Studioのバージョンは Titanium Studio, build: 1.0.2.201107130739 で、Titanium SDK は 1.7.2 だそうです。

基本

まあこれは問題ではないんですが、設定を弄るダイアログがメニューバーの Window -> Preferences に入ってるのがちょっと意外だったので。

Android SDK

2つほど問題があるようです。

Android SDKのパス

http://www.b4u.sakura.ne.jp/wp/?p=238 によるとAndroid SDKのパスにスペースが入ってるとおかしくなるらしい
私も試行錯誤の途中でやってみただけなので、本当におかしくなるのかはわかりませんがコードのビルドが成功した環境ではSDKをスペースが入ってないパスに置いてあるので、まあやって損はないんじゃないでしょうか。

Android SDKが入ってるディレクトリの指定

私はインストーラで入れたのでデフォルトの android-sdk というディレクトリを指定します。
SDK Manager.exeとかplatform-toolsが入ってるディレクトリです。

で、まあ指定するんですが、なぜかAndroid 2.1のSDKが入ってないとAndroid SDKディレクトリと認識されません。なんだこれ

そういうわけなのでAndroidSDK ManagerからAndroid 2.1のSDKを入れましょう。使わないけど。

Android Emuratorの引数

さて、設定は済みましたがこのままだとビルド後にエミュレータで実行するときにこけると思われます。

Android Emuratorの引数にスペースの入ったパスが含まれている場合、このスペースが引数の区切りとして解釈されるためこんな引数しらねーよ!(意訳)と怒られた結果、エミュレータが起動しないからです。

幸いにもエミュレータに渡す引数の構築はPythonスクリプトでやっているようなので、さくっとfixします。

書き換えるスクリプト
%ALLUSERSPROFILE%\Application Data\Titanium\mobilesdk\win32\1.7.2\android\builder.py
です。環境変数やTitanium SDKのバージョンは適当に読み替えてください。
私の環境では %ALLUSERSPROFILE% は C:\Documents and Settings\All Users に展開されました

このスクリプトの393行目あたりでエミュレータの引数を構築しています。

# start the emulator
emulator_cmd = [
    self.sdk.get_emulator(),
    '-avd',
    avd_name,
    '-port',
    '5560',
    '-sdcard',
    self.sdcard,    # この行を書き換える
    '-logcat',
    '*:d,*',
    '-no-boot-anim',
    '-partition-size',
    '128' # in between nexusone and droid
]

上で示した行のコードを '"%s"' % self.sdcard に書き換えます。
書き換えるとこんな感じ

# start the emulator
emulator_cmd = [
    self.sdk.get_emulator(),
    '-avd',
    avd_name,
    '-port',
    '5560',
    '-sdcard',
    '"%s"' % self.sdcard,    # self.sdcard から書き換えた
    '-logcat',
    '*:d,*',
    '-no-boot-anim',
    '-partition-size',
    '128' # in between nexusone and droid
]

まあパスをダブルクオートしただけです。

なお、ためしに self.sdcard にパスを代入する段階でクオートしたところ別の場所でこけるようになってしまったので、エミュレータの引数を構築するこの段階でクオートするのが正解のようです。

その他の注意点

パス

環境変数 Path に (Android SDKの入ってるディレクトリ)\platform-tools と (同ディレクトリ)\tools が入っていることを確認しましょう。

パスが通っていれば、コマンドプロンプトから adb version でadbのバージョンが出てくるはずです

また、JDKへのパスが通っていないかもしれません
その場合も環境変数 Path に (JDKディレクトリ)\bin を追加しましょう

パスが通っていれば、 javac -version でjavacのバージョンが(ry

adb.exe

ビルドして実行するときに adb.exe が動いたままになっていたらタスクマネージャから殺しましょう

最後に

これでビルドして実行できるはずです。
できない人はビルドログか何かを見て頑張ってください。