Flask源码解析(2)

在flask.app模块里,__call__方法会调用wsgi_app方法

这个方法如下:

    def wsgi_app(self, environ, start_response):
        ctx = self.request_context(environ)
        ctx.push()
        error = None
        try:
            try:
                response = self.full_dispatch_request()
            except Exception as e:
                error = e
                response = self.handle_exception(e)
            except:
                error = sys.exc_info()[1]
                raise
            return response(environ, start_response)
        finally:
            if self.should_ignore_error(error):
                error = None
            ctx.auto_pop(error)

首先,request_context方法会返回一个RequestContext类的实例,这个实例是用传入的app,environ初始化的

RequestContext的初始化过程如下:

class RequestContext(object):
    def __init__(self, app, environ, request=None):
        self.app = app
        if request is None:
            request = app.request_class(environ)
        self.request = request
        self.url_adapter = app.create_url_adapter(self.request)
        self.flashes = None
        self.session = None

        self._implicit_app_ctx_stack = []

        self.preserved = False

        self._preserved_exc = None

        self._after_request_functions = []

        self.match_request()

去掉了注释,这里有几个关键的操作:

request = app.request_class(environ)

这一句,request_class是在Flask类中引用的Request类,这个类的继承关系如下:

BaseRequest -> RequestBase -> Request

下面的代码:

self.url_adapter = app.create_url_adapter(self.request)

 展开如下:

    def create_url_adapter(self, request):

        if request is not None:
            # MapAdapter
            return self.url_map.bind_to_environ(request.environ,
                server_name=self.config['SERVER_NAME'])
        # We need at the very least the server name to be set for this
        # to work.
        if self.config['SERVER_NAME'] is not None:
            return self.url_map.bind(
                self.config['SERVER_NAME'],
                script_name=self.config['APPLICATION_ROOT'] or '/',
                url_scheme=self.config['PREFERRED_URL_SCHEME'])

这里,url_map是Flask类中的Map类,bind_to_environ方法会返回一个MapAdapter实例

在werkzeug.routing模块里,有3个主要的类:Rule, Map 和 MapAdaptor

根据文档:


Rule:  A Rule represents one URL pattern.这个类主要是处理传入的URL。

几个比较重要的方法:

compile: 可以将URL映射为正则表达式,以便匹配

match: 根据传入的URL生成参数

build:根据传入的参数拼接URL

 

Map: The map class stores all the URL rules and some configuration parameters. 可以认为它是统一管理Rule的工具

比较重要的方法:
add:向rule list里添加rule, 并且compile the rule

bind: 添加server_name等对所有Rule生效的信息,返回MapAdaptor的实例

bind_to_environ: 与bind方法功能相同,但支持传入environ

 

MapAdaptor: 接收一个Map,这个类有两个主要的方法:match and build

match: 根据传入的URL匹配endpoint,本质上调用Rule中的match方法

build: 和match的功能相反,根据传入的endpoint和参数,构造URL本质上调用Rule中的build方法

 

初始化url_adaptor的过程使用了本次请求相关的信息(path_info, query_args ... etc)

这里只是初始化了url_adaptor,等待之后的调用

 

继续分析代码,初始化的最后,调用了:

self.match_request()

深入到这个方法里,可以看到:

    def match_request(self):
        """Can be overridden by a subclass to hook into the matching
        of the request.
        """
        try:
            url_rule, self.request.view_args = \
                self.url_adapter.match(return_rule=True)
            self.request.url_rule = url_rule
        except HTTPException as e:
            self.request.routing_exception = e

这里调用了我们刚刚初始化过的url_adaptor的match方法,这个方法的作用之前已经说过了:根据传入的URL生成参数(endpoint等)

由于本次请求的相关信息之前已经用来初始化了url_adaptor,所以match的时候可以读到

但是之前我们写在代码里的那些URL,match的时候肯定也要拿出来,和当前请求的URL对比,这部分工作是怎么完成的呢?

原来,当我们定义路由的时候:

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

会调用一个add_url_rule方法,除去复杂的参数处理,这个方法的关键代码如下:

    @setupmethod
    def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
        ......
if endpoint is None:
endpoint = _endpoint_from_view_func(view_func)
options['endpoint'] = endpoint
        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

可见当我们定义路由的时候,就已经根据此路由的URL和endpoint生成了Rule实例

self.url_map.add(rule)这一句,作用之前也已经说过了

同时,这个方法还将endpoint作为key, view_func(视图函数的名字)作为value存入了view_function字典

之后的话会根据endpoint找到对应的view_func

 

这样,执行完RequestContext的初始化过程之后,我们就知道了这个请求该用哪个endpoint(存储在self.request.url_rule里)处理,请求中的参数(存储在self.request.view_args里)是什么

 

