yokon's blog

Flask的经典错误

2018.07.29

在编写 Flask 应用的单元测试,离线应用时候经常会遇到,*RuntimeError: Working outside of application context.*的报错。这个报错是在没有激活程序上下文情况下,进行了一些程序上下文或请求上下文的操作。先看下面的两段代码:

from flask import Flask, current_app

app = Flask(__name__)
print(current_app.name)
from flask import Flask, current_app

app = Flask(__name__)

@app.route('/')
def index():
    print(current_app.name)
    return 'hello, world'

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

如果运行第一段代码,他就会报错:RuntimeError: Working outside of application context. 而第二段代码却不会。为什么会这样呢,使用搜索引擎或者直接在flask文档中,可以找到他的完美解决方案。但是可能知道怎么处理这个错误,却不知道他为何会这样,所以如果有时间,我建议去看相关源码。如果使用pycharm做IDE,只需要按住ctrl然后左键点击current_app就可以跳到flask该部分的源码。

源码分析

def _find_app():
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return top.app

# context locals
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)

可以看到,current_app是一个LocalProxy的实例,不去管werkzeug的部分,直接看这个初始化实例时接受的参数_find_app。可以看到_find_app是一个函数,他的代码也很简单,第一句top = _app_ctx_stack.top_app_ctx_stack是flask的应用上下文栈(栈,是一种后进先出的数据结构),当然从命名中就可以看出来,而它又是LocalStack()类的实例。接着往下看LocalStack()的部分:

class LocalStack(object):
    
    # ...

    @property
    def top(self):
        """The topmost item on the stack.  If the stack is empty,
        `None` is returned.
        """
        try:
            return self._local.stack[-1]
        except (AttributeError, IndexError):
            return None

LocalStack()类实现了入栈push(),出栈pop()。而top是一个属性,他不是出栈,是拿到栈顶的元素。那可以知道_find_app()函数是要拿到当前应用上下文。如果拿到他就返回top.app,那么current_app其实是当前应用上下文的app实例(先不管他怎么实现的)。而如果拿不到这个top,栈顶没有元素,他就会抛出这个经典错误raise RuntimeError(_app_ctx_err_msg)

解决这个错误,最简单的办法就是在调用current_app实例前,将当前应用上下文推入栈中。首先要得到应用上下文对象,幸运的是flask应用对象提供了app_context()方法,直接调用他,将得到一个上下文对象。

from flask import Flask, current_app

app = Flask(__name__)
ctx = app.app_context()
ctx.push()
print(current_app.name)

ctx并其实调用的是_app_ctx_stackpush()入栈哦。跳到app_context()的源码,可以看到他其实是返回了应用上下文AppContext()类的实例,并且传给了他selfpython中默认selef代表的是类的实例,这里就是app应用实例。下面接着看AppContext()代码。

class AppContext(object):

    def __init__(self, app):
        self.app = app
        ...
        self._refcnt = 0

    def push(self):
        """Binds the app context to the current context."""
        self._refcnt += 1
        if hasattr(sys, 'exc_clear'):
            sys.exc_clear()
        _app_ctx_stack.push(self)
        appcontext_pushed.send(self.app)

    def pop(self, exc=_sentinel):
        """Pops the app context."""
        try:
            self._refcnt -= 1
            if self._refcnt <= 0:
                if exc is _sentinel:
                    exc = sys.exc_info()[1]
                self.app.do_teardown_appcontext(exc)
        finally:
            rv = _app_ctx_stack.pop()
        assert rv is self, 'Popped wrong app context.  (%r instead of %r)' \
            % (rv, self)
        appcontext_popped.send(self.app)

很明显,AppContext()类实现了入栈和出栈,其实就是调用了应用上下文_app_ctx_stack的方法。而他push()的是self,就是该类的实例。所以上面_find_app()函数返回top.app,就是app实例(这里有点绕QAQ~)。大致如下:

  1. app=Flask(__name__),app 是Flask的实例,
  2. app.app_context(),调用 Flask 类里的app_context(self)方法,
  3. app_context(self)返回AppContext()类实例,传给他 self,
  4. selfFlask 类的实例,即 app,
  5. AppContext()类初始化实例,需要传入 app 属性,self.app = app
  6. AppContext()类的push方法,调用_app_ctx_stack.push(self)
  7. _app_ctx_stack.push(self)中传入的selfAppContext()类的实例(他有app属性,见第5行),
  8. _app_ctx_stack.push(self)就是将应用上下文实例推入应用上下文栈,
  9. _find_app()返回_app_ctx_stack.app,即是返回栈中上下文的app实例,
  10. current_app = LocalProxy(_find_app)拿到这个 app 实例。

post37_0.png

疑问

上面的代码,把应用上下文推入栈中,运行的时候仍然会报错AssertionError: Popped wrong app context.。这是因为在请求结束,需要调用ctx.pop()将应用上下文弹出栈。

那么为什么在正常的路由中,调用current_app请求却不会抛出Working outside of application 的错误呢?

Flask其实不仅仅有应用上下文,还有请求上下文:

_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, 'request'))

请求上下文同样使用栈保存。在正常的flask路由请求中,flask会在将请求上下文入栈前,判断_app_ctx_stack栈顶是否存在应用上下文,如果不存在,flask会自己先将它入栈。下面来看一下flask是如何处理的,首先在flask代码里找到哪里有将请求上下文入栈的代码:

def wsgi_app(self, environ, start_response):
    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:
            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 __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)

看这个方法名,应该是个符合WSGI标准的一个HTTP处理函数。关于WSGI是什么以及他的参数environ, start_response是什么,可以参考这篇文章 理解Python的Web开发{:target="_blank"}。

ctx = self.request_context(environ)
...
ctx.push()

跟踪self.request_context(environ)方法,他返回RequestContext()类实例。看该类的push()方法:

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

可以看到在请求上下文入栈前,会先将请求上下文入栈app_ctx.push()

那这个wsgi_app方法是在哪里调用的,截取wsgi_app方法的代码时,我也截取了下面的__call__定制类方法,不理解改个方法的参考这篇文章Python的定制类笔记{:target="_blank"}。它让我们能像调用函数那样调用实例方法,而它返回的正是self.wsgi_app(environ, start_response)。那要调用wsgi_app方法只需要:

# 实例Flask类
app = Flask(__name__)
# 函数一样调用他
app(environ, start_response)

wsgi_app方法是一个HTTP处理函数,拿他肯定会在flask应用启动的时候被调用,我们看run()方法:

def run(self, host=None, port=None, debug=None,
            load_dotenv=True, **options):
    ...
    try:
        run_simple(host, port, self, **options)
    finally:
        self._got_first_request = False

可以看到app.run()其实是调用了werkzeug库的run_simple方法,该方法接收的第三个参数就是应用程序实例了,他会在werkzeug库的wsgi.py中像函数一样被调用: self.app(environ, start_response),启动flask的wsgi_app方法。

最后

flask代码的注释写的很详细,他的文档大部分就是他的注释。所以,看flask源码的难度其实是很低的。

参考文章:

Flask—上下文源码分析{:target="_blank"}