2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

この記事は前回の続きになっていますが、単独でも問題なく読めるはずです。
まあ、自分用の整理のための記事ですが...

本丸の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という名前で環境登録されている、という感覚です。

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

2
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?