总结一下,当我们启动app,请求还未到来之前,这个app的Map和Rule就已经初始化好了

当请求到来之后才创建MapAdaptor,MapAdaptor也是基于之前创建的Map和Rule的

一个app里,Map和Rule是不变的,MapAdaptor是可变的

 

然后继续往下走:

ctx.push()

push对应代码如下:

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

        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)

        self.session = self.app.open_session(self.request)
        if self.session is None:
            self.session = self.app.make_null_session()

这段代码的主要作用是创建请求上下文,同时如果没有app context的话,则创建它

几个问题:

0 为什么要使用context?

这部分要参照文档:
(ref:http://flask.pocoo.org/docs/1.0/appcontext/#purpose-of-the-context)

The Flask application object has attributes, such as config, that are useful to access within views and CLI commands. However, importing the app instance within the modules in your project is prone to circular import issues. When using the app factory pattern or writing reusable blueprints or extensions there won’t be an app instance to import at all.

可见,主要是解决循环依赖,以及在使用工厂模式的时候无法import app

为啥使用使用工厂模式的时候无法import app呢?这需要去看工厂模式是怎么回事,文档里举了一个例子:

(ref:http://flask.pocoo.org/docs/1.0/patterns/appfactories/#basic-factories)

def create_app(config_filename):
    app = Flask(__name__)
    app.config.from_pyfile(config_filename)

    from yourapplication.model import db
    db.init_app(app)

    from yourapplication.views.admin import admin
    from yourapplication.views.frontend import frontend
    app.register_blueprint(admin)
    app.register_blueprint(frontend)

    return app

简而言之,使用工厂模式的主要目的是在测试,开发,部署的时候,根据传入的参数就能创建一个独立特殊的app,非常有用。

另外关于使用reusable extentions(工厂模式下的扩展), 文档里建议:the extension object does not initially get bound to the application.

错误的例子(db被app绑定了):

def create_app(config_filename):
    app = Flask(__name__)
    app.config.from_pyfile(config_filename)

    db = SQLAlchemy(app)

正确的做法(使用db初始化app中的配置,db不依赖app):

in model.py:

db = SQLAlchemy()

in application.py

def create_app(config_filename):
    app = Flask(__name__)
    app.config.from_pyfile(config_filename)

    from yourapplication.model import db
    db.init_app(app)

Using this design pattern, no application-specific state is stored on the extension object, so one extension object can be used for multiple apps.

1 为什么要使用_request_ctx_stack来存储request context:

注意到(flask.globals):

_request_ctx_stack = LocalStack()

LocalStack以stack的形式存储线程(协程)隔离的信息

在flask的全局变量里,request和session是和request相关的

当我们去使用request和session的时候,我们希望使用的是最新的,也就是每次使用都要去_requst_ctx_stack这个栈上重新去获取本线程下这两个变量的相关信息

注意到:

def _lookup_req_object(name):
    top = _request_ctx_stack.top
    if top is None:
        raise RuntimeError(_request_ctx_err_msg)
    return getattr(top, name)

request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))

代理模式实现了这一点,每次取requst的时候都使用_lookup_req_object这个代理函数去获取

如果我们不每次读request的时候都获取最新的,会发生什么呢?

