13
13
from werkzeug .utils import redirect as _wz_redirect
14
14
from werkzeug .wrappers import Response as BaseResponse
15
15
16
+ from .globals import _cv_app
16
17
from .globals import _cv_request
17
18
from .globals import current_app
18
19
from .globals import request
@@ -62,35 +63,40 @@ def stream_with_context(
62
63
def stream_with_context (
63
64
generator_or_function : t .Iterator [t .AnyStr ] | t .Callable [..., t .Iterator [t .AnyStr ]],
64
65
) -> t .Iterator [t .AnyStr ] | t .Callable [[t .Iterator [t .AnyStr ]], t .Iterator [t .AnyStr ]]:
65
- """Request contexts disappear when the response is started on the server.
66
- This is done for efficiency reasons and to make it less likely to encounter
67
- memory leaks with badly written WSGI middlewares. The downside is that if
68
- you are using streamed responses, the generator cannot access request bound
69
- information any more.
66
+ """Wrap a response generator function so that it runs inside the current
67
+ request context. This keeps :data:`request`, :data:`session`, and :data:`g`
68
+ available, even though at the point the generator runs the request context
69
+ will typically have ended.
70
70
71
- This function however can help you keep the context around for longer::
71
+ Use it as a decorator on a generator function:
72
+
73
+ .. code-block:: python
72
74
73
75
from flask import stream_with_context, request, Response
74
76
75
- @app.route(' /stream' )
77
+ @app.get(" /stream" )
76
78
def streamed_response():
77
79
@stream_with_context
78
80
def generate():
79
- yield 'Hello '
80
- yield request.args['name']
81
- yield '!'
81
+ yield "Hello "
82
+ yield request.args["name"]
83
+ yield "!"
84
+
82
85
return Response(generate())
83
86
84
- Alternatively it can also be used around a specific generator::
87
+ Or use it as a wrapper around a created generator:
88
+
89
+ .. code-block:: python
85
90
86
91
from flask import stream_with_context, request, Response
87
92
88
- @app.route(' /stream' )
93
+ @app.get(" /stream" )
89
94
def streamed_response():
90
95
def generate():
91
- yield 'Hello '
92
- yield request.args['name']
93
- yield '!'
96
+ yield "Hello "
97
+ yield request.args["name"]
98
+ yield "!"
99
+
94
100
return Response(stream_with_context(generate()))
95
101
96
102
.. versionadded:: 0.9
@@ -105,35 +111,36 @@ def decorator(*args: t.Any, **kwargs: t.Any) -> t.Any:
105
111
106
112
return update_wrapper (decorator , generator_or_function ) # type: ignore[arg-type]
107
113
108
- def generator () -> t .Iterator [t .AnyStr | None ]:
109
- ctx = _cv_request .get (None )
110
- if ctx is None :
114
+ def generator () -> t .Iterator [t .AnyStr ]:
115
+ if (req_ctx := _cv_request .get (None )) is None :
111
116
raise RuntimeError (
112
117
"'stream_with_context' can only be used when a request"
113
118
" context is active, such as in a view function."
114
119
)
115
- with ctx :
116
- # Dummy sentinel. Has to be inside the context block or we're
117
- # not actually keeping the context around.
118
- yield None
119
-
120
- # The try/finally is here so that if someone passes a WSGI level
121
- # iterator in we're still running the cleanup logic. Generators
122
- # don't need that because they are closed on their destruction
123
- # automatically.
120
+
121
+ app_ctx = _cv_app .get ()
122
+ # Setup code below will run the generator to this point, so that the
123
+ # current contexts are recorded. The contexts must be pushed after,
124
+ # otherwise their ContextVar will record the wrong event loop during
125
+ # async view functions.
126
+ yield None # type: ignore[misc]
127
+
128
+ # Push the app context first, so that the request context does not
129
+ # automatically create and push a different app context.
130
+ with app_ctx , req_ctx :
124
131
try :
125
132
yield from gen
126
133
finally :
134
+ # Clean up in case the user wrapped a WSGI iterator.
127
135
if hasattr (gen , "close" ):
128
136
gen .close ()
129
137
130
- # The trick is to start the generator. Then the code execution runs until
131
- # the first dummy None is yielded at which point the context was already
132
- # pushed. This item is discarded. Then when the iteration continues the
133
- # real generator is executed.
138
+ # Execute the generator to the sentinel value. This ensures the context is
139
+ # preserved in the generator's state. Further iteration will push the
140
+ # context and yield from the original iterator.
134
141
wrapped_g = generator ()
135
142
next (wrapped_g )
136
- return wrapped_g # type: ignore[return-value]
143
+ return wrapped_g
137
144
138
145
139
146
def make_response (* args : t .Any ) -> Response :
0 commit comments