軽量、シンプルな Web フレームワーク bobo を使ってみよう

@ (Python製テンプレートエンジンあれこれとJinja2 - YAMAGUCHI::weblog) さんから Python Web フレームワーク アドベントカレンダー2010 のバトンがまわってきました *1 *2 。今年のお題は「Web フレームワーク」です。普通だったら何を書こうか悩みますよね、でも、ちょっと WebFrameworks - Python Wiki のページを見てください。フルスタックだ、ベースフレームワークだ、テンプレート付きだと、ざっと見渡しても両手の指では数え切れないぐらい Python の Web フレームワークはあるのですね。かぶらないように、、、というか、選り取り見取り、参差錯落(しんしさくらく)、石を投げればフレームワークに当たる、、、えっと何の話でしたっけ。

bobo って何?

ということで bobo という WSGI アプリケーションを作成する軽量 Web フレームワークの紹介です。OSC Tokyo/Fall 2009 で Web開発者に贈る昨今のPython事情 *3 の発表を聴講して知りました。bobo は Zope の前身だったフレームワークの名前で、それがいま風に復活したようです。

bobo の機能は以下の2つです。

  • オブジェクトへ URL をマッピングする
  • HTTP レスポンスを生成するオブジェクトを呼び出す*4

とにかく簡単で小さいフレームワークを目標に開発されています。テンプレート言語は持たず、DB とのインタフェースも持たず、その他の機能は WSGIミドルウェアか、別のライブラリを使ってねと。何て潔く、清々しい、気持ちの良いフレームワークでしょうか。紹介記事の分量も少なくて済みます!

bobo ことはじめ

ことさらに簡単です。

import bobo

@bobo.query('/')
def hello():
    return "Hello world!"

bobo (デバッグ)サーバを起動します。

$ bobo -f hello.py 
Serving ['bobo__main__'] on port 8080...
client.example.com - - [14/Dec/2010 10:56:54] "GET / HTTP/1.1" 200 12

以下の URL にブラウザでアクセスすると "Hello world!" が表示されます。

http://localhost:8080/

URL のマッピングをデコレータで書けるのは簡潔で良いですね。

ちょっと余談になりますが、@bobo.query のリファレンス によると、以下のコードは同じです。

@bobo.query
def example():
    ...

@bobo.query()
def example():
    ...

bobo.py を覗いてみると、

def query(route=None, method=('GET', 'POST', 'HEAD'),
          content_type=_default_content_type, check=None, order=None):
    return _handler(route, method=method, params='params', check=check,
                    content_type=content_type, order_=order)
...
676 def _handler(route, func=None, **kw):
677     if func is None:
678         if route is None or isinstance(route, basestring):
679             return lambda f: _handler(route, f, **kw)
680         func = route
681         route = None
682     elif route is not None:
683         assert isinstance(route, basestring)
684         if route and not route.startswith('/'):
685             raise ValueError("Non-empty routes must start with '/'.", route)
686 
687     return _Handler(route, func, **kw)
...

bobo.query デコレータは _handler() を返しているので、その部分を引用しました。本来、デコレータの扱いは、カッコの有無で振る舞いが大きく変わってしまいます。このカッコの有無の違いを意識させない実装になっています *5。この意識させるさせないの議論は賛否両論ありそうでコメントし辛いですが、ドキュメントにも明記されているので bobo はそういう仕様なんだということですね。

bobo にできること

ドキュメントを眺めながらざっくり見ていきます。

自動的なレスポンスオブジェクトの生成

bobo は値を返すときに レスポンスオブジェクト自動的に生成します。実際には webob.Response を使用しています。例えば、@bobo.query デコレータに content_type を指定することで生成するレスポンスオブジェクトを変更することができます。

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

import bobo

@bobo.query("/", content_type="text/html; charset=sjis")
def hello():
    return unicode("Hello ワールド!", "utf-8")

@bobo.query("/data", content_type="application/json")
def get_data():
    return dict(a=1, b=2)
ルート

Ruby on Rails のルーティングを模した URL をマッピングする構文を bobo では ルート と呼びます。

ちょっと汚いですが、GET パラメータとルート構文で受け取る文字列の型が違うっぽいような振る舞いになることを確認しました。よくはまるところですね。URL エンコードされたパラメータの扱いがどうなるのかエラーになってよく分かりませんでした(> <)

# -*- coding: utf-8 -*-
import bobo

@bobo.query("/route/:path?", "GET")
def route_test(param=unicode("指定なし", "utf-8"), path="myapp"):
    s = unicode("GET パラメータは %s (%s 型) ですが<br />"
                "route 構文のパスは %s (%s 型)です。", "utf-8")
    s = s % (param, type(param).__name__,
             unicode(path, "utf-8"), type(path).__name__)
    return s

以下の URL にアクセスします。

http://localhost:8080/route/アプリ?param=test

ブラウザの出力です。

