Pyramid でポニーを表示する 7 つの方法

これは python advent calendar 2012 の xx日目の記事です。

まえがき

Django 大人気ですね(挨拶)。 pylonsproject.jp の方から来ました。 @knzm です。 Web フレームワークは数年前から Pylons を使っていて、 その流れで今も Pyramid 周辺で活動しています。

@aodag さんから「なんか書かない? Pylons でもいいよ」 と言われたんですが、正直 Pylons はオワコンなので 代わりに Pyramid でネタを考えることにしました。 それで、 Django にあって Pyramid にないものって何だろう? と考えてみたところ、 やっぱりポニーじゃないかと。

http://djangopony.com/media/img/small/wallpaper.png

ゆるキャラ重要ですよ。 Pyramid はこれですから。

http://www.pylonsproject.org/static/images/pyramid-tee-banner.png

せっかくなので、

  • Pyramid をあまり使ったことがない人の参考になる
  • 昔から Pylons や WSGI を知っている人には懐かしい
  • Django に対抗できる

そんな記事にしたいと思いました。 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 で pyramid_pony.not_foundpyramid_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() 関数で追加されたものです。

route ファクトリを使ってポニーを表示する

同じようにして pyramid_pony.before_renderpyramid_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 クラスのインスタンスを 返しています (UnicornPony のサブクラスです)。

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)

tween でポニーを表示する

pyramid.includespyramid_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.includespyramid_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 にはポニーがない、なんて言われなくて済みますね!