Another Do-It-Yourself Framework

Introduction and Audience

私が このチュートリアルの初版 [日本語訳 ] を書いてから2年以上が経ちました。 私はその間にできたツールで別のやり方を与えることにしました。(特に WebOb で).

時々Pythonはwebフレームワークが多すぎることを責められます。 確かに多いです。 ですから、フレームワークを書くことは実用的な練習であると思います。 フレームワークの理解なしで、多くのことを超えることはできません。 フレームワークの理解は魔法を取り除きます。 他の今あるフレームワークを使う際に(そうすることをおすすめします)、自分でフレームワークを書くとより理解することができるでしょう。

このチュートリアルはWSGIとWebObを使ったオレオレwebフレームワークの作り方を紹介します。他のライブラリは使いません。

長いセクションでも例に従い、行単位でトリッキーな箇所を説明します。

What Is WSGI?

最も単純に言えば、WSGIとは、WebサーバーとWebアプリケーション間のインターフェイスです。 以下で説明するのは、WSGIの構造についてです。 上位のレベルから見れば、WSGIはwebリクエストを非常に公式な方法で、コードに渡します。 単純な要約です、さらに言うならば – WSGIはリクエストに注釈やメタデータを付加します。

より厳密に言えば、WSGIは アプリケーションサーバー で構成されています。 アプリケーションはリクエストを受け取り、レスポンスを起こす関数です。 サーバーはアプリケーション関数を呼ぶものです。

単純なアプリケーションはこのようになります。:

>>> def application(environ, start_response):
...     start_response('200 OK', [('Content-Type', 'text/html')])
...     return ['Hello World!']

引数 environ はCGIリクエストの環境変数の値を持つ辞書です。 例えば、ヘッダー Host:environ['HTTP_HOST'] に入っています。 PATHは``environ[‘SCRIPT_NAME’]`` (アプリケーションに通じるPATH) と environ['PATH_INFO'] (保持されているアプリケーションが受け取るべきPATH)に入っています。

サーバーにはあまり焦点を合わせず、アプリケーションを扱うのにWebObを使います。 WebObにはいくらか簡単なサーバーインターフェースがあります。 WebObを使って req = webob.Request('http://localhost/test') で新しいリクエストを作成し、 resp = req.get_response(app) でアプリケーションを呼びます。

例えば

>>> from webob import Request
>>> req = Request.blank('http://localhost/test')
>>> resp = req.get_response(application)
>>> print resp
200 OK
Content-Type: text/html

Hello World!

これはアプリケーションを検査する簡単な方法です、私たちが作成しているフレームワークをテストするのに使用します。

About WebOb

WebObはリクエストとレスポンスのオブジェクトを作成するライブラリです。WSGIモデルを中心に置きました。リクエストは環境周りのラッパーです。例えば:

>>> req = Request.blank('http://localhost/test')
>>> req.environ['HTTP_HOST']
'localhost:80'
>>> req.host
'localhost:80'
>>> req.path_info
'/test'

レスポンスは...えーと、レスポンスを表すオブジェクトです。状態、ヘッダー、本体について:

>>> from webob import Response
>>> resp = Response(body='Hello World!')
>>> resp.content_type
'text/html'
>>> resp.content_type = 'text/plain'
>>> print resp
200 OK
Content-Length: 12
Content-Type: text/plain; charset=UTF-8

Hello World!

レスポンスはまたWSGIアプリケーションを返します。 つまり resp(environ, start_response) を呼ぶことが可能です。 もちろん、標準のWSGIアプリケーションよりもあまり 動的 ではありません。

これらの2つの断片がフレームワークを作る多くの、より退屈な部品を解決します。 ほとんどのHTTPヘッダの構文解析、有効なレスポンスの作成、多くのユニコード問題を取扱います。

Serving Your Application

WebObを使ってアプリケーションのテストする間、アプリケーションを提供したいかもしれません。 Paste HTTPサーバーを使う基本的なやり方です。:

if __name__ == '__main__':
    from paste import httpserver
    httpserver.serve(app, host='127.0.0.1', port=8080)

