在编写 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_stack
的push()
入栈哦。跳到app_context()
的源码,可以看到他其实是返回了应用上下文AppContext()
类的实例,并且传给了他self
。python
中默认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~)。大致如下:
app=Flask(__name__)
,app 是Flask
的实例,app.app_context()
,调用Flask
类里的app_context(self)
方法,app_context(self)
返回AppContext()
类实例,传给他 self,self
是Flask
类的实例,即 app,AppContext()
类初始化实例,需要传入 app 属性,self.app = app
,AppContext()
类的push方法,调用_app_ctx_stack.push(self)
,_app_ctx_stack.push(self)
中传入的self
是AppContext()
类的实例(他有app属性,见第5行),_app_ctx_stack.push(self)
就是将应用上下文实例推入应用上下文栈,_find_app()
返回_app_ctx_stack.app
,即是返回栈中上下文的app
实例,current_app = LocalProxy(_find_app)
拿到这个 app 实例。
疑问
上面的代码,把应用上下文推入栈中,运行的时候仍然会报错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"}