1313from werkzeug .utils import redirect as _wz_redirect
1414from werkzeug .wrappers import Response as BaseResponse
1515
16+ from .globals import _cv_app
1617from .globals import _cv_request
1718from .globals import current_app
1819from .globals import request
@@ -62,35 +63,40 @@ def stream_with_context(
6263def stream_with_context (
6364 generator_or_function : t .Iterator [t .AnyStr ] | t .Callable [..., t .Iterator [t .AnyStr ]],
6465) -> 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.
7070
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
7274
7375 from flask import stream_with_context, request, Response
7476
75- @app.route(' /stream' )
77+ @app.get(" /stream" )
7678 def streamed_response():
7779 @stream_with_context
7880 def generate():
79- yield 'Hello '
80- yield request.args['name']
81- yield '!'
81+ yield "Hello "
82+ yield request.args["name"]
83+ yield "!"
84+
8285 return Response(generate())
8386
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
8590
8691 from flask import stream_with_context, request, Response
8792
88- @app.route(' /stream' )
93+ @app.get(" /stream" )
8994 def streamed_response():
9095 def generate():
91- yield 'Hello '
92- yield request.args['name']
93- yield '!'
96+ yield "Hello "
97+ yield request.args["name"]
98+ yield "!"
99+
94100 return Response(stream_with_context(generate()))
95101
96102 .. versionadded:: 0.9
@@ -105,35 +111,36 @@ def decorator(*args: t.Any, **kwargs: t.Any) -> t.Any:
105111
106112 return update_wrapper (decorator , generator_or_function ) # type: ignore[arg-type]
107113
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 :
111116 raise RuntimeError (
112117 "'stream_with_context' can only be used when a request"
113118 " context is active, such as in a view function."
114119 )
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 :
124131 try :
125132 yield from gen
126133 finally :
134+ # Clean up in case the user wrapped a WSGI iterator.
127135 if hasattr (gen , "close" ):
128136 gen .close ()
129137
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.
134141 wrapped_g = generator ()
135142 next (wrapped_g )
136- return wrapped_g # type: ignore[return-value]
143+ return wrapped_g
137144
138145
139146def make_response (* args : t .Any ) -> Response :
0 commit comments