Posted at

【Python】 Bottleのソースコードを読んでみる その2

この記事は前回の続きになっていますが、単独でも問題なく読めるはずです。

まあ、自分用の整理のための記事ですが...


本丸のBottleクラス

いよいよ天守閣のBottleクラス。

まずは前回からの流れで、__call__メソッドから。


bottle.py


class Bottle(object):

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

def wsgi(self, environ, start_response):
""" The bottle WSGI-interface. """
try:
out = self._cast(self._handle(environ))
# rfc2616 section 4.3
if response._status_code in (100, 101, 204, 304) or environ['REQUEST_METHOD'] == 'HEAD':
if hasattr(out, 'close'): out.close()
out = []
start_response(response._status_line, response.headerlist)
return out


つまりレスポンスのメッセージbody部分はself._cast(self._handle(environ))から生成されてるってことね。

self.cast()はデータをHTTPのメッセージとして適した形式にエンコードする処理だから、実質的にはself._handle(environ)でレスポンス内容が作られてる。

これが少し長い。

なのでexceptブロックを除いてスッキリさせてみる。


bottle.py


#: A thread-safe instance of :class:`LocalResponse`. It is used to change the
#: HTTP response for the *current* request.
response = LocalResponse()

class Bottle(object):

def _handle(self, environ):
path = environ['bottle.raw_path'] = environ['PATH_INFO']
if py3k:
environ['PATH_INFO'] = path.encode('latin1').decode('utf8', 'ignore')

environ['bottle.app'] = self
request.bind(environ)
response.bind()

try:
while True: # Remove in 0.14 together with RouteReset
out = None
try:
self.trigger_hook('before_request')
route, args = self.router.match(environ)
environ['route.handle'] = route
environ['bottle.route'] = route
environ['route.url_args'] = args
out = route.call(**args)
break

finally:
if isinstance(out, HTTPResponse):
out.apply(response)
try:
self.trigger_hook('after_request')

return out


ここでいきなり登場したresponse = LocalResponse()だが、LocalResponseクラスの親クラスBaseResponseは以下で定義されている。


bottle.py

class BaseResponse(object):

""" Storage class for a response body as well as headers and cookies.

This class does support dict-like case-insensitive item-access to
headers, but is NOT a dict. Most notably, iterating over a response
yields parts of the body and not the headers. # 以下のコメントは略
"""

default_status = 200
default_content_type = 'text/html; charset=UTF-8'

def __init__(self, body='', status=None, headers=None, **more_headers):
self._cookies = None
self._headers = {}
self.body = body


クッキー、ヘッダー、ボディーなんかの情報を保持してくれるらしい。

今はこれ以上深追いしないでおく。

さて、Bottle()._handler()

route, args = self.router.match(environ)

out = route.call(**args)
return out

って部分がキモで、ここでルーティングの処理が行われているみたいですね。

ここでself.routerとはRouter()のこと(Bottle.__init__で定義されている)なので、このクラスをみてみましょう。


bottle.py


class Router(object):
""" 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/<page>`). The wildcard syntax
and details on the matching order are described in docs:`routing`.
"""

def __init__(self, strict=False):
self.rules = [] # All rules in order
self._groups = {} # index of regexes to find them in dyna_routes
self.builder = {} # Data structure for the url builder
self.static = {} # Search structure for static routes
self.dyna_routes = {}
self.dyna_regexes = {} # Search structure for dynamic routes
#: If true, static routes are no longer checked first.
self.strict_order = strict
self.filters = {
're': lambda conf: (_re_flatten(conf or self.default_pattern),
None, None),
'int': lambda conf: (r'-?\d+', int, lambda x: str(int(x))),
'float': lambda conf: (r'-?[\d.]+', float, lambda x: str(float(x))),
'path': lambda conf: (r'.+?', None, None)
}

def match(self, environ):
""" Return a (target, url_args) tuple or raise HTTPError(400/404/405). """
verb = environ['REQUEST_METHOD'].upper()
path = environ['PATH_INFO'] or '/'