また、標準ライブラリから wsgiref 使うことができます。 シングルスレッドですので、テストするにはほぼ適切です。

if __name__ == '__main__':
    from wsgiref.simple_server import make_server
    server = make_server('127.0.0.1', 8080, app)
    server.serve_forever()

Making A Framework

私たちのフレームワークに取り組み始める必要があります。

ここに私たちが作成する基本モデルがあります。

  • コントローラを示すルートを定義する
  • コントローラを作成する単純なフレームワークを作成する

Routing

私たちは多くのパスに(ドメインを除いた)URIテンプレートを使う明らかなルートを使います。 {名前:正規表現} という表現を使うことができる少しの拡張を加えます。 命名されたセグメントがその正規表現に合わなければいけません。 マッチは”モジュール名:関数名”で表現される「コントローラ」変数を含むでしょう。 例として単純なブログを使います。

ルートっぽいものを示します。

app = Router()
app.add_route('/', controller='controllers:index')
app.add_route('/{year:\d\d\d\d}/',
              controller='controllers:archive')
app.add_route('/{year:\d\d\d\d}/{month:\d\d}/',
              controller='controllers:archive')
app.add_route('/{year:\d\d\d\d}/{month:\d\d}/{slug}',
              controller='controllers:view')
app.add_route('/post', controller='controllers:post')

こうするためには、2、3必要です。

  • URIテンプレートにマッチする
  • コントローラを読み込む
  • 一緒に適用するオブジェクト(ルート)

Routing: Templates

マッチングするために、テンプレートを正規表現にコンパイルします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
 >>> import re
 >>> var_regex = re.compile(r'''
 ...     \{          # The exact character "{"
 ...     (\w+)       # The variable name (restricted to a-z, 0-9, _)
 ...     (?::([^}]+))? # The optional :regex part
 ...     \}          # The exact character "}"
 ...     ''', re.VERBOSE)
 >>> def template_to_regex(template):
 ...     regex = ''
 ...     last_pos = 0
 ...     for match in var_regex.finditer(template):
 ...         regex += re.escape(template[last_pos:match.start()])
 ...         var_name = match.group(1)
 ...         expr = match.group(2) or '[^/]+'
 ...         expr = '(?P<%s>%s)' % (var_name, expr)
 ...         regex += expr
 ...         last_pos = match.end()
 ...     regex += re.escape(template[last_pos:])
 ...     regex = '^%s$' % regex
 ...     return regex

2行目: 正規表現を作成します。 re.VERBOSE フラグは正規表現パーサからスペースを取り除き、コメントを許し、そうすることでラインノイズを除くことができます。 これはいろいろな値にマッチします。i.e.、 {var:regex} (:regex はオプションです) 。 キャプチャする2つのグループがあることに注意してください:match.group(1) は変数名で、 match.group(2) は正規表現です(正規表現がない場合はNone)。 (?:...)? は、セクションが任意であることを意味することに注意してください。

10行目:この変数は作成している正規表現を保持します。

11行目:最後にマッチした終わりの位置を含んでいます。

12行目: finditer メソッドはすべてのマッチをもたらします。

13行目: 始まりから最後のマッチまでの {} ではないテキストをすべて得ます。 特別な意味を持っている文字をエスケープするのに re.escape を呼びます。 .html\.html にエスケープされます。

14行目: 最初に一致する変数名です。

15行目: expr は一致する、または、任意の二番目に一致する正規表現です。通常は [^/]+ (これは空でない場合,/でない文字列にマッチします。) これは合理的なデフォルト値です。

16行目: 正規表現を作成します。 (?P<name>...) は命名された、分類された表現です。一致したときに、 match.groupdict() で見ることができ、名前と値を得られます。

17,18行目: 完全な正規表現に式を加えて、最後の位置を保持します。

19行目: 残っている可変でないテキストを正規表現に加えます。

そして、完全な文字列に一致する正規表現を作成します。 (^ は先頭にマッチし、 $ は最後にマッチします。)