(ref:http://cizixs.com/2017/01/13/flask-insight-context

比如对于这样一个简单的应用:

from flask import Flask, request
app = Flask(__name__)


@app.route('/', methods=['GET', 'POST'])
def hello_world():
    if request.method == 'GET':
        return 'Hello, World from Get'
    elif request.method == 'POST':
        return 'Hello, World from Post'
    return 'Hello, World from ???'


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

当app启动的时候,request里面是空的,如果每次不重新去获取request的话,执行到判断method那里就一定会报错

2 push的最初几行代码的判断是什么意思?

根据注释:

The rationale is that you want to access that information under debug situations.

However if someone forgets to pop that context again we want to make sure that on the next push it's invalidated, otherwise we run at risk that something leaks memory.

This is usually only a problem in test suite since this functionality is not active in production environments.

也就是说,在测试的情况下,有可能当前的线程能看到的_requst_ctx_stack里有之前残存的request context,如果有并且之前的request context已经被保存过,那么pop它

3 区分app context与request context的意义何在,为什么要使用stack存储

这是一个很核心的问题。

首先我们知道,如果是在web环境运行的话,本线程的app 和 request的stack里肯定只有一个item,既然只有一个item,那就没有使用stack的必要了

(ref:https://blog.tonyseek.com/post/the-context-mechanism-of-flask/

可是有时候我们也需要在非web环境下运行,这往往也需要我们自己手动往app stack或者request stack上push条目,并且我们只在主线程中进行这种操作。典型的有离线脚本和测试。

回到问题,那为啥要区分app和request呢?

(ref:https://stackoverflow.com/questions/20036520/what-is-the-purpose-of-flasks-context-stacks/20041823#20041823

原来,flask允许多个app并存。

下面的代码说明了为啥要使用stack存储app context:

(ref:http://cizixs.com/2017/01/13/flask-insight-context

from flask import current_app

app = create_app()
admin_app = create_admin_app()

def do_something():
    with app.app_context():
        work_on(current_app)
        with admin_app.app_context():
            work_on(current_app)

同时也说明了为啥要区分app context和request context,因为有些时候我们只需要关注某个app上下文的信息,不需要关注请求。所以从概念上和应用上把它们区分、解耦是很必要的,也是合情合理的。

上面stackoverflow的答案里有提到Flask在处理重定向的时候,会使用一种叫做 "internal redirect"的做法,在进行这步操作的时候,request context stack上会有不止一个request context,但是这一步我没有复现。

 另外在使用Flask的test_client测试的时候,源码中是既使用了app context,也使用了request context了的,这和我原本想象的只使用request context不同。

4 app context里有什么,如何实现的?

//TODO 待续

之后执行full_dispatch_request函数,这个函数的功能如下:

    def full_dispatch_request(self):
        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)

首先,在try_trigger_before_first_request_functions 主要是执行了before_first_request_funcs里的方法,这个方法的作用是在每个app实例启动的时候执行一次不是在每个请求来的时候执行一次)

    def try_trigger_before_first_request_functions(self):
        if self._got_first_request:
            return
        with self._before_request_lock:
            if self._got_first_request:
                return
            for func in self.before_first_request_funcs:
                func()
            self._got_first_request = True

 

before_first_request_funcs是一个数组,里面存储了我们使用before_first_request装饰器装饰的方法

接着回到full_dispatch_request里来,我们调用了preprocess_request方法。
贴一下这个方法的注释:
Called before the actual request dispatching and will call each :meth:`before_request` decorated function, passing no
arguments.
If any of these functions returns a value, it's handled as if it was the return value from the view and further request handling is stopped.

This also triggers the :meth:`url_value_preprocessor` functions before the actual :meth:`before_request` functions are called.

结合这个方法的代码:

    def preprocess_request(self):
        bp = _request_ctx_stack.top.request.blueprint

        funcs = self.url_value_preprocessors.get(None, ())
        if bp is not None and bp in self.url_value_preprocessors:
            funcs = chain(funcs, self.url_value_preprocessors[bp])
        for func in funcs:
            func(request.endpoint, request.view_args)

        funcs = self.before_request_funcs.get(None, ())
        if bp is not None and bp in self.before_request_funcs:
            funcs = chain(funcs, self.before_request_funcs[bp])
        for func in funcs:
            rv = func()
            if rv is not None:
                return rv

 

首先,这个url_value_preprocessor是一个dict,不使用蓝图的话,key是None, value是list,list里存储的是使用
url_value_preprocessor这个装饰器装饰的方法
正如这个dict的名字,使用这个装饰器装饰的方法的主要作用是,在请求正式进入视图函数处理之前,提取请求URL中的values
使用url_value_preprocessor装饰的方法需要接收两个参数:

        for func in funcs:
            func(request.endpoint, request.view_args)

 


接下来执行的是before_request_funcs里的方法,这个方法的介绍是这样的:
Registers a function to run before each request. The function will be called without any arguments.

If the function returns a non-None value, it's handled as if it was the return value from the view and further request handling is stopped.

也就是每个请求到来之前执行一次。一旦用before_request这个装饰器装饰了某个方法,那么这个方法就会添加到
before_request_funcs这个字典中去

接下来在full_dispatch_request方法里,继续执行方法dispatch_request,这个方法的执行简明易懂:

    def dispatch_request(self):
        req = _request_ctx_stack.top.request
        if req.routing_exception is not None:
            self.raise_routing_exception(req)
        rule = req.url_rule
        if getattr(rule, 'provide_automatic_options', False) \
           and req.method == 'OPTIONS':
            return self.make_default_options_response()
        return self.view_functions[rule.endpoint](**req.view_args)

 


这个方法在本线程可见的_request_ctx_stack上取出request,显然,这个request就是在RequestContext的构造函数
里初始化的那个request
接下来取request的url_rule,这里的url_rule是在RequestContext里match_request的时候被初始化的
(下面用到的view_args也是在match_request里被初始化的)
接下来这一句:

        return self.view_functions[rule.endpoint](**req.view_args)

 


显然是根据endpoint找到view_func,然后传入参数,剩下的工作交给业务逻辑了

posted @ 2018-08-20 13:10  geeklove  阅读(271)  评论(0)    收藏  举报