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

まえがき

Note

この記事は、 2012 Python アドベントカレンダー (Webフレームワーク) で Django が大人気だったことに対抗して書かれました。

Django のマスコットキャラといえば Django Pony ですね。

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

それに対して Pyramid はこれです。

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

……どうしてこうなった。これでは Django 人気にはとても対抗できません。 今時代が求めているのはゆるキャラです!

実は Django Pony が誕生する以前からポニーは Paste に含まれていて、 Pylons ユーザの間で親しまれていました。

そこでこの記事では、マスコットキャラ不在の問題を解消するとともに、 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 を使うと条件分岐を明示的に書かなくても 条件に合った適切なビューが自動的に呼び出されるようになります。

Note

この例のようなリクエストパラメータによる条件分岐であれば、サードパーティー predicate の代わりに request_param を使って書くこともできます。 詳しくは Pyramid のドキュメント を参照してください。

あとがき

いかがだったでしょうか。 Pyramid の拡張性の高さが分かってもらえたのではないかと思います。

あと、今回プロジェクト側のコードは最初に生成してから 1 行も書き換えていないことに 注目してください。変更したのは設定ファイルだけです。 つまり、便利な add-on が増えると、ほとんどコードを書かなくても 開発ができてしまうということです。 将来 Pyramid ユーザが増えて add-on も沢山公開されて 皆が幸せになれる、そんな未来が来るといいなと思っています。

Pyramid にポニーを対応させたのは実はこれが初めてではありません。 akhet 2.0 という偉大な先例があります。 今回作成した add-on の中にも、akhet から丸々コピーしたファイルが 含まれています。 ただ、今回この記事を書いている間に akhet.pony が Python 3 で正常に動かないことを 発見したので、 pull request を送っておきました。 これで Django vs Pyramid で Pyramid にはポニーがない、なんて言われなくて済みますね!