これは python advent calendar 2012 の xx日目の記事です。
Django 大人気ですね(挨拶)。 pylonsproject.jp の方から来ました。 @knzm です。 Web フレームワークは数年前から Pylons を使っていて、 その流れで今も Pyramid 周辺で活動しています。
@aodag さんから「なんか書かない? Pylons でもいいよ」 と言われたんですが、正直 Pylons はオワコンなので 代わりに Pyramid でネタを考えることにしました。 それで、 Django にあって Pyramid にないものって何だろう? と考えてみたところ、 やっぱりポニーじゃないかと。
ゆるキャラ重要ですよ。 Pyramid はこれですから。
せっかくなので、
そんな記事にしたいと思いました。 Pyramid のセールスポイントは拡張性の高さなので、それが伝わる内容になったらいいな。
ということで、 Pyramid を拡張するあらゆる手段を駆使してポニーを表示してみようと思います。
なぜか「Pyramid はこわい」というイメージが広がっているようなので、 Pyramid を使ったことがない人にも安心して読んでもらえるように 一から丁寧に説明していきます。 よく分かっている人はスキップしても OK です。
最初に virtualenv を使って Pyramid をインストールする環境を作ります。 今回は何となく Python 3 で作ってみました。
$ cd pyramid_pony_demo
$ python3 virtualenv.py --no-site-packages .
$ . bin/activate
pip を使って pyramid をインストールします。 依存関係にあるパッケージがまとめてインストールされるので多少時間がかかります。
(pyramid_pony_demo)$ pip install pyramid
pcreate コマンドでプロジェクトを作成します。
(pyramid_pony_demo)$ pcreate -s starter pyramid_pony_demo
...
Welcome to Pyramid. Sorry for the convenience.
Pyramid を使ったプロジェクトのひな形ができました。
(ちなみに最後のメッセージ “Sorry for the convenience.” が長い間 謎だったんですが、どうやら アメリカの有名なコメディが元ネタ みたいですね。 知らんわ、そんなの)
プロジェクトのセットアップを行います。 ここでもいくつかの依存パッケージがインストールされます。
(pyramid_pony_demo)$ cd pyramid_pony_demo
(pyramid_pony_demo)$ python setup.py develop
pserve で実行します。
(pyramid_pony_demo)$ pserve --reload development.ini
http://localhost:6543/ にアクセスすれば、 Pyramid のロゴが 表示されるはずです。
はい、簡単ですね。
今回のために add-on を用意しました。 以下のコマンドでインストールしてください。
$ pip install https://github.com/knzm/pyramid_pony/archive/master.zip
インストールが終わったら development.ini を開いて、
pyramid.includes =
pyramid_debugtoolbar
となっている箇所を見つけて、次のように1行追加してください。
pyramid.includes =
pyramid_debugtoolbar
pyramid_pony
http://localhost:6543/pony にアクセスすると、知っている人にはおなじみの ポニー(の ASCII アート)が表示されます。 “add horn!” でユニコーンにも変身します。
,_.-('--.
.( '-.'.\'\_,
/ `-.`_;;-./(/=,
|.-.-'.' .`.\
{ .-.' //,|\\
/-'./ '._ <| \)
{ -./ |\ |
{_/ \ | |
.--"""--..,___,.;' |( o/
.' | `"`
/ /
_.-'| `-._
.' .'/| _ `\
/ .-'| \ , _.-`'-.__.' | /
|( / | | / `'------'` \ \/ /
\ ) \ / /`._ /`-./ /
|.' / )', \._ `\ \___ /
/ .'_.; \ \ `) | / /`
'._;.-,) `\ \/ | .-' /
( ) |_/ |_.'
jgs .-' /
\_.'
add horn!
Home
これは main 関数に以下のように書いたのと同じです。
config.add_route("pony", "/pony")
config.add_view("pyramid_pony.pony.view", route_name="pony")
再度 development.ini を開いて先ほど編集した行を以下のように書き換えてください。
pyramid.includes =
pyramid_debugtoolbar
pyramid_pony.not_found
今度は http://localhost:6543/ のトップページ以外のどこにアクセスしても ポニーが表示されるようになります。
これは main 関数に以下のように書いたのと同じです。
config.add_notfound_view("pyramid_pony.pony.view")
development.ini で pyramid_pony.not_found を pyramid_pony.before_render に 書き換えてください。 http://localhost:6543/pony でやっぱりポニーが表示されます。
pyramid_pony.before_render では、 BeforeRender イベントにサブスクライバを 登録して、テンプレートに値を渡しています。
def add_global(event):
req = event['request']
event["home"] = req.script_name or "/"
url = req.path
if req.params.get("horn"):
data = UNICORN
event["link"] = "remove horn!"
event["url"] = req.path
else:
data = PONY
event["link"] = "add horn!"
event["url"] = req.path + "?horn=1"
data = base64.b64decode(data)
animal = zlib.decompress(data).decode('ascii')
event["animal"] = animal
def view(request):
return {}
def includeme(config):
config.add_route("pony", "/pony")
config.add_subscriber(add_global, BeforeRender)
config.add_view(view, route_name="pony", renderer='pyramid_pony:pony.mako')
見ての通り view 関数の中では何もしていません。
pony.mako の内容は以下の通りです。
<!DOCTYPE html>
<html><head><title>Pony</title></head><body>
<pre>${animal}</pre>
<p><a href="${url}">${link}</a></p>
<p><a href="${home}">Home</a></p>
</body></html>
テンプレートの中で参照している変数 (${animal} など) は、 add_global() 関数で追加されたものです。
同じようにして pyramid_pony.before_render を pyramid_pony.route_factory に書き換えると、 http://localhost:6543/pony でポニーが表示されます。 (面倒くさくなってきたので、これ以降の節ではこのステップの説明を省略します)
pyramid_pony.route_factory では config.add_route() に factory 引数を 指定しています。こうすることで view が呼ばれた時に request.context の中身が 指定した factory のインスタンスになっています。
class PonyContext(object):
def __init__(self, request):
self.request = request
if request.params.get("horn"):
self.data = UNICORN
self.link = "remove horn!"
self.url = request.path
else:
self.data = PONY
self.link = "add horn!"
self.url = request.path + "?horn=1"
@reify
def home(self):
self.request.script_name or "/"
def decode(self, data):
data = base64.b64decode(data)
return zlib.decompress(data).decode('ascii')
def view(request):
context = request.context
data = context.data
html = TEMPLATE.format(
animal=context.decode(data),
url=context.url,
link=context.link,
home=context.home)
return Response(html)
def includeme(config):
config.add_route("pony", "/pony", factory=PonyContext)
config.add_view(view, route_name='pony')
view の中では context の属性参照とメソッド呼び出ししかしていないので、 非常にすっきりしています。
次に pyramid.includes に指定するのは pyramid_pony.response_adapter です。
pyramid_pony.response_adapter では view で Pony クラスのインスタンスを 返しています (Unicorn は Pony のサブクラスです)。
def view(request):
if request.params.get("horn"):
return Unicorn(request)
else:
return Pony(request)
def includeme(config):
config.add_route("pony", "/pony")
config.add_response_adapter(pony_response_adapter, Pony)
config.add_view(view, route_name="pony")
それでもポニーが表示されるのは、以下のようなレスポンスアダプターが 登録されているからです。この関数はビューがレスポンスを返した後で、 それが Pony クラスのインスタンスだった場合に呼ばれます。
def pony_response_adapter(pony):
html = TEMPLATE.format(
animal=pony.animal,
url=pony.url,
link=pony.link,
home=pony.home)
return Response(html)
pyramid.includes に pyramid_pony.tween を指定してください。
このモジュールでは、以下のような tween が定義されています。
def pony_tween_factory(handler, registry):
def pony_tween(request):
if request.path == '/pony':
return pony_view(request)
else:
return handler(request)
return pony_tween
def includeme(config):
config.add_tween('pyramid_pony.tween.pony_tween_factory')
tween は ‘between’ から作られた造語で、その意味からも分かる通り Pyramid がビューを呼び出す過程の途中で呼ばれます。 tween は WSGI における WSGI ミドルウェアに相当します。
最後に Pyramid 1.4 の新機能であるサードパーティー predicate を使ってみます。
pyramid.includes に pyramid_pony.view_predicate を指定してください。
pyramid_pony.view_predicate では 2 つのビューが定義されています。
def pony_view(request):
home = request.script_name or "/"
link = "add horn!"
url = request.path + "?horn=1"
animal = decode(PONY)
html = TEMPLATE.format(animal=animal, url=url, link=link, home=home)
return Response(html)
def unicorn_view(request):
home = request.script_name or "/"
link = "remove horn!"
url = request.path
animal = decode(UNICORN)
html = TEMPLATE.format(animal=animal, url=url, link=link, home=home)
return Response(html)
2つのビューは同じ route_name に対応付けられています。 どちらのビューが呼び出されるかを決めているのが config.add_view() の horn= 引数です。
class HornPredicate(object):
def __init__(self, val, config):
self.val = val
def text(self):
return 'content_type = %s' % (self.val,)
phash = text
def __call__(self, context, request):
return bool(request.params.get("horn")) == bool(self.val)
def includeme(config):
config.add_route("pony", "/pony")
config.add_view_predicate('horn', HornPredicate)
config.add_view(pony_view, route_name="pony", horn=False)
config.add_view(unicorn_view, route_name="pony", horn=True)
このように、 view predicate を使うと条件分岐を明示的に書かなくても 条件に合った適切なビューが自動的に呼び出されるようになります。
いかがだったでしょうか。 Pyramid の拡張性の高さが分かってもらえたのではないかと思います。
あと、今回プロジェクト側のコードは最初に生成してから 1 行も書き換えていないことに 注目してください。変更したのは設定ファイルだけです。 つまり、便利な add-on が増えると、ほとんどコードを書かなくても 開発ができてしまうということです。 将来 Pyramid ユーザが増えて add-on も沢山公開されて 皆が幸せになれる、そんな未来が来るといいなと思っています。
今回題材にしたポニーは、元は Paste に含まれていたものです。 Django Pony が誕生するずっと以前から Pylons ユーザの間では親しまれていました。
Pyramid にポニーを対応させたのは実はこれが初めてではありません。 akhet 2.0 という偉大な先例があります。 今回作成した add-on の中にも、akhet から丸々コピーしたファイルが 含まれています。 ただ、今回この記事を書いている間に akhet.pony が Python 3 で正常に動かないことを 発見したので、 pull request を送っておきました。 これで Django vs Pyramid で Pyramid にはポニーがない、なんて言われなくて済みますね!