taisablog

taisa's engineer blog

Python

Python Bottleのソースを読む リクエスト・レスポンス編

投稿日:


Pythonの軽量WebフレームワークBottleのソースを読む リクエスト・レスポンス編
 

Class Hierarchy

ここの部分
20160213010717

リクエスト受付からレスポンスまで

前回、サーバの立ち上げ時にルーティングが読み込まれるところまでを確認したので今回はリクエスト受付からレスポンスを返すまでを見てみる。

まずリクエストが来るとwsgiref/handlers.pyのrunが呼ばれ、Bottleの__call__が呼び出される
wsgiref/handlers.py

self.setup_environ()
# リクエストを処理しレスポンスを取得する
self.result = application(self.environ, self.start_response)
# レスポンスを返す
self.finish_response()

bottle.py

def __call__(self, environ, start_response):
    """ Each instance of :class:'Bottle' is a WSGI application. """
    return self.wsgi(environ, start_response)

リクエストを処理する際に、LocalRequestのbindを呼び出しBaseRequestのコンストラクタにてenvironとLocalRequestをセットする。これでBaseRequestのラッパーからパラメータを取得可能になる。レスポンスも同様にLocalResponseのbindを呼び出す。

try:
    out = None
    environ['bottle.app'] = self
    request.bind(environ)
    response.bind()
    try:
        self.trigger_hook('before_request')
    except HTTPResponse:
        return  _e()
    # ここでリクエストの主処理を実行する
    out = _inner_handle()
    return out;
finally:
    if isinstance(out, HTTPResponse):
        out.apply(response)
    self.trigger_hook('after_request')

BaseRequestのコンストラクタ

self.environ = {} if environ is None else environ
self.environ['bottle.request'] = self

リクエスト情報は以下のように取得可能になる

def index():
    request.forms.get('test')
    return 'TOP'

LocalRequestは以下の通りマルチスレッドに対応している

#: A thread-safe instance of :class:`LocalRequest`. If accessed from within a
#: request callback, this instance always refers to the *current* request
#: (even on a multi-threaded server).

hookポイントで以下のようにbefore_requestとafter_requestが利用可能

@hook('before_request')
def before_request():
    request.session = 'session'
@hook('after_request')
def after_request():
    print 'test'

_handleの_inner_handle()にてrouteのactionを呼び出しresponseを取得する

def _inner_handle():
    # Maybe pass variables as locals for better performance?
    try:
        route, args = self.router.match(environ)
        environ['route.handle'] = route
        environ['bottle.route'] = route
        environ['route.url_args'] = args
        return route.call(**args)

リダイレクトは以下のメソッドが用意されている

def redirect(url, code=None):
    """ Aborts execution and causes a 303 or 302 redirect, depending on
        the HTTP protocol version. """
    if not code:
        code = 303 if request.get('SERVER_PROTOCOL') == "HTTP/1.1" else 302
    res = response.copy(cls=HTTPResponse)
    res.status = code
    res.body = ""
    res.set_header('Location', urljoin(request.url, url))
    raise res

-Python
-,

執筆者:

関連記事

Python Bottleのソースを読む 起動編

Pythonの軽量WebフレームワークBottleのソースを読む 起動編 Bottleとは Bottle is a fast, simple and lightweight WSGI micro web-framework for Python. It is distributed as a single file module and has no dependencies other than the Python Standard Library. Pythonの軽量Webフレームワークで、特徴はシンプルで早く、Pythonの標準ライブラリにも依存していないWebフレームワークであることとフレームワーク本体が1ファイルで構成されていることである Class Hierarchy Doxygenを使って出力した図Bottleは1ファイルながら中でそれぞれのクラス、主にServer、Templateが継承関係にあるのがわかる(コード量は4000行位)。ServerやTemplateクラスはたくさんあるが実際はその中のどれかを選択して利用する形となる 起動 Bottleの起動はrun()を呼び出す方法とコマンドラインインターフェースを使う方法が用意されている run()を使う方法 以下のように記載し起動することでサーバが立ち上がる from bottle import run, route @route(‘/’) def index(): return ‘Hello World’ run(host=’localhost’, port=8000, debug=True) コマンドラインインターフェースを使う場合 以下のコマンドで起動可能 # コントローラを指定 python -m bottle ‘package.controller’ # 説明は省略するが明示的にアプリを指定することも可能 python -m bottle ‘package.controller:app’ controller.py # runは不要 from bottle import ,route @route(‘/’) def index(): return ‘Hello World’ 起動処理を確認する コマンドラインインターフェースを使う場合 mainが2箇所あるが、これはサーバアダプダに必要なライブラリを必要としているからで、1つ目のmainでまずサーバアダプタに必要なライブラリを読み込み2つ目のmainでサーバが起動される仕組みになっている run()でサーバを起動する run()では、渡された引数の値をそれぞれ読み込んだあと最後にServerAdapterのrun()を呼び出している。Bottleでは多くのサーバをServerAdapterを継承することでサポートしており、指定されたサーバを起動するようになっている。指定しない場合はwsgirefがデフォルトで呼ばれる。また、appパラメータは特別指定しなければ、内部で自動的にdefaultが使われるので特別指定する必要はない。 サーバをロードするまで処理を追ってみる まず、Bottleでサポートしているサーバは以下のように宣言してある。 server_names = { ‘cgi’: CGIServer, ‘flup’: FlupFCGIServer, ‘wsgiref’: WSGIRefServer, ‘waitress’: WaitressServer, …

Python Bottleのソースを読む ルータ編

Pythonの軽量WebフレームワークBottleのソースを読む ルータ編 Class Hierarchy ここの部分 Bottleのルータについて bottleのルータは特に継承関係はなくRouteとRouterクラスで構成されている Routerの役割 A Router is an ordered collection of route->target pairs. It is used to efficiently match WSGI requests against a number of routes and return the first target that satisfies the request. The target may be anything, usually a string, ID or callable object. A route consists of a path-rule and a HTTP method.The path-rule is either a static path (e.g. `/contact`) or a dynamic path that contains wildcards (e.g. `/wiki/`). The wildcard syntax and details on the matching order are described in docs:`routing`. RouterはRoute情報のコレクションを保持する。 Routeの役割 This class wraps a route callback along …

Python Bottleのソースを読む テンプレート編

前回に続きPythonの軽量WebフレームワークBottleのソースを読む テンプレート編 Class Hierarchy ここの部分 Bottleのテンプレートについて Bottleのテンプレートは、Simple、Cheetah、Jinja2、Makoの4種類があり、BaseTemplateを継承しているPythonのテンプレートの種類(参考)http://www.cmscom.jp/blog/af0ga8 テンプレートの使い方 テンプレートの拡張子には以下が利用できる extensions = [‘tpl’, ‘html’, ‘thtml’, ‘stpl’] 例えばjinja2を使う場合は以下のように呼び出すことができる(jinja2をpip installする必要あり) @route(‘/’) def jinja2(): name = ‘text’ return jinja2_template(‘jinja2’, name=name) view {{ name }} テンプレート呼び出しの処理をみてみる 各テンプレートは以下のように設定されている。 functools.partialにfunc templateが渡されている mako_template = functools.partial(template, template_adapter=MakoTemplate) cheetah_template = functools.partial(template, template_adapter=CheetahTemplate) jinja2_template = functools.partial(template, template_adapter=Jinja2Template) jinja2_templateが呼ばれた時の処理 *argsにはjinja2が、**kwargsにはtemplate_adapter=Jinja2Templateが渡される def template(*args, **kwargs): 引数からjinja2テンプレート名を取得 tpl = args[0] if args else None Jinja2Templateを取得 adapter = kwargs.pop(‘template_adapter’, SimpleTemplate) テンプレートパスをlookup(パスは./views/か./) lookup = kwargs.pop(‘template_lookup’, TEMPLATE_PATH) Jinja2Templateをインスタンス化 TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings) BaseTemplateのコンストラクタの最後のprepareでJinja2Templateのprepareが呼ばれる self.prepare(**self.settings) ここではじめてjinja2がimportされる from jinja2 import Environment, FunctionLoader テンプレートのrenderメソッドにてレンダリングしviewを返す return TEMPLATES[tplid].render(kwargs) BottleのテンプレートはBaseTemplateを継承しprepareとrenderを実装することで使える仕組みになっている def prepare(self, **options): “”” Run preparations (parsing, caching, …). It should be possible …

Python Bottleのソースを読む プラグイン編