テストするために、いくつかの変換を試みることができます。 直接 template_to_regex 機能のdocstringにこれらを入れることができ、 テスト用に doctest を使うことができます。 しかし、このドキュメント内でテスト用にdoctestを使っているので、doctest内でdocstring doctestを置くことはできません。しかしながら、テスト風のものを置きます。

>>> print template_to_regex('/a/static/path')
^\/a\/static\/path$
>>> print template_to_regex('/{year:\d\d\d\d}/{month:\d\d}/{slug}')
^\/(?P<year>\d\d\d\d)\/(?P<month>\d\d)\/(?P<slug>[^/]+)$

Routing: controller loading

コントローラを読み込むために、私たちはモジュールをimportしなければならず、そうすることで機能を取り出します。 モジュールをimportするために、 __import__ ビルトイン関数を使います。 __import__ の返り値は余り役に立ちません、しかし、それはモジュールを sys.modules (すべての荷を積んだモジュールの辞書)に入れます。

また、一部の人々は、文字列メソッド split がどう分割したか、知ることができません。 それは2つの引数を取ります。 1番目は分割するキャラクタです、そして、2番目は分割する最大数です。

>>> import sys
>>> def load_controller(string):
...     module_name, func_name = string.split(':', 1)
...     __import__(module_name)
...     module = sys.modules[module_name]
...     func = getattr(module, func_name)
...     return func

Routing: putting it together

Router クラス。 そのクラスは add_route__call__ メソッドを持つ。 __call__ はRouterオブジェクト自身をWSGIアプリケーションにします。 だからリクエストが来た際に、 PATH_INFO (req.path_info として知られている)を見て、そのパスに合っているコントローラに要求を渡します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
 >>> from webob import Request
 >>> from webob import exc
 >>> class Router(object):
 ...     def __init__(self):
 ...         self.routes = []
 ...
 ...     def add_route(self, template, controller, **vars):
 ...         if isinstance(controller, basestring):
 ...             controller = load_controller(controller)
 ...         self.routes.append((re.compile(template_to_regex(template)),
 ...                             controller,
 ...                             vars))
 ...
 ...     def __call__(self, environ, start_response):
 ...         req = Request(environ)
 ...         for regex, controller, vars in self.routes:
 ...             match = regex.match(req.path_info)
 ...             if match:
 ...                 req.urlvars = match.groupdict()
 ...                 req.urlvars.update(vars)
 ...                 return controller(environ, start_response)
 ...         return exc.HTTPNotFound()(environ, start_response)

5行目: 順序付きリストのルートオプションを保持します。 それぞれのアイテムは (regex,controller,vars) です。 regex は合致する正規表現オブジェクトで、 controller は実行するコントローラー、 vars は特別な (定) 変数です。

8,9行目: インポートされた文字列で add_route を呼んだり、コントローラーオブジェクトを呼ぶことを許します。 文字列をテストする際に、必要ならばimportします。

13行目: ここで __call__ メソッドを加えます。 関数のようにオブジェクトを呼ぶ際に使うメソッドです。 これがWSGIの特徴であると認めるべきです

14行目: リクエストオブジェクトを作成します。 この関数でのみリクエストオブジェクトを使います。 コントローラーがリクエストオブジェクトを必要とするならば、作成します。

16行目: req.path_info に対して正規表現をテストします。 environ['PATH_INFO'] と同じです。 すべてのリクエストパスは処理を残します。

18行目: 正規表現でマッチした辞書を req.urlvars を設定します。 この変数は実際に environ['wsgiorg.routing_args'] にマップされます。 リクエストのときに設定するどんな属性もどうかして環境辞書にマップするでしょう。 要求はそれ自身の状態を全く持ちません。

19行目: add_route() で通過されたどんな明白な変数も加えます。

20行目: WSGIアプリケーション自体としてコントローラを呼びます。 コントローラがしたいことを詰め込んだ手が込んだフレームワークでも 自分で呼ばなければなりません。

21行目: マッチしなければ、404 Not Found レスポンスを返します。 webob.exc.HTTPNotFound() は404 レスポンスを返すWSGIアプリケーションです。 webob.exc.HTTPNotFound('No route matched') のようにして、メッセージを追加することも出来ます。 もちろん、アプリケーションを呼びます。

