4
8

More than 3 years have passed since last update.

【Python】Flaskのソースコードを読んでみる

Posted at

以前Bottleのソースコードを概観してみたので、今回はFlaskを見てみたいと思います。

まずは起動から

Flaskのチュートリアルによると、最小限のアプリケーションはこんな感じで作成できるとな。

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return "Hello World!"

if __name__ == '__main__':
    app.run()

Bottleとほとんど同じなわけです。
runメソッドの定義はこんな感じ。

src/flask/app.py
class Flask(_PackageBoundObject):

    def run(self, host=None, port=None, debug=None, load_dotenv=True, **options):

        if os.environ.get("FLASK_RUN_FROM_CLI") == "true":
            from .debughelpers import explain_ignored_app_run

            explain_ignored_app_run()
            return

        if get_load_dotenv(load_dotenv):
            cli.load_dotenv()

            # if set, let env vars override previous values
            if "FLASK_ENV" in os.environ:
                self.env = get_env()
                self.debug = get_debug_flag()
            elif "FLASK_DEBUG" in os.environ:
                self.debug = get_debug_flag()

        # debug passed to method overrides all other sources
        if debug is not None:
            self.debug = bool(debug)

        _host = "127.0.0.1"
        _port = 5000
        server_name = self.config.get("SERVER_NAME")
        sn_host, sn_port = None, None

        if server_name:
            sn_host, _, sn_port = server_name.partition(":")

        host = host or sn_host or _host
        # pick the first value that's not None (0 is allowed)
        port = int(next((p for p in (port, sn_port) if p is not None), _port))

        options.setdefault("use_reloader", self.debug)
        options.setdefault("use_debugger", self.debug)
        options.setdefault("threaded", True)

        cli.show_server_banner(self.env, self.debug, self.name, False)

        from werkzeug.serving import run_simple

        try:
            run_simple(host, port, self, **options)
        finally:
            # reset the first request information if the development server
            # reset normally.  This makes it possible to restart the server
            # without reloader and that stuff from an interactive shell.
            self._got_first_request = False

Bottleの場合はwsgirefライブラリを使っていましたね。
wsgiref.simple_server.make_server()でサーバーを立ち上げて、レスポンスを作成するときはstart_response()のような生臭い処理が実装されていました。

対してFlaskではwerkzeugっていう便利なライブラリを使っていますね。
ドキュメントによると、サンプルはこんな感じ。


from werkzeug.wrappers import Request, Response

@Request.application
def application(request):
    return Response('Hello, World!')

if __name__ == '__main__':
    from werkzeug.serving import run_simple
    run_simple('localhost', 4000, application)

return Response('Hello, World!')のように楽にレスポンスが返せるようになっている。

run_simple(host, port, self, **options)で本丸のFlaskクラスがアプリケーション関数として登録される点はBottleと同じ。

ってことは、レスポンス内容はFlask()()な形で__call__メソッドを呼び出すことで得られるはずだよね。

次にデコレータの定義

src/flask/app.py

class Flask(_PackageBoundObject):

    def route(self, rule, **options):
        """A decorator that is used to register a view function for a
        given URL rule.  This does the same thing as :meth:`add_url_rule`
        but is intended for decorator usage::

            @app.route('/')
            def index():
                return 'Hello World'

        For more information refer to :ref:`url-route-registrations`.
     (以下略)
        """

        def decorator(f):
            endpoint = options.pop("endpoint", None)
            self.add_url_rule(rule, endpoint, f, **options)
            return f

        return decorator


    @setupmethod
    def add_url_rule(
        self,
        rule,
        endpoint=None,
        view_func=None,
        provide_automatic_options=None,
        **options
    ):
        """
        Basically this example::

            @app.route('/')
            def index():
                pass

        Is equivalent to the following::

            def index():
                pass
            app.add_url_rule('/', 'index', index)

        (以下略)
        """
        if endpoint is None:
            endpoint = _endpoint_from_view_func(view_func)
        options["endpoint"] = endpoint
        methods = options.pop("methods", None)

        # if the methods are not given and the view_func object knows its
        # methods we can use that instead.  If neither exists, we go with
        # a tuple of only ``GET`` as default.
        if methods is None:
            methods = getattr(view_func, "methods", None) or ("GET",)
        if isinstance(methods, string_types):
            raise TypeError(
                "Allowed methods have to be iterables of strings, "
                'for example: @app.route(..., methods=["POST"])'
            )
        methods = set(item.upper() for item in methods)

        # Methods that should always be added
        required_methods = set(getattr(view_func, "required_methods", ()))

        # starting with Flask 0.8 the view_func object can disable and
        # force-enable the automatic options handling.
        if provide_automatic_options is None:
            provide_automatic_options = getattr(
                view_func, "provide_automatic_options", None
            )

        if provide_automatic_options is None:
            if "OPTIONS" not in methods:
                provide_automatic_options = True
                required_methods.add("OPTIONS")
            else:
                provide_automatic_options = False

        # Add the required methods now.
        methods |= required_methods

        rule = self.url_rule_class(rule, methods=methods, **options)
        rule.provide_automatic_options = provide_automatic_options

        self.url_map.add(rule)
        if view_func is not None:
            old_func = self.view_functions.get(endpoint)
            if old_func is not None and old_func != view_func:
                raise AssertionError(
                    "View function mapping is overwriting an "
                    "existing endpoint function: %s" % endpoint
                )
            self.view_functions[endpoint] = view_func

結局は

self.view_functions[endpoint] = view_func

の部分で、ユーザが定義したview関数を辞書として登録しているわけね。
基本はBottleと同じですね。

リクエストが来たら

Flaskインスタンスの__call__メソッドからレスポンス内容を取得するであろう、という話でした。