GET パラメータは test (unicode 型) ですが
route 構文のパスは アプリ (str 型)です。
サブルート

ルートの仕組みを利用して、もう少し高度な機能も用意されています。subroute というデコレータがあり、クラス(呼び出し可能オブジェクト)をデコレートします。クラスデコレータは Python 2.6 から使用できます *6。subroute デコレータを使用すると、URL に対するレスポンスをあらかじめ対応付けるのではなく、リクエストされた時に URL のマッチング処理(階層構造の解析)を経て、レスポンスを返すことができます。ドキュメントによると「より巧みな抽象化、柔軟なリソースへのアクセスといった機能を提供する」ものだそうです。

クラスデコレータとか使ったことないな、先ずはサンプルコードです。

# -*- coding: utf-8 -*-
import bobo

USER_TABLE = {
    "1": dict(name="t2y", hobby=("programming", ("0", "2"))),
}

PROG_LANGUAGE_TABLE = {
    "0": "Python",
    "1": "PHP",
    "2": "Bash",
    "3": "C",
    "4": "PL/SQL",
}

HOBBY_TABLE = {
    "programming": PROG_LANGUAGE_TABLE,
}

@bobo.subroute("/user/:user_id", scan=True)
class User(object):

    def __init__(self, request, user_id):
        self.user_id = user_id
        self.data = USER_TABLE[user_id]

    @bobo.query("/")
    @bobo.query("/index.html")
    def index(self):
        return """
        id: %s
        name: %s
        See my <a href="hobby/">hobby</a>.
        """ % (self.user_id, self.data["name"])

    @bobo.subroute
    def hobby(self, request):
        return Hobby(self.data["hobby"])

@bobo.scan_class
class Hobby(object):

    def __init__(self, data):
        self.hobby = HOBBY_TABLE[data[0]]
        self.data = data[1]

    @bobo.query("/")
    def index(self):
        return "\n".join("<li><a href=%s>%s<a></li>" % (
            key, self.hobby.get(key)) for key in self.hobby)

    @bobo.subroute("/:item_id")
    def subitem(self, request, item_id):
        return ProgrammingLanguage(self.data, item_id)

@bobo.scan_class
class ProgrammingLanguage(object):

    def __init__(self, data, item_id):
        self.data = data 
        self.item_id = item_id

    @bobo.query("")
    def get(self):
        text = "I don't like %s programming"
        if self.item_id in self.data:
            text = text.replace("don't", "")
        return text % (PROG_LANGUAGE_TABLE.get(self.item_id))

凄いです、、、。よくこういうの考えるなーとつくづく感心します。

例えば、以下の URL へアクセスします。

http://localhost:8080/user/1/hobby/0

ブラウザの出力です。

I like Python programing

scan_class クラスデコレータはクラス内のルートを調べるために使用します。URL のマッチング処理は次の順番で行われ、適切な処理が呼び出されます。

  1. "/user/1" に対する "/user/:user_id" がマッチして User クラスが見つかる
  2. ".../hobby" は User クラス内の hobby() メソッドにマッチして Hobby クラスが呼び出される
  3. ".../0" は Hobby クラスの "/:item_id" にマッチして ProgrammingLanguage クラスが呼び出される
  4. 最終的に ProgrammingLanguage クラスの空のパスに対するメソッド get() が呼ばれます

URL の階層構造がパッと見では分からないというデメリットがありますが、URL の階層と機能を分離できるので、うまく設計するとメンテナンス性が高くなりそうな気がします。

ちなみに以下のように scan_class クラスデコレータと subroute クラスデコレータのキーワードに scan=True をセットするのは同じようです。

@bobo.scan_class
class A(object):
    ...

@bobo.subroute(scan=True)
class A(object):
    ...

bobo 応用編

何かちょっとしたアプリを自作しようという構想はあったのですが、サブルートの仕組みがよくできてるなーと感心しているうちに時間がなくなってしまいました(> <)、bobo のサイトに アプリケーションサンプル があるのでそちらを参考にしてください。

まとめ

要するに bobo ドキュメントチュートリアルをざっくり「紹介しました」って記事になってしまいました。これを機に bobo という Web フレームワークの存在を知って、いつか役に立てばいいなと思います。とっても簡単ですしね。次は blockdiag が素敵な感じの @ へバトンを渡します。

*1:gihyo.jp さんの 本日12月1日より,プログラマ有志による技術系Advent Calendarが各所ではじまる で他のコミュニティで行われているアドベントカレンダーが紹介されています。今、数えてみたら33個もあります

*2:Python Advent Calendar jp 2010

*3:こちらから発表資料がダウンロードできます: OSC2009 Tokyo/Fallで発表しました

*4:bobo は HTTP リクエスト/レスポンスオブジェクトを生成するために WebOb を使用(依存)するようです。

*5:Shibu's Diary: 引数のカッコが省略可能なデコレータの実装方法 がとても分かり易いです

*6:PEP3129 クラスデコレータ