Merge branch 'stable'
This commit is contained in:
commit
330123258e
20 changed files with 548 additions and 437 deletions
|
|
@ -13,6 +13,7 @@ from werkzeug.exceptions import abort as _wz_abort
|
|||
from werkzeug.utils import redirect as _wz_redirect
|
||||
from werkzeug.wrappers import Response as BaseResponse
|
||||
|
||||
from .globals import _cv_app
|
||||
from .globals import _cv_request
|
||||
from .globals import current_app
|
||||
from .globals import request
|
||||
|
|
@ -62,35 +63,40 @@ def stream_with_context(
|
|||
def stream_with_context(
|
||||
generator_or_function: t.Iterator[t.AnyStr] | t.Callable[..., t.Iterator[t.AnyStr]],
|
||||
) -> t.Iterator[t.AnyStr] | t.Callable[[t.Iterator[t.AnyStr]], t.Iterator[t.AnyStr]]:
|
||||
"""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.
|
||||
"""Wrap a response generator function so that it runs inside the current
|
||||
request context. This keeps :data:`request`, :data:`session`, and :data:`g`
|
||||
available, even though at the point the generator runs the request context
|
||||
will typically have ended.
|
||||
|
||||
This function however can help you keep the context around for longer::
|
||||
Use it as a decorator on a generator function:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from flask import stream_with_context, request, Response
|
||||
|
||||
@app.route('/stream')
|
||||
@app.get("/stream")
|
||||
def streamed_response():
|
||||
@stream_with_context
|
||||
def generate():
|
||||
yield 'Hello '
|
||||
yield request.args['name']
|
||||
yield '!'
|
||||
yield "Hello "
|
||||
yield request.args["name"]
|
||||
yield "!"
|
||||
|
||||
return Response(generate())
|
||||
|
||||
Alternatively it can also be used around a specific generator::
|
||||
Or use it as a wrapper around a created generator:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from flask import stream_with_context, request, Response
|
||||
|
||||
@app.route('/stream')
|
||||
@app.get("/stream")
|
||||
def streamed_response():
|
||||
def generate():
|
||||
yield 'Hello '
|
||||
yield request.args['name']
|
||||
yield '!'
|
||||
yield "Hello "
|
||||
yield request.args["name"]
|
||||
yield "!"
|
||||
|
||||
return Response(stream_with_context(generate()))
|
||||
|
||||
.. versionadded:: 0.9
|
||||
|
|
@ -105,35 +111,36 @@ def stream_with_context(
|
|||
|
||||
return update_wrapper(decorator, generator_or_function) # type: ignore[arg-type]
|
||||
|
||||
def generator() -> t.Iterator[t.AnyStr | None]:
|
||||
ctx = _cv_request.get(None)
|
||||
if ctx is None:
|
||||
def generator() -> t.Iterator[t.AnyStr]:
|
||||
if (req_ctx := _cv_request.get(None)) is None:
|
||||
raise RuntimeError(
|
||||
"'stream_with_context' can only be used when a request"
|
||||
" context is active, such as in a view function."
|
||||
)
|
||||
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.
|
||||
app_ctx = _cv_app.get()
|
||||
# Setup code below will run the generator to this point, so that the
|
||||
# current contexts are recorded. The contexts must be pushed after,
|
||||
# otherwise their ContextVar will record the wrong event loop during
|
||||
# async view functions.
|
||||
yield None # type: ignore[misc]
|
||||
|
||||
# Push the app context first, so that the request context does not
|
||||
# automatically create and push a different app context.
|
||||
with app_ctx, req_ctx:
|
||||
try:
|
||||
yield from gen
|
||||
finally:
|
||||
# Clean up in case the user wrapped a WSGI iterator.
|
||||
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.
|
||||
# Execute the generator to the sentinel value. This ensures the context is
|
||||
# preserved in the generator's state. Further iteration will push the
|
||||
# context and yield from the original iterator.
|
||||
wrapped_g = generator()
|
||||
next(wrapped_g)
|
||||
return wrapped_g # type: ignore[return-value]
|
||||
return wrapped_g
|
||||
|
||||
|
||||
def make_response(*args: t.Any) -> Response:
|
||||
|
|
@ -398,7 +405,7 @@ def _prepare_send_file_kwargs(**kwargs: t.Any) -> dict[str, t.Any]:
|
|||
|
||||
|
||||
def send_file(
|
||||
path_or_file: os.PathLike[t.AnyStr] | str | t.BinaryIO,
|
||||
path_or_file: os.PathLike[t.AnyStr] | str | t.IO[bytes],
|
||||
mimetype: str | None = None,
|
||||
as_attachment: bool = False,
|
||||
download_name: str | None = None,
|
||||
|
|
|
|||
|
|
@ -521,7 +521,7 @@ class App(Scaffold):
|
|||
return os.path.join(prefix, "var", f"{self.name}-instance")
|
||||
|
||||
def create_global_jinja_loader(self) -> DispatchingJinjaLoader:
|
||||
"""Creates the loader for the Jinja2 environment. Can be used to
|
||||
"""Creates the loader for the Jinja environment. Can be used to
|
||||
override just the loader and keeping the rest unchanged. It's
|
||||
discouraged to override this function. Instead one should override
|
||||
the :meth:`jinja_loader` function instead.
|
||||
|
|
@ -744,7 +744,7 @@ class App(Scaffold):
|
|||
"""
|
||||
if callable(name):
|
||||
self.add_template_test(name)
|
||||
return name # type: ignore[return-value]
|
||||
return name
|
||||
|
||||
def decorator(f: T_template_test) -> T_template_test:
|
||||
self.add_template_test(f, name=name)
|
||||
|
|
|
|||
|
|
@ -520,7 +520,7 @@ class Blueprint(Scaffold):
|
|||
"""
|
||||
if callable(name):
|
||||
self.add_app_template_test(name)
|
||||
return name # type: ignore[return-value]
|
||||
return name
|
||||
|
||||
def decorator(f: T_template_test) -> T_template_test:
|
||||
self.add_app_template_test(f, name=name)
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ def _default_template_ctx_processor() -> dict[str, t.Any]:
|
|||
|
||||
|
||||
class Environment(BaseEnvironment):
|
||||
"""Works like a regular Jinja2 environment but has some additional
|
||||
"""Works like a regular Jinja environment but has some additional
|
||||
knowledge of how Flask's blueprint works so that it can prepend the
|
||||
name of the blueprint to referenced templates if necessary.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -240,10 +240,10 @@ class FlaskClient(Client):
|
|||
response.json_module = self.application.json # type: ignore[assignment]
|
||||
|
||||
# Re-push contexts that were preserved during the request.
|
||||
while self._new_contexts:
|
||||
cm = self._new_contexts.pop()
|
||||
for cm in self._new_contexts:
|
||||
self._context_stack.enter_context(cm)
|
||||
|
||||
self._new_contexts.clear()
|
||||
return response
|
||||
|
||||
def __enter__(self) -> FlaskClient:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue