Posted on

FlaskをBottleと比較した雑感

以前Bottleのソースをチェックしてみた流れでFlaskも見てみた。結論から言うとBottleと大して変わらん。もちろんFlaskのほうがコード量が多く多少リッチではあるもののざっくり機能ベースで言うと大して変わらんのです。
そもそも両方ともマイクロWebフレームワークが売りなので当たり前といえば当たり前ですが、歴史的にもBottleが2009年リリースでFlaskが2010年4月1日(エイプリルフールのネタ
として)リリースと、名前もBottleに対抗してFlask(フラスコ)という名前をつけたということなので、もともとがBottleっぽいフレームワークを遊びで作ってみたという感じなんだと思います。それっぽいことはwikiに書かれています。

  • [Bottle Wiki](https://en.wikipedia.org/wiki/Bottle_(web_framework)
  • Flask Wiki

クラス構成に関しては以下にある通りで、Bottleが1ファイル構成でFlaskは機能にファイルが分かれてる構成という違いはあるものの、機能自体にほとんど違いはありません。ただFlaskはBlueprintという大規模アプリ対応の機能があります。
20160628103203
なお、使い方については公式ドキュメントとサンプルコードも複数用意されているのでアプリの構成や使い方が確認しやすいです。

Posted on

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

Python Bottleのソースを読む テスト編
前回までで一通りメインの機能は確認できました。ではいよいよコード追加してプルリクを投げましょうというところですが、テストは?ということでユニットテストとカバレッジ状況をチェックしてみます。また自作アプリに対してのテスト方法もチェックしてみます。

Bottleのユニットテストとカバレッジ状況

GitHub上のBottleをクローンすると直下にtestディレクトリがあります。ここにずらーっとテストがありますが、よくよく見てみるとtestall.pyという全実行のエントリポイントが用意されているのでそれを実行します。

# noseを入れてない場合はインストール
pip install nose
# こんな感じに実行
nosetests --with-coverage --cover-html testall.py

でました。
20160623211937
bottle.pyだけを見てみると
76%
しっかりテストが書かれてるのがわかります。

bottle.py                       2277    485    849     79    76%

そしてhtmlをチェック
20160623211742
コードのカバレッジ状況が一目瞭然。あとは足りないとこテストしたり自分で処理追加してテストしたり。これでいろいろ安心。

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 app.get('/').status == '200 OK'

テスト実行

nosetests test_app.py
Ran 2 tests in 0.003s
OK

これでいろいろ安心

Posted on

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

Pythonの軽量WebフレームワークBottleのソースを読む ルータ編
 

Class Hierarchy

ここの部分
20160208112338
 

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 with route specific metadata and configuration and applies Plugins on demand. It is also responsible for turing an URL path rule into a regular expression usable by the Router.

Routeはrouteに対するcallback関数やroute特有の情報を保持するオブジェクト。RouteオブジェクトをRouterが一覧で保持することになる。

ルータの使い方

ルータは以下のようにデコレータを指定する。@getや@postはそれぞれGETのみPOSTのみを受け付ける

@route('/routes')
def routes():
    return 'routes'
@get('/get')
def get():
    return 'get'
@post('/post')
def post():
    return 'post'

デコレータには以下が用意されている

route     = make_default_app_wrapper('route')
get       = make_default_app_wrapper('get')
post      = make_default_app_wrapper('post')
put       = make_default_app_wrapper('put')
delete    = make_default_app_wrapper('delete')
patch     = make_default_app_wrapper('patch')
error     = make_default_app_wrapper('error')
mount     = make_default_app_wrapper('mount')
hook      = make_default_app_wrapper('hook')
install   = make_default_app_wrapper('install')
uninstall = make_default_app_wrapper('uninstall')
url       = make_default_app_wrapper('get_url')

ルータの処理

まず起動時のBottleインスタンス化時(default_appが生成されAppStackにpushされる)に、コンストラクタにてRouterがセットされる。

app = default_app = AppStack()
app.push()

コントローラのデコレータを読み込む

@get('/get')
def get():
    return 'get'

make_default_app_wrapperが呼ばれBottleのdef getが呼ばれる

def make_default_app_wrapper(name):
    """ Return a callable that relays calls to the current default app. """
    @functools.wraps(getattr(Bottle, name))
    def wrapper(*a, **ka):
        return getattr(app(), name)(*a, **ka)
    return wrapper

def getなどはrouteを少しラップしたもの

def get(self, path=None, method='GET', **options):
    """ Equals :meth:`route`. """
    return self.route(path, method, **options)
def post(self, path=None, method='POST', **options):
    """ Equals :meth:`route` with a ``POST`` method parameter. """
    return self.route(path, method, **options)
def put(self, path=None, method='PUT', **options):
    """ Equals :meth:`route` with a ``PUT`` method parameter. """
    return self.route(path, method, **options)
def delete(self, path=None, method='DELETE', **options):
    """ Equals :meth:`route` with a ``DELETE`` method parameter. """
    return self.route(path, method, **options)
def patch(self, path=None, method='PATCH', **options):
    """ Equals :meth:`route` with a ``PATCH`` method parameter. """
    return self.route(path, method, **options)

routeのdecoratorでRouteをインスタンス化(loadでcallback functionを取得しruleとセットで渡す)しBottleのroutesとRouterにrouteを追加する。これらを繰り返しrouteをすべて読み込む。これでpathから特定のactionを呼び出すことができるようになる。

def decorator(callback):
    if isinstance(callback, basestring): callback = load(callback)
    for rule in makelist(path) or yieldroutes(callback):
        for verb in makelist(method):
            verb = verb.upper()
            route = Route(self, rule, verb, callback,
                          name=name,
                          plugins=plugins,
                          skiplist=skiplist, **config)
            self.add_route(route)
    return callback
Posted on

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'] = str(end - start)
        return body
    return wrapper
install(stopwatch)
@route('/')
def index():
    return 'INDEX'

プラグインのinstall()処理をチェック

プラグインはインターフェースが定義されているのでそれに従って書く必要がありますが、setup()で事前準備をして、apply()で実際にプラグインの処理を実行するといった流れになります。実際にはstopwatch()のように関数だけを実装して渡すことも可能です。

def install(self, plugin):
    """ Add a plugin to the list of plugins and prepare it for being
        applied to all routes of this application. A plugin may be a simple
        decorator or an object that implements the :class:`Plugin` API.
    """
    if hasattr(plugin, 'setup'): plugin.setup(self)
    if not callable(plugin) and not hasattr(plugin, 'apply'):
        raise TypeError("Plugins must be callable or implement .apply()")
    self.plugins.append(plugin)
    self.reset()
    return plugin

プラグイン組み合わせて使うことでスムーズにBottleを使ったWeb開発が進められるようになります。ここまでで主要機能は一通りみることができたので、次回はテストをチェックしてみます。

Posted on

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
Posted on

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

前回に続きPythonの軽量WebフレームワークBottleのソースを読む テンプレート編
 

Class Hierarchy

ここの部分
20160206230621
 

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 to call this again to refresh a template or to
update settings.
"""
raise NotImplementedError
def render(self, *args, **kwargs):
""" Render the template with the specified local variables and return
a single byte or unicode string. If it is a byte string, the encoding
must match self.encoding. This method must be thread-safe!
Local variables may be provided in dictionaries (args)
or directly, as keywords (kwargs).
"""
raise NotImplementedError
Posted on

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を使って出力した図
20160204171644
20160204171658
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,
    'cherrypy': CherryPyServer,
    'paste': PasteServer,
    'fapws3': FapwsServer,
    'tornado': TornadoServer,
    'gae': AppEngineServer,
    'twisted': TwistedServer,
    'diesel': DieselServer,
    'meinheld': MeinheldServer,
    'gunicorn': GunicornServer,
    'eventlet': EventletServer,
    'gevent': GeventServer,
    'geventSocketIO': GeventSocketIOServer,
    'rocket': RocketServer,
    'bjoern': BjoernServer,
    'aiohttp': AiohttpServer,
    'auto': AutoServer,
}

サーバを指定するには、run()実行時にserverとして必要な値を文字列で指定して渡す。以下に記載の通りデフォルトでは’wsgiref’が指定されている。

def run(app=None,
        server='wsgiref',
        host='127.0.0.1',
        port=8080,
        interval=1,
        reloader=False,
        quiet=False,
        plugins=None,
        debug=None,
        config=None, **kargs):

このあたりでサーバを取得し設定しておりServerAdapterを継承していない値が渡された場合はサポート外としてExceptionを投げる

if server in server_names:
    server = server_names.get(server)
if isinstance(server, basestring):
    server = load(server)
if isinstance(server, type):
    server = server(host=host, port=port, **kargs)
if not isinstance(server, ServerAdapter):
    raise ValueError("Unknown or unsupported server: %r" % server)

WSGIRefServerなどは標準エラーを出力する設定になっている

server.quiet = server.quiet or quiet
if not server.quiet:
    _stderr("Bottle v%s server starting up (using %s)...\n" %
            (__version__, repr(server)))
    _stderr("Listening on http://%s:%d/\n" %
            (server.host, server.port))
    _stderr("Hit Ctrl-C to quit.\n\n")

reloaderをTrueにした場合はFileCheckerThread(ファイルの変更をチェックして変更があったら自動でリロードする)を立ち上げて起動する

if reloader:
    lockfile = os.environ.get('BOTTLE_LOCKFILE')
    bgcheck = FileCheckerThread(lockfile, interval)
    with bgcheck:
        server.run(app)
    if bgcheck.status == 'reload':
        sys.exit(3)
else:
    server.run(app)

簡単に起動までの流れをおってみたが、細かく見てみると他にも起動オプションがあるので必要に応じて設定するとよい
ざっと見てみたところBottleは1ファイルで構成されているので中身を確認しやすくPython初級者の私にとってはとっつきやすいフレームワークに思う。起動自体も1ファイルだけでできるので開発も簡単に始められる。ただその分逆にどのようにアプリケーションとしてファイルを構成していけばいいかが分かりにくいというデメリットもあるのではと感じた。最終的には自分なりに構成していけばいいのだがその辺はまた追って確認していこうと思う。