Controllers

ルータはリクエストをコントローラに渡すので、コントローラはそれ自身がWSGIアプリケーションになります。 しかし、これらのアプリケーションを好意的に書くため、調整したくなるでしょう。

そのため、 デコレータ を書きます。 デコレータは、別の関数をラップする関数です。 デコレーションの後に、関数はWSGIアプリケーションになるでしょうが、それは controller_func(req, **urlvars) のような署名を関数にデコレートするでしょう。 コントローラ関数はレスポンスオブジェクト(覚えているでしょうが、これ自身もWSGIアプリケーションです)を返すでしょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 >>> from webob import Request, Response
 >>> from webob import exc
 >>> def controller(func):
 ...     def replacement(environ, start_response):
 ...         req = Request(environ)
 ...         try:
 ...             resp = func(req, **req.urlvars)
 ...         except exc.HTTPException, e:
 ...             resp = e
 ...         if isinstance(resp, basestring):
 ...             resp = Response(body=resp)
 ...         return resp(environ, start_response)
 ...     return replacement

3行目: これはデコレータのための典型的な署名です。 それは、引数として1つの関数を取り、ラップされた関数を返します。

4行目: これは私たちが返すつもりである交換関数です。 これはクロージャと呼ばれます。 この関数は func にアクセスし、新しい関数をデコレートするたびに、 func の値を持つ新しい交換関数ができます。 お分かりのように、これはWSGIアプリケーションです。

5行目: リクエストを作成します。

6行目: ここで webob.exc.HTTPException 例外を受け取ります。 関数内で raise webob.exc.HTTPNotFound() (Python 2.4 では raise webob.exc.HTTPNotFound().exception) 例外を受け取ることができます。 これらの例外はそれ自身がWSGIアプリケーションです。

7行目: リクエストオブジェクトと一緒に req.urlvars のいずれかの値で関数を呼びます。 そして、レスポンスを取り戻します。

line 10: We’ll allow the function to return a full response object, or just a string. If they return a string, we’ll create a Response object with that (and with the standard 200 OK status, text/html content type, and utf8 charset/encoding).

10行目: 完全なレスポンスオブジェクト、または、文字列を返す関数します。 文字列を返すならば、 Response オブジェクト(標準的な 200 OK 状態、 text/html コンテントタイプ と utf8 charset/encodingを持つ)を作成します。

12行目: リクエストのレスポンスに合格します。 ここでまたWSGIアプリケーションが発生します。 WSGIアプリケーションは空から落ちてきます。

13行目: 関数オブジェクト自体を返します。それは関すの代理をするでしょう。

このようにコントローラを使います。

>>> @controller
... def index(req):
...     return 'This is the index'

Putting It Together

Now we’ll show a basic application. Just a hello world application for now. Note that this document is the module __main__.

ここで、基本的なアプリケーションをお見せします。hello world アプリケーションです。 このドキュメントは __main__ モジュールであることを注意してください。

>>> @controller
... def hello(req):
...     if req.method == 'POST':
...         return 'Hello %s!' % req.params['name']
...     elif req.method == 'GET':
...         return '''<form method="POST">
...             You're name: <input type="text" name="name">
...             <input type="submit">
...             </form>'''
>>> hello_world = Router()
>>> hello_world.add_route('/', controller=hello)

アプリケーションをテストしてみましょう:

>>> req = Request.blank('/')
>>> resp = req.get_response(hello_world)
>>> print resp
200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 131

<form method="POST">
            You're name: <input type="text" name="name">
            <input type="submit">
            </form>
>>> req.method = 'POST'
>>> req.body = 'name=Ian'
>>> resp = req.get_response(hello_world)
>>> print resp
200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 10

Hello Ian!

Another Controller

コントローラに挑戦するための面白いかもしれない別のパターンがあります。 関数の代わりに、 get,``post’‘,etc の様なメソッドを持つクラスを作ることができます。 urlvars はクラスを例示することに使われるでしょう。

