Marvin's Blog【程式人生】

Ability will never catch up with the demand for it

15 Jun 2020

Flask文档阅读笔记(二)

Flask 文档1.1.x走读笔记。

The Application Context

Purpose of the Context¶

应用级别的数据可以通过current_app以及g这俩全局变量访问。

Flask的app实例有很多属性,可以在view或者cli里面访问。但是在app factory pattern或者blueprints以及extensions里面,不可导入app实例,因为可能引起循环引用。

为此,Flask提供了一个current_app代理对象来替代对app实例的直接访问。在处理一个request的时候,Flask会自动生成这个current_app代理对象,并在current_app中提供对相应的上下文的访问。

对于Flask.cli的@app.cli.command(),Flask也会提供上下文。

Lifetime of the Context¶

上下文代理对象是按需生成,用完即毁。在处理一个请求,或者处理一个命令之前会生成,在处理完之后会销毁。

Manually Push a Context¶

如果在上下文之外使用current_app,那么会产生下面的错误信息:

RuntimeError: Working outside of application context.

This typically means that you attempted to use functionality that
needed to interface with the current application object in some way.
To solve this, set up an application context with app.app_context().

这时候,你可以显示地生成一个上下文:

def create_app():
    app = Flask(__name__)

    with app.app_context():
        init_db()

    return app

基本上,还是要把代码放置在视图或者命令地代码里面。

Storing Data¶

上下文还提供了一个g对象用来储存用户自定地数据。常见的用法:

  1. get_X()会创建资源X,如果这个资源不存在,并将其保存在g.X
  2. teardown_X()释放资源。这个可以注册在teardown_appcontext(),以便自动释放。

一个管理数据库链接的例子:

from flask import g

def get_db():
    if 'db' not in g:
        g.db = connect_to_database()

    return g.db

@app.teardown_appcontext
def teardown_db():
    db = g.pop('db', None)

    if db is not None:
        db.close()

每一次get_db()都返回相同的数据库链接。

你也可以使用LocalProxy来为get_db()创建一个新的上下文:

from werkzeug.local import LocalProxy
db = LocalProxy(get_db)

访问db会内部调用get_db,跟访问current_app的效果一样。

如果在写扩展的时候,不要直接使用g对象,而是可以在上下文里面直接放置数据。

Events and Signals¶

在上下文被弃用的时候,使用teardown_appcontext()注册的函数会被调用。如果开启了信号,也就是signals_available为true,下面的信号会被发送:appcontext_pushed,appcontext_tearing_down,以及appcontext_popped。

The Request Context¶

请求级别的数据可以用request和session代理对象来访问。

Purpose of the Context¶

当Flask处理收到一个请求的时候,它根据从WSGI服务器的环境创建一个Request对象。一个工作者可以是一个线程、进程、协程,但是只处理一个request。所以这个request可以被看成工作者的全局数据,被叫做context local。和处理应用上下文的方式类似,Flask会在处理请求的时候会自动生成请求上下文。

Lifetime of the Context¶

请求上下文和应用上下文的生命周期类似。但是请求上下文无法传给另外的线程。请求上下文是Werkzeug的Context Locals实现的。

Manually Push a Context¶

在请求上下文之外访问reqeust,会产生如下错误:

RuntimeError: Working outside of request context.

This typically means that you attempted to use functionality that
needed an active HTTP request. Consult the documentation on testing
for information about how to avoid this problem.

对于测试代码,没有请求上下文,为了避免这个问题,可以使用test_client来模拟一个完整的请求。你可以在with语句里面使用test_request_context(),这样这个块里面的代码可以访问request对象。

def generate_report(year):
    format = request.args.get('format')
    ...

with app.test_request_context(
        '/make_report/2017', data={'format': 'short'}):
    generate_report()

How the Context Works¶

每次处理一个请求之前,会访问Flask.wsgi_app()。它管理着请求的各种上下文。从内部看,应用和请求上下文以栈的方式工作,_request_ctx_stack和_app_ctx_stack。当上下文被推到栈上,这些上下文的代理就指向栈顶。