src/flask/app.py

class Flask(_PackageBoundObject):

    def __call__(self, environ, start_response):
        """The WSGI server calls the Flask application object as the
        WSGI application. This calls :meth:`wsgi_app` which can be
        wrapped to applying middleware."""
        return self.wsgi_app(environ, start_response)

    def wsgi_app(self, environ, start_response):
        """The actual WSGI application. This is not implemented in
        :meth:`__call__` so that middlewares can be applied without
        losing a reference to the app object. Instead of doing this::

            app = MyMiddleware(app)

        It's a better idea to do this instead::

            app.wsgi_app = MyMiddleware(app.wsgi_app)

        Then you still have the original application object around and
        can continue to call methods on it.
        """
        ctx = self.request_context(environ)
        error = None
        try:
            try:
                ctx.push()
                response = self.full_dispatch_request()
            except Exception as e:
                error = e
                response = self.handle_exception(e)
            except:  # noqa: B001
                error = sys.exc_info()[1]
                raise
            return response(environ, start_response)
        finally:
            if self.should_ignore_error(error):
                error = None
            ctx.auto_pop(error)

    def full_dispatch_request(self):
        """Dispatches the request and on top of that performs request
        pre and postprocessing as well as HTTP exception catching and
        error handling.
        """
        self.try_trigger_before_first_request_functions()
        try:
            request_started.send(self)
            rv = self.preprocess_request()
            if rv is None:
                rv = self.dispatch_request()
        except Exception as e:
            rv = self.handle_user_exception(e)
        return self.finalize_request(rv)

request_started.send(self)の部分も気になる。
これによって、プロセス管理をしているのかな。
でもこれ一旦置いておく。

で、今追いかけたいのは

rv = self.preprocess_request()
if rv is None:
    rv = self.dispatch_request()

の部分で、基本的にはpreprocess_request()self.dispatch_request()からデータを受け取っている。
rvってresponse bodyの略かしら?
なのでこの2つのメソッドをみてみる。

1、 process_response()

src/flask/app.py

class Flask(_PackageBoundObject):

    def process_response(self, response):
        """Can be overridden in order to modify the response object
        before it's sent to the WSGI server.  By default this will
        call all the :meth:`after_request` decorated functions.
        """
        ctx = _request_ctx_stack.top
        bp = ctx.request.blueprint
        funcs = ctx._after_request_functions
        if bp is not None and bp in self.after_request_funcs:
            funcs = chain(funcs, reversed(self.after_request_funcs[bp]))
        if None in self.after_request_funcs:
            funcs = chain(funcs, reversed(self.after_request_funcs[None]))
        for handler in funcs:
            response = handler(response)
        if not self.session_interface.is_null_session(ctx.session):
            self.session_interface.save_session(self, ctx.session, response)
        return response

ctxって"context"のことだよね?
contextに登録されているハンドラー達に処理を実行させている。
じゃあctxはいつ_request_ctx_stackに積み上げられた?

実は素通りしましたが、先ほど見たFlaskクラスのwsgi_app()メソッドの中でcontextが作成・プッシュされていました(見返してみてください)。

ちなみに_request_ctx_stack

src/flask/globals.py
_request_ctx_stack = LocalStack()

で作成されたもの。プッシュは以下で定義されている。

src/flask/ctx.py
class RequestContext(object):

    def push(self):
        """Binds the request context to the current context."""

        top = _request_ctx_stack.top
        if top is not None and top.preserved:
            top.pop(top._preserved_exc)

        # Before we push the request context we have to ensure that there
        # is an application context.
        app_ctx = _app_ctx_stack.top
        if app_ctx is None or app_ctx.app != self.app:
            app_ctx = self.app.app_context()
            app_ctx.push()
            self._implicit_app_ctx_stack.append(app_ctx)
        else:
            self._implicit_app_ctx_stack.append(None)

        if hasattr(sys, "exc_clear"):
            sys.exc_clear()

        _request_ctx_stack.push(self)

2、 dispatch_request()

src/flask/app.py

class Flask(_PackageBoundObject):

    def dispatch_request(self):
        """Does the request dispatching.  Matches the URL and returns the
        return value of the view or error handler.  This does not have to
        be a response object.  In order to convert the return value to a
        proper response object, call :func:`make_response`.
        """
        req = _request_ctx_stack.top.request
        if req.routing_exception is not None:
            self.raise_routing_exception(req)
        rule = req.url_rule
        # if we provide automatic options for this URL and the
        # request came with the OPTIONS method, reply automatically
        if (
            getattr(rule, "provide_automatic_options", False)
            and req.method == "OPTIONS"
        ):
            return self.make_default_options_response()
        # otherwise dispatch to the handler for that endpoint
        return self.view_functions[rule.endpoint](**req.view_args)

続いてこちらではself.view_functions[rule.endpoint]で取得した関数を実行しているわけですが、ここに登録されるのはいつなのか?

2つの場面を見つけました。

1つめは、最初の方で見たFlask.add_url_rule()メソッドの最後の方(これも見返してみてね)。

2つめは、


class Flask():

    @setupmethod
    def endpoint(self, endpoint):
        def decorator(f):
            self.view_functions[endpoint] = f
            return f
        return decorator

の部分。

でもこれらは、例えばadd_url_rule()した時にendpointがどうこうしてたから、利用場面は別ではないというか、うーん。
疲れてきたから確認するのが面倒くなってきた。

まあでも、add_url_rule()とかをみた感じ、結局dispatch_request()でユーザが定義したendpoint関数が実行されてるんよね。

じゃあprocess_response()の方はプラグインとか周辺の処理になるのかな?

最後に

疲れたので今回はこれくらいで。

4
8
0

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
4
8