Pythonの軽量WebフレームワークBottleのソースを読む プラグイン編Bottleを触ってみると通常のWebフレームワークには用意されているであろう機能がなかったりします。これはマイクロフレームワークであるが故であり、すべてがそろってない状態がむしろ正しい状態と言えます。Bottleではそういったものを補うためにプラグインが用意されていてある程度の機能はそちらでまかなうことができます。また、Plugin Development Guide を参考にしてプラグインを自作することも可能です。 Class Hierarchy plugin用クラスはなくインターフェースが定義されているのでそれにしたがって実装します。 プラグインの使い方 公式ドキュメントに簡単な使い方が乗っているのでこちらを参考にすれば簡単に導入することができます。以下がサンプルコードで、簡単な流れとしてはinstall()で任意のプラグインをインストールするリクエスト時にプラグイン実行となります。サンプルコードでは、プラグインでkwargsにdbをセットされている為ルートのアクションでdb変数が利用できるようになってます。 from bottle import route, install, template from bottle_sqlite import SQLitePlugin install(SQLitePlugin(dbfile=’/tmp/test.db’)) @route(‘/show/<post_id:int>’) def show(db, post_id): c = db.execute(‘SELECT title, content FROM posts WHERE id = ?’, (post_id,)) row = c.fetchone() return template(‘show_post’, title=row[‘title’], text=row[‘content’]) @route(‘/contact’) def contact_page(): ”’ This callback does not need a db connection. Because the ‘db’ keyword argument is missing, the sqlite plugin ignores this callback completely. ”’ return template(‘contact’) プラグインの作り方 プラグインの作り方も公式ドキュメントにもっともシンプルな形のサンプルがあります。これは実行速度をレスポンスヘッダーにつけて返す処理ですが、サーバ起動時にstopwatchをインストールし、リクエストが来た際にデコレータを実行(表現あってるか分からない)することでリクエストの処理時間は計測できるようになっています。 from bottle import response, install, route import time def stopwatch(callback): def wrapper(*args, **kwargs): start = time.time() body = callback(*args, **kwargs) end = time.time() response.headers[‘X-Exec-Time’] = …

Python Bottleのソースを読む ユニットテスト編

Python Bottleのソースを読む テスト編前回までで一通りメインの機能は確認できました。ではいよいよコード追加してプルリクを投げましょうというところですが、テストは?ということでユニットテストとカバレッジ状況をチェックしてみます。また自作アプリに対してのテスト方法もチェックしてみます。 Bottleのユニットテストとカバレッジ状況 GitHub上のBottleをクローンすると直下にtestディレクトリがあります。ここにずらーっとテストがありますが、よくよく見てみるとtestall.pyという全実行のエントリポイントが用意されているのでそれを実行します。 # noseを入れてない場合はインストール pip install nose # こんな感じに実行 nosetests –with-coverage –cover-html testall.py でました。bottle.pyだけを見てみると76%しっかりテストが書かれてるのがわかります。 bottle.py 2277 485 849 79 76% そしてhtmlをチェックコードのカバレッジ状況が一目瞭然。あとは足りないとこテストしたり自分で処理追加してテストしたり。これでいろいろ安心。 BottleでつくったWebアプリのテスト方法 せっかくなのでWebアプリのテスト方法も見てみます。これは公式ドキュメントにしっかり書かれているのでそこをみればだいたい分かると思います。noseインストール pip install nose テスト対象コード import bottle @bottle.route(‘/’) def index(): return ‘Hi!’ if __name__ == ‘__main__’: bottle.run() テストコード単純にindex()を実行して結果をassertする import mywebapp def test_webapp_index(): assert mywebapp.index() == ‘Hi!’ テスト実行 nosetests test_app.py Ran 1 test in 0.014s OK 上記は直接関数をテストする方法ですがwebtestを使ってwebベースでアクセスしてテストする方法もあります。ただこの方法をやってみたところ明示的 app = Bottle()を宣言しないとうまく実行できませんでした。ここら辺はもう少しwebtestの使い方がわかればなんとかなるかもしれません。webtestインストール pip install webtest テスト対象コード from bottle import Bottle # アプリを明示的に作成 app = Bottle() @app.route(‘/’) def index(): return ‘Hi!’ if __name__ == ‘__main__’: run(app=app) テストコード公式ドキュメントではログイン・ログアウトのテストが書かれてましたがここでは省略 from webtest import TestApp import mywebapp def test_index(): app = TestApp(mywebapp.app) assert …