この記事は前回の続きになっていますが、単独でも問題なく読めるはずです。
まあ、自分用の整理のための記事ですが...
本丸のBottleクラス
いよいよ天守閣のBottle
クラス。
まずは前回からの流れで、__call__
メソッドから。
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ブロックを除いてスッキリさせてみる。
#: 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
は以下で定義されている。
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__
で定義されている)なので、このクラスをみてみましょう。
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()
が登録されているわけです。
いつの間に!?
以下のソースをみると、全体像がだんだん見えてきます!
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 # レスポンスデータが含まれている!
におけるroute
はRoute
インスタンスであり(当たり前っぽいけど)、Route().call()
にお望みのレスポンスデータが入っているってこと!
そこでRoute
クラスの定義を見てみると
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
の中の
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
という名前で環境登録されている、という感覚です。
もし理解している方がいらっしゃったら教えていただきたいです...泣