forked from orbit-oss/flask
Added flask.stream_with_context
This commit is contained in:
parent
2e816f554a
commit
d5218997d9
8 changed files with 222 additions and 27 deletions
2
CHANGES
2
CHANGES
|
|
@ -71,6 +71,8 @@ Relase date to be decided, codename to be chosen.
|
||||||
- Added `required_methods` attribute to view functions to force-add methods
|
- Added `required_methods` attribute to view functions to force-add methods
|
||||||
on registration.
|
on registration.
|
||||||
- Added :func:`flask.after_this_request`.
|
- Added :func:`flask.after_this_request`.
|
||||||
|
- Added :func:`flask.stream_with_context` and the ability to push contexts
|
||||||
|
multiple times without producing unexpected behavior.
|
||||||
|
|
||||||
Version 0.8.1
|
Version 0.8.1
|
||||||
-------------
|
-------------
|
||||||
|
|
|
||||||
|
|
@ -375,6 +375,11 @@ Extensions
|
||||||
|
|
||||||
.. versionadded:: 0.8
|
.. versionadded:: 0.8
|
||||||
|
|
||||||
|
Stream Helpers
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. autofunction:: stream_with_context
|
||||||
|
|
||||||
Useful Internals
|
Useful Internals
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,3 +59,26 @@ The template is then evaluated as the stream is iterated over. Since each
|
||||||
time you do a yield the server will flush the content to the client you
|
time you do a yield the server will flush the content to the client you
|
||||||
might want to buffer up a few items in the template which you can do with
|
might want to buffer up a few items in the template which you can do with
|
||||||
``rv.enable_buffering(size)``. ``5`` is a sane default.
|
``rv.enable_buffering(size)``. ``5`` is a sane default.
|
||||||
|
|
||||||
|
Streaming with Context
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
.. versionadded:: 0.9
|
||||||
|
|
||||||
|
Note that when you stream data, the request context is already gone the
|
||||||
|
moment the function executes. Flask 0.9 provides you with a helper that
|
||||||
|
can keep the request context around during the execution of the
|
||||||
|
generator::
|
||||||
|
|
||||||
|
from flask import stream_with_context, request, Response
|
||||||
|
|
||||||
|
@app.route('/stream')
|
||||||
|
def streamed_response():
|
||||||
|
def generate():
|
||||||
|
yield 'Hello '
|
||||||
|
yield request.args['name']
|
||||||
|
yield '!'
|
||||||
|
return Response(stream_with_context(generate()))
|
||||||
|
|
||||||
|
Without the :func:`~flask.stream_with_context` function you would get a
|
||||||
|
:class:`RuntimeError` at that point.
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,8 @@ from .app import Flask, Request, Response
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .helpers import url_for, jsonify, json_available, flash, \
|
from .helpers import url_for, jsonify, json_available, flash, \
|
||||||
send_file, send_from_directory, get_flashed_messages, \
|
send_file, send_from_directory, get_flashed_messages, \
|
||||||
get_template_attribute, make_response, safe_join
|
get_template_attribute, make_response, safe_join, \
|
||||||
|
stream_with_context
|
||||||
from .globals import current_app, g, request, session, _request_ctx_stack, \
|
from .globals import current_app, g, request, session, _request_ctx_stack, \
|
||||||
_app_ctx_stack
|
_app_ctx_stack
|
||||||
from .ctx import has_request_context, has_app_context, \
|
from .ctx import has_request_context, has_app_context, \
|
||||||
|
|
|
||||||
64
flask/ctx.py
64
flask/ctx.py
|
|
@ -22,14 +22,6 @@ class _RequestGlobals(object):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _push_app_if_necessary(app):
|
|
||||||
top = _app_ctx_stack.top
|
|
||||||
if top is None or top.app != app:
|
|
||||||
ctx = app.app_context()
|
|
||||||
ctx.push()
|
|
||||||
return ctx
|
|
||||||
|
|
||||||
|
|
||||||
def after_this_request(f):
|
def after_this_request(f):
|
||||||
"""Executes a function after this request. This is useful to modify
|
"""Executes a function after this request. This is useful to modify
|
||||||
response objects. The function is passed the response object and has
|
response objects. The function is passed the response object and has
|
||||||
|
|
@ -110,15 +102,22 @@ class AppContext(object):
|
||||||
self.app = app
|
self.app = app
|
||||||
self.url_adapter = app.create_url_adapter(None)
|
self.url_adapter = app.create_url_adapter(None)
|
||||||
|
|
||||||
|
# Like request context, app contexts can be pushed multiple times
|
||||||
|
# but there a basic "refcount" is enough to track them.
|
||||||
|
self._refcnt = 0
|
||||||
|
|
||||||
def push(self):
|
def push(self):
|
||||||
"""Binds the app context to the current context."""
|
"""Binds the app context to the current context."""
|
||||||
|
self._refcnt += 1
|
||||||
_app_ctx_stack.push(self)
|
_app_ctx_stack.push(self)
|
||||||
|
|
||||||
def pop(self, exc=None):
|
def pop(self, exc=None):
|
||||||
"""Pops the app context."""
|
"""Pops the app context."""
|
||||||
if exc is None:
|
self._refcnt -= 1
|
||||||
exc = sys.exc_info()[1]
|
if self._refcnt <= 0:
|
||||||
self.app.do_teardown_appcontext(exc)
|
if exc is None:
|
||||||
|
exc = sys.exc_info()[1]
|
||||||
|
self.app.do_teardown_appcontext(exc)
|
||||||
rv = _app_ctx_stack.pop()
|
rv = _app_ctx_stack.pop()
|
||||||
assert rv is self, 'Popped wrong app context. (%r instead of %r)' \
|
assert rv is self, 'Popped wrong app context. (%r instead of %r)' \
|
||||||
% (rv, self)
|
% (rv, self)
|
||||||
|
|
@ -128,7 +127,7 @@ class AppContext(object):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, tb):
|
def __exit__(self, exc_type, exc_value, tb):
|
||||||
self.pop()
|
self.pop(exc_value)
|
||||||
|
|
||||||
|
|
||||||
class RequestContext(object):
|
class RequestContext(object):
|
||||||
|
|
@ -169,15 +168,16 @@ class RequestContext(object):
|
||||||
self.flashes = None
|
self.flashes = None
|
||||||
self.session = None
|
self.session = None
|
||||||
|
|
||||||
|
# Request contexts can be pushed multiple times and interleaved with
|
||||||
|
# other request contexts. Now only if the last level is popped we
|
||||||
|
# get rid of them. Additionally if an application context is missing
|
||||||
|
# one is created implicitly so for each level we add this information
|
||||||
|
self._implicit_app_ctx_stack = []
|
||||||
|
|
||||||
# indicator if the context was preserved. Next time another context
|
# indicator if the context was preserved. Next time another context
|
||||||
# is pushed the preserved context is popped.
|
# is pushed the preserved context is popped.
|
||||||
self.preserved = False
|
self.preserved = False
|
||||||
|
|
||||||
# Indicates if pushing this request context also triggered the pushing
|
|
||||||
# of an application context. If it implicitly pushed an application
|
|
||||||
# context, it will be stored there
|
|
||||||
self._pushed_application_context = None
|
|
||||||
|
|
||||||
# Functions that should be executed after the request on the response
|
# Functions that should be executed after the request on the response
|
||||||
# object. These will be called before the regular "after_request"
|
# object. These will be called before the regular "after_request"
|
||||||
# functions.
|
# functions.
|
||||||
|
|
@ -222,7 +222,13 @@ class RequestContext(object):
|
||||||
|
|
||||||
# Before we push the request context we have to ensure that there
|
# Before we push the request context we have to ensure that there
|
||||||
# is an application context.
|
# is an application context.
|
||||||
self._pushed_application_context = _push_app_if_necessary(self.app)
|
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)
|
||||||
|
|
||||||
_request_ctx_stack.push(self)
|
_request_ctx_stack.push(self)
|
||||||
|
|
||||||
|
|
@ -241,22 +247,28 @@ class RequestContext(object):
|
||||||
.. versionchanged:: 0.9
|
.. versionchanged:: 0.9
|
||||||
Added the `exc` argument.
|
Added the `exc` argument.
|
||||||
"""
|
"""
|
||||||
self.preserved = False
|
app_ctx = self._implicit_app_ctx_stack.pop()
|
||||||
if exc is None:
|
|
||||||
exc = sys.exc_info()[1]
|
clear_request = False
|
||||||
self.app.do_teardown_request(exc)
|
if not self._implicit_app_ctx_stack:
|
||||||
|
self.preserved = False
|
||||||
|
if exc is None:
|
||||||
|
exc = sys.exc_info()[1]
|
||||||
|
self.app.do_teardown_request(exc)
|
||||||
|
clear_request = True
|
||||||
|
|
||||||
rv = _request_ctx_stack.pop()
|
rv = _request_ctx_stack.pop()
|
||||||
assert rv is self, 'Popped wrong request context. (%r instead of %r)' \
|
assert rv is self, 'Popped wrong request context. (%r instead of %r)' \
|
||||||
% (rv, self)
|
% (rv, self)
|
||||||
|
|
||||||
# get rid of circular dependencies at the end of the request
|
# get rid of circular dependencies at the end of the request
|
||||||
# so that we don't require the GC to be active.
|
# so that we don't require the GC to be active.
|
||||||
rv.request.environ['werkzeug.request'] = None
|
if clear_request:
|
||||||
|
rv.request.environ['werkzeug.request'] = None
|
||||||
|
|
||||||
# Get rid of the app as well if necessary.
|
# Get rid of the app as well if necessary.
|
||||||
if self._pushed_application_context:
|
if app_ctx is not None:
|
||||||
self._pushed_application_context.pop(exc)
|
app_ctx.pop(exc)
|
||||||
self._pushed_application_context = None
|
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
self.push()
|
self.push()
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ from zlib import adler32
|
||||||
from threading import RLock
|
from threading import RLock
|
||||||
from werkzeug.routing import BuildError
|
from werkzeug.routing import BuildError
|
||||||
from werkzeug.urls import url_quote
|
from werkzeug.urls import url_quote
|
||||||
|
from functools import update_wrapper
|
||||||
|
|
||||||
# try to load the best simplejson implementation available. If JSON
|
# try to load the best simplejson implementation available. If JSON
|
||||||
# is not installed, we add a failing class.
|
# is not installed, we add a failing class.
|
||||||
|
|
@ -92,6 +93,78 @@ def _endpoint_from_view_func(view_func):
|
||||||
return view_func.__name__
|
return view_func.__name__
|
||||||
|
|
||||||
|
|
||||||
|
def stream_with_context(generator_or_function):
|
||||||
|
"""Request contexts disappear when the response is started on the server.
|
||||||
|
This is done for efficiency reasons and to make it less likely to encounter
|
||||||
|
memory leaks with badly written WSGI middlewares. The downside is that if
|
||||||
|
you are using streamed responses, the generator cannot access request bound
|
||||||
|
information any more.
|
||||||
|
|
||||||
|
This function however can help you keep the context around for longer::
|
||||||
|
|
||||||
|
from flask import stream_with_context, request, Response
|
||||||
|
|
||||||
|
@app.route('/stream')
|
||||||
|
def streamed_response():
|
||||||
|
@stream_with_context
|
||||||
|
def generate():
|
||||||
|
yield 'Hello '
|
||||||
|
yield request.args['name']
|
||||||
|
yield '!'
|
||||||
|
return Response(generate())
|
||||||
|
|
||||||
|
Alternatively it can also be used around a specific generator:
|
||||||
|
|
||||||
|
from flask import stream_with_context, request, Response
|
||||||
|
|
||||||
|
@app.route('/stream')
|
||||||
|
def streamed_response():
|
||||||
|
def generate():
|
||||||
|
yield 'Hello '
|
||||||
|
yield request.args['name']
|
||||||
|
yield '!'
|
||||||
|
return Response(stream_with_context(generate()))
|
||||||
|
|
||||||
|
.. versionadded:: 0.9
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
gen = iter(generator_or_function)
|
||||||
|
except TypeError:
|
||||||
|
def decorator(*args, **kwargs):
|
||||||
|
gen = generator_or_function()
|
||||||
|
return stream_with_context(gen)
|
||||||
|
return update_wrapper(decorator, generator_or_function)
|
||||||
|
|
||||||
|
def generator():
|
||||||
|
ctx = _request_ctx_stack.top
|
||||||
|
if ctx is None:
|
||||||
|
raise RuntimeError('Attempted to stream with context but '
|
||||||
|
'there was no context in the first place to keep around.')
|
||||||
|
with ctx:
|
||||||
|
# Dummy sentinel. Has to be inside the context block or we're
|
||||||
|
# not actually keeping the context around.
|
||||||
|
yield None
|
||||||
|
|
||||||
|
# The try/finally is here so that if someone passes a WSGI level
|
||||||
|
# iterator in we're still running the cleanup logic. Generators
|
||||||
|
# don't need that because they are closed on their destruction
|
||||||
|
# automatically.
|
||||||
|
try:
|
||||||
|
for item in gen:
|
||||||
|
yield item
|
||||||
|
finally:
|
||||||
|
if hasattr(gen, 'close'):
|
||||||
|
gen.close()
|
||||||
|
|
||||||
|
# The trick is to start the generator. Then the code execution runs until
|
||||||
|
# the first dummy None is yielded at which point the context was already
|
||||||
|
# pushed. This item is discarded. Then when the iteration continues the
|
||||||
|
# real generator is executed.
|
||||||
|
wrapped_g = generator()
|
||||||
|
wrapped_g.next()
|
||||||
|
return wrapped_g
|
||||||
|
|
||||||
|
|
||||||
def jsonify(*args, **kwargs):
|
def jsonify(*args, **kwargs):
|
||||||
"""Creates a :class:`~flask.Response` with the JSON representation of
|
"""Creates a :class:`~flask.Response` with the JSON representation of
|
||||||
the given arguments with an `application/json` mimetype. The arguments
|
the given arguments with an `application/json` mimetype. The arguments
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,26 @@ class AppContextTestCase(FlaskTestCase):
|
||||||
self.assert_equal(
|
self.assert_equal(
|
||||||
flask.render_template_string('{{ g.spam }}'), 'eggs')
|
flask.render_template_string('{{ g.spam }}'), 'eggs')
|
||||||
|
|
||||||
|
def test_context_refcounts(self):
|
||||||
|
called = []
|
||||||
|
app = flask.Flask(__name__)
|
||||||
|
@app.teardown_request
|
||||||
|
def teardown_req(error=None):
|
||||||
|
called.append('request')
|
||||||
|
@app.teardown_appcontext
|
||||||
|
def teardown_app(error=None):
|
||||||
|
called.append('app')
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
with flask._app_ctx_stack.top:
|
||||||
|
with flask._request_ctx_stack.top:
|
||||||
|
pass
|
||||||
|
self.assert_(flask._request_ctx_stack.request.environ
|
||||||
|
['werkzeug.request'] is not None)
|
||||||
|
c = app.test_client()
|
||||||
|
c.get('/')
|
||||||
|
self.assertEqual(called, ['request', 'app'])
|
||||||
|
|
||||||
|
|
||||||
def suite():
|
def suite():
|
||||||
suite = unittest.TestSuite()
|
suite = unittest.TestSuite()
|
||||||
|
|
|
||||||
|
|
@ -397,6 +397,64 @@ class NoImportsTestCase(FlaskTestCase):
|
||||||
self.fail('Flask(import_name) is importing import_name.')
|
self.fail('Flask(import_name) is importing import_name.')
|
||||||
|
|
||||||
|
|
||||||
|
class StreamingTestCase(FlaskTestCase):
|
||||||
|
|
||||||
|
def test_streaming_with_context(self):
|
||||||
|
app = flask.Flask(__name__)
|
||||||
|
app.testing = True
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
def generate():
|
||||||
|
yield 'Hello '
|
||||||
|
yield flask.request.args['name']
|
||||||
|
yield '!'
|
||||||
|
return flask.Response(flask.stream_with_context(generate()))
|
||||||
|
c = app.test_client()
|
||||||
|
rv = c.get('/?name=World')
|
||||||
|
self.assertEqual(rv.data, 'Hello World!')
|
||||||
|
|
||||||
|
def test_streaming_with_context_as_decorator(self):
|
||||||
|
app = flask.Flask(__name__)
|
||||||
|
app.testing = True
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
@flask.stream_with_context
|
||||||
|
def generate():
|
||||||
|
yield 'Hello '
|
||||||
|
yield flask.request.args['name']
|
||||||
|
yield '!'
|
||||||
|
return flask.Response(generate())
|
||||||
|
c = app.test_client()
|
||||||
|
rv = c.get('/?name=World')
|
||||||
|
self.assertEqual(rv.data, 'Hello World!')
|
||||||
|
|
||||||
|
def test_streaming_with_context_and_custom_close(self):
|
||||||
|
app = flask.Flask(__name__)
|
||||||
|
app.testing = True
|
||||||
|
called = []
|
||||||
|
class Wrapper(object):
|
||||||
|
def __init__(self, gen):
|
||||||
|
self._gen = gen
|
||||||
|
def __iter__(self):
|
||||||
|
return self
|
||||||
|
def close(self):
|
||||||
|
called.append(42)
|
||||||
|
def next(self):
|
||||||
|
return self._gen.next()
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
def generate():
|
||||||
|
yield 'Hello '
|
||||||
|
yield flask.request.args['name']
|
||||||
|
yield '!'
|
||||||
|
return flask.Response(flask.stream_with_context(
|
||||||
|
Wrapper(generate())))
|
||||||
|
c = app.test_client()
|
||||||
|
rv = c.get('/?name=World')
|
||||||
|
self.assertEqual(rv.data, 'Hello World!')
|
||||||
|
self.assertEqual(called, [42])
|
||||||
|
|
||||||
|
|
||||||
def suite():
|
def suite():
|
||||||
suite = unittest.TestSuite()
|
suite = unittest.TestSuite()
|
||||||
if flask.json_available:
|
if flask.json_available:
|
||||||
|
|
@ -404,4 +462,5 @@ def suite():
|
||||||
suite.addTest(unittest.makeSuite(SendfileTestCase))
|
suite.addTest(unittest.makeSuite(SendfileTestCase))
|
||||||
suite.addTest(unittest.makeSuite(LoggingTestCase))
|
suite.addTest(unittest.makeSuite(LoggingTestCase))
|
||||||
suite.addTest(unittest.makeSuite(NoImportsTestCase))
|
suite.addTest(unittest.makeSuite(NoImportsTestCase))
|
||||||
|
suite.addTest(unittest.makeSuite(StreamingTestCase))
|
||||||
return suite
|
return suite
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue