Pyramid でポニーを表示する 7 つの方法 ======================================== .. author:: default .. categories:: none .. tags:: none .. comments:: まえがき -------------------- .. note:: この記事は、 `2012 Python アドベントカレンダー (Webフレームワーク) `_ で Django が大人気だったことに対抗して書かれました。 Django のマスコットキャラといえば Django Pony ですね。 .. figure:: http://djangopony.com/media/img/small/wallpaper.png :align: center それに対して Pyramid はこれです。 .. figure:: http://www.pylonsproject.org/static/images/pyramid-tee-banner.png :align: center ……どうしてこうなった。これでは Django 人気にはとても対抗できません。 今時代が求めているのはゆるキャラです! 実は Django Pony が誕生する以前からポニーは Paste に含まれていて、 Pylons ユーザの間で親しまれていました。 .. seealso:: `Django vs Pylons: Pylons fails to deliver a pony? `_ そこでこの記事では、マスコットキャラ不在の問題を解消するとともに、 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") not found ビューでポニーを表示する ---------------------------------------- 再度 development.ini を開いて先ほど編集した行を以下のように書き換えてください。 :: pyramid.includes = pyramid_debugtoolbar pyramid_pony.not_found 今度は http://localhost:6543/ のトップページ以外のどこにアクセスしても ポニーが表示されるようになります。 これは main 関数に以下のように書いたのと同じです。 :: config.add_notfound_view("pyramid_pony.pony.view") before renderer イベントを使ってポニーを表示する -------------------------------------------------- development.ini で :mod:`pyramid_pony.not_found` を :mod:`pyramid_pony.before_render` に 書き換えてください。 http://localhost:6543/pony でやっぱりポニーが表示されます。 :mod:`pyramid_pony.before_render` では、 :class:`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 の内容は以下の通りです。 :: Pony
${animal}

${link}

Home

テンプレートの中で参照している変数 (``${animal}`` など) は、 :func:`add_global` 関数で追加されたものです。 route ファクトリを使ってポニーを表示する ---------------------------------------- 同じようにして :mod:`pyramid_pony.before_render` を :mod:`pyramid_pony.route_factory` に書き換えると、 http://localhost:6543/pony でポニーが表示されます。 (面倒くさくなってきたので、これ以降の節ではこのステップの説明を省略します) :mod:`pyramid_pony.route_factory` では :func:`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`` に指定するのは :mod:`pyramid_pony.response_adapter` です。 :mod:`pyramid_pony.response_adapter` では view で :class:`Pony` クラスのインスタンスを 返しています (:class:`Unicorn` は :class:`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") それでもポニーが表示されるのは、以下のようなレスポンスアダプターが 登録されているからです。この関数はビューがレスポンスを返した後で、 それが :class:`Pony` クラスのインスタンスだった場合に呼ばれます。 :: def pony_response_adapter(pony): html = TEMPLATE.format( animal=pony.animal, url=pony.url, link=pony.link, home=pony.home) return Response(html) tween でポニーを表示する ------------------------------ ``pyramid.includes`` に :mod:`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 ミドルウェアに相当します。 view predicate を使ってポニーを表示する -------------------------------------------------- 最後に Pyramid 1.4 の新機能であるサードパーティー predicate を使ってみます。 ``pyramid.includes`` に :mod:`pyramid_pony.view_predicate` を指定してください。 :mod:`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`` に対応付けられています。 どちらのビューが呼び出されるかを決めているのが :meth:`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 を使うと条件分岐を明示的に書かなくても 条件に合った適切なビューが自動的に呼び出されるようになります。 .. note:: この例のようなリクエストパラメータによる条件分岐であれば、サードパーティー predicate の代わりに ``request_param`` を使って書くこともできます。 詳しくは `Pyramid のドキュメント `_ を参照してください。 あとがき -------------------- いかがだったでしょうか。 Pyramid の拡張性の高さが分かってもらえたのではないかと思います。 あと、今回プロジェクト側のコードは最初に生成してから 1 行も書き換えていないことに 注目してください。変更したのは設定ファイルだけです。 つまり、便利な add-on が増えると、ほとんどコードを書かなくても 開発ができてしまうということです。 将来 Pyramid ユーザが増えて add-on も沢山公開されて 皆が幸せになれる、そんな未来が来るといいなと思っています。 Pyramid にポニーを対応させたのは実はこれが初めてではありません。 akhet 2.0 という偉大な先例があります。 今回作成した add-on の中にも、akhet から丸々コピーしたファイルが 含まれています。 ただ、今回この記事を書いている間に :mod:`akhet.pony` が Python 3 で正常に動かないことを 発見したので、 `pull request `_ を送っておきました。 これで Django vs Pyramid で Pyramid にはポニーがない、なんて言われなくて済みますね!