私たちはこのことをスーパークラスでできましたが、実装はラッパー(デコレータがラッパーであるように)によりエレガントになるでしょう。 Python 3.0では クラスデコレータ が追加されます。 このように動くことでしょう。

私たちは付加的な 動作 変数を許すつもりです。それは、メソッド(特に action_method , _method はリクエストメソッド )を定義するでしょう。 動作を全く与えないなら、私たちはまさしくメソッド (i.e., get, post , etc) を使用するつもりです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
 >>> def rest_controller(cls):
 ...     def replacement(environ, start_response):
 ...         req = Request(environ)
 ...         try:
 ...             instance = cls(req, **req.urlvars)
 ...             action = req.urlvars.get('action')
 ...             if action:
 ...                 action += '_' + req.method.lower()
 ...             else:
 ...                 action = req.method.lower()
 ...             try:
 ...                 method = getattr(instance, action)
 ...             except AttributeError:
 ...                 raise exc.HTTPNotFound("No action %s" % action)
 ...             resp = method()
 ...             if isinstance(resp, basestring):
 ...                 resp = Response(body=resp)
 ...         except exc.HTTPException, e:
 ...             resp = e
 ...         return resp(environ, start_response)
 ...     return replacement

1行目: ここで、私たちはクラスにちょっと飾り付けをしています。 しかし、本当に、WSGIアプリケーションラッパーを作成するつもりです。

2-4行目: WSGIアプリケーションをクロージャに置き換えます。そして、 そして、デコレータなどのように要求を作成して、例外を捕らえます。

5行目: 初期化するためにリクエストと req.urlvars の両方でクラスを説明します。 インスタンスは1つのリクエストにのみ使用されるでしょう。 ( インスタンス がスレッド・セーフである必要はないことに注意してください。)

6行目: actionがあれば、私たちは動作変数を出します。

7, 8行目: actionがあれば, メソッド名 {action}_{method} を使います。

8, 9行目: ... しかしながら、メソッド名に関するメソッドを使用します。

10-13行目 インスタンスからメソッドを得、メソッドがなければ404エラーを返します。

14行目: メソッドを呼び、レスポンスを得ます。

15,16行目: 応答が文字列であれば、それから完全な応答オブジェクトを作成します。

19行目: そして、私たちはリクエストを転送します...

20行目: ... そして、作成したラッパーオブジェクトを返します。

hello worldはこれです。

>>> class Hello(object):
...     def __init__(self, req):
...         self.request = req
...     def get(self):
...         return '''<form method="POST">
...             You're name: <input type="text" name="name">
...             <input type="submit">
...             </form>'''
...     def post(self):
...         return 'Hello %s!' % self.request.params['name']
>>> hello = rest_controller(Hello)

以前と同じテストを実行します:

>>> hello_world = Router()
>>> hello_world.add_route('/', controller=hello)
>>> req = Request.blank('/')
>>> resp = req.get_response(hello_world)
>>> print resp
200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 131

<form method="POST">
            You're name: <input type="text" name="name">
            <input type="submit">
            </form>
>>> req.method = 'POST'
>>> req.body = 'name=Ian'
>>> resp = req.get_response(hello_world)
>>> print resp
200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 10

Hello Ian!

URL Generation and Request Access

HTMLにハードコードされたリンクを使うことができますが、これには問題があります。 相対リンクは管理しにくいです、そして、絶対リンクはあなたのアプリケーションが特定の場所にあると推定します。 WSGIは変数 SCRIPT_NAME を与えます。それはこのアプリケーションに導いた経路の一部です。 ブログアプリケーションを書いて、例えば、誰かがそれを /blog/ にインストールしたければ、そのとき SCRIPT_NAME は "/blog/" となります。そのことを考慮に入れてリンクを作るべきです。

SCRIPT_NAMEを使用する基本的なURLは、 req.application_url です。 それで、リクエストにアクセスする手段を持っているなら、私たちはURLを作ることができます。 しかし、アクセスする手段を持たなければ、どうなるでしょうか?