当请求开始时,RequestContext被创建并入栈,它会检查相应的的AppContext是否存在,如果不存在,则会被创建并入栈。当这些上下文都入栈时,current_app, g,request,以及session这些代理对象都可以作为全局对象使用了。

除了把应用上下文和请求上下文入栈以外,有特殊的需求,还可以将自定义的上下文入栈。这就属于比较高级的用法了。

当请求被处理,并且应答被发送之后,请求上下文会被出栈,然后应用上下文也会被出栈。当它们出栈之后,teardown_request()和teardown_appcontext()会被调用。即便在出现异常的时候,这些钩子函数也会被调用。

Callbacks and Errors¶

Flask派遣一个请求的时候,会分多个阶段。一个Blueprint可以在blueprint内为事件增加处理器,这些处理将为运行,如果这个blueprint处理请求所对应的route:

  1. 在处理请求之前,before_request()函数被调用。如果某个函数返回一个值,其他函数将被忽略。这个返回值将被视为应答,视图函数将不会被调用。
  2. 如果before_request()没有返回合适的值,那么符合route的视图函数会被调用
  3. 视图函数的返回值会被转化为应答对象,然后传给after_request(),后者可以修改response的值
  4. 当完成生成应答对象的时候,会调用teardown_request()以及teardown_appcontext()函数。

当一个异常在teardown函数调用前发生,Flask会尝试把它跟errorhandler()函数匹配,看能不能返回一个应答对象。如果没有对应的errorhandler(),或者errorhandler()自己产生了一个异常,那么Flask就会返回通用的500错误。teardown函数依然会被调用,异常对象会被作为参数传入。

但是如何开启了debug模式,那么未处理的异常不会被转化为500,而是会被传递给WSGI服务器,允许development服务器开启debugger进行调试。

Teardown Callbacks¶

teardown回调函数跟request本身不相关,而是跟context相关。在context被出栈的时候会调用。异常的时候也会调用,手动入栈的context出栈的时候也会调用。所以注册teardown回调的时候要注意不要依赖于request的其他状态。

在测试的时候,可以把context延迟出栈,这样允许测试代码来校验context中的数据。使用with语句来延长test_client()的有效性:

from flask import Flask, request

app = Flask(__name__)

@app.route('/')
def hello():
    print('during view')
    return 'Hello, World!'

@app.teardown_request
def show_teardown(exception):
    print('after with block')

with app.test_request_context():
    print('during with block')

# teardown functions are called after the context with block exits

with app.test_client() as client:
    client.get('/')
    # the contexts are not popped even though the request ended
    print(request.path)

# the contexts are popped and teardown functions are called after
# the client with block exits

signals

如果signals_available 为true,那么下列信号会被发送:

  1. 在before_request()调用之前发送request_started
  2. 在fater_request()调用之后发送reqeust_finished
  3. got_request_exception会在异常开始处理的时候处理,时机在查询errorhandler()之前
  4. request_tearing_down被发送的时候teardown_request()应该已经结束

Context Preservation on Error¶

在FLASK_EVN设为’development'的时候,在异常的时候依然会保持上下文,这样调试器中就可以看到这个异常了。

PRESERVE_CONTEXT_ON_EXCEPTION 配置可以用来控制上述行为。在development环境下,这个配置为true。如果是开发环境,不可开启这个配置。

Notes On Proxies¶

Flask使用代理对象来控制依赖关系,大部分时候不需要直接访问代理对象所指向的对象。但是有些情况下需要知道:

  • 代理对象和所指对象是不同的类型。如果操作依赖于所指对象的类型,那么就需要获取所指对象。
  • 在发送信号,或者传数据给后台线程的时候需要知道所指对象的类型

通过_get_current_object()方法可以获取所指对象:

app = current_app._get_current_object()
my_signal.send(app)

(本篇完)

Categories

Tags