if verb == 'HEAD':
methods = ['PROXY', verb, 'GET', 'ANY']
else:
methods = ['PROXY', verb, 'ANY']

for method in methods:
if method in self.static and path in self.static[method]:
target, getargs = self.static[method][path]
return target, getargs(path) if getargs else {}
elif method in self.dyna_regexes:
for combined, rules in self.dyna_regexes[method]:
match = combined(path)
if match:
target, getargs = rules[match.lastindex - 1]
return target, getargs(path) if getargs else {}


ここら辺、少しごちゃごちゃで結局return target, getargs(path)targetって何かわかりづらいですが、route, args = self.router.match(environ)を見て察しがつく通り、Routeインスタンスが返されるわけですね。

つまりいつの間にか、self.static[method][path]としてRoute()が登録されているわけです。

いつの間に!?

以下のソースをみると、全体像がだんだん見えてきます!


bottle.py


class Bottle(object):

def route(self,
path=None,
method='GET',
callback=None,
name=None,
apply=None,
skip=None, **config):
""" A decorator to bind a function to a request URL. Example::

@app.route('/hello/<name>')
def hello(name):
return 'Hello %s' % name

The ``<name>`` part is a wildcard. See :class:`Router` for syntax
details.
"""
if callable(path): path, callback = None, path
plugins = makelist(apply)
skiplist = makelist(skip)

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

return decorator(callback) if callback else decorator

def add_route(self, route):
""" Add a route object, but do not change the :data:`Route.app`
attribute."""

self.routes.append(route)
self.router.add(route.rule, route.method, route, name=route.name)
if DEBUG: route.prepare()


つまり我々が


@app.route('/hello/<name>')
def hello(name):
return 'Hello %s' % name

とデコレータをつけることによって、リクエストが飛んできた際に


route = Route(self, rule, verb, callback, name=name, plugins=plugins, skiplist=skiplist, **config)
self.add_route(route)

RouterインスタンスにRouteインスタンスの情報を与えていたわけですね。

なんか頭がこんがらがってきた...


レスポンスデータ

以上からわかったことは、

route, args = self.router.match(environ)

out = route.call(**args)
return out # レスポンスデータが含まれている!

におけるrouteRouteインスタンスであり(当たり前っぽいけど)、Route().call()にお望みのレスポンスデータが入っているってこと!

そこでRouteクラスの定義を見てみると


bottle.py


class Route(object):
""" 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.
"""

def __init__(self, app, rule, method, callback,
name=None,
plugins=None,
skiplist=None, **config):
#: The application this route is installed to.
self.app = app
self.callback = callback
#: A list of route-specific plugins (see :meth:`Bottle.route`).
self.plugins = plugins or []

@cached_property
def call(self):
""" The route callback with all plugins applied. This property is
created on demand and then cached to speed up subsequent requests."""

return self._make_callback()

def _make_callback(self):
callback = self.callback
for plugin in self.all_plugins():
try:
if hasattr(plugin, 'apply'):
callback = plugin.apply(callback, self)
else:
callback = plugin(callback)
except RouteReset: # Try again with changed configuration.
return self._make_callback()
if not callback is self.callback:
update_wrapper(callback, self.callback)
return callback


つまりまあ、プラグインを挟み込んでいて、大事な部分はself.callbackなわけだけど、そもそもこいつはBottle.routeの中の


bottle.py


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

return decorator(callback) if callback else decorator


で出てきたcallbackが渡されただけだよね。

これは

@app.route('/hello/<name>')

def hello(name):
return 'Hello %s' % name

におけるhello関数に当たるわけで、これもまた至極当然だけど、ここで自分が定義した関数に従ってレスポンスデータが生成されるとな。

ちゃんちゃん。


誰か助けて

ってことで、ようやく全体像が見えてきました(ロギングとかエラー処理の部分は無視してきたけど)。

しかし疑問なのは、

from bottle import route

でどうしてrouteデコレータが読み込まれるのか、さっぱり理解できていません。

Routeクラス内で定義されているデコレータ関数が、いつの間にかrouteという名前で環境登録されている、という感覚です。

もし理解している方がいらっしゃったら教えていただきたいです...泣