どんな機能も正しい要求に近づく手段を得るのを簡単にするためにスレッドローカル変数を使用できます。 「スレッドローカル」な変数の値が各スレッドで別々に監視される変数であるので、 異なったスレッドに複数のリクエストがあれば、そのリクエストはお互いを上書きしないでしょう。

スレッド局所変数を使用する基本的な手段は”threading.local()”です。 これはスレッドローカルの属性をそれに割り当てることができる空のオブジェクトを作成します。 スレッドローカルの値を得る最も良い方法が関数と共にあるのがわかりました。 あるグローバルなオブジェクトを得ることと対照的に、これが、オブジェクトをとって来ていると断言します。

ローカルでの基本的な構成はこれです:

>>> import threading
>>> class Localized(object):
...     def __init__(self):
...         self.local = threading.local()
...     def register(self, object):
...         self.local.object = object
...     def unregister(self):
...         del self.local.object
...     def __call__(self):
...         try:
...             return self.local.object
...         except AttributeError:
...             raise TypeError("No object has been registered for this thread")
>>> get_request = Localized()

今、何らかの ミドルウェア が、リクエストオブジェクトを登録するのに必要です。 ミドルウェアはアプリケーションをラップし、入口か出口に関するリクエストを変更します。 ある意味 Router オブジェクトはミドルウェアでした。 もっとも一つのアプリケーションをラップしているわけではないので厳密には違いますが。

登録ミドルウェアはこのようになります。

>>> class RegisterRequest(object):
...     def __init__(self, app):
...         self.app = app
...     def __call__(self, environ, start_response):
...         req = Request(environ)
...         get_request.register(req)
...         try:
...             return self.app(environ, start_response)
...         finally:
...             get_request.unregister()

こうしてください。

>>> hello_world = RegisterRequest(hello_world)

リクエストはその都度、登録されます。 URL発生関数を作成してください:

>>> import urllib
>>> def url(*segments, **vars):
...     base_url = get_request().application_url
...     path = '/'.join(str(s) for s in segments)
...     if not path.startswith('/'):
...         path = '/' + path
...     if vars:
...         path += '?' + urllib.urlencode(vars)
...     return base_url + path

テストします。

>>> get_request.register(Request.blank('http://localhost/'))
>>> url('article', 1)
'http://localhost/article/1'
>>> url('search', q='some query')
'http://localhost/search?q=some+query'

Templating

さて、私たちは 本当に テンプレートをフレームワークの要素として考慮する必要はありません。 結局、あなたはコントローラから文字列を返します。 そして、テンプレートからレンダリングされた文字列を手に入れる方法を理解できます。

しかし、そうすることが賢いトリックを示すと思うので、小さなヘルパーを加えます。

テンプレートに Tempita を使います。 読み込むのが非常に簡単であるためです。 基本的なフォームはこうです。:

import tempita
template = tempita.HTMLTemplate.from_filename('some-file.html')

しかし、命名されたテンプレートをレンダリングする関数 render(template_name, **vars) を実行し、render() が呼ばれた位置を相対的パスとして扱います。 それがトリックです。

それをするのに、 sys._getframe を使用します。 それは、スコープの情報を見る方法です。 一般にこうすることは反対されますが、私はこの場合正当であると思います。

また、私たちはあなたにテンプレート名の代わりに例示されたテンプレートを通します。 他の容易にアクセス可能なファイルがないdoctestのような場所では、役に立ちます。

>>> import os
>>> import tempita
>>> def render(template, **vars):
...     if isinstance(template, basestring):
...         caller_location = sys._getframe(1).f_globals['__file__']
...         filename = os.path.join(os.path.dirname(caller_location), template)
...         template = tempita.HTMLTemplate.from_filename(filename)
...     vars.setdefault('request', get_request())
...     return template.substitute(vars)

Conclusion

フレームワークができました。 ジャジャーン!

もちろん、これはいくつかのことに扱いません。 特に:

  • 設定
  • ルートをデバッグ可能にする
  • 例外の取得と他の基本的なインフラストラクチャ
  • データベースへの接続
  • フォームの取り扱い
  • 認証

でも、当分、このドキュメントの範囲外とします。