add generate_template and generate_template_string functions

This commit is contained in:
pgjones 2022-06-09 09:21:48 +01:00 committed by David Lord
parent 762382e436
commit 46433e9807
No known key found for this signature in database
GPG key ID: 7A1C87E3F5BC42A8
7 changed files with 139 additions and 37 deletions

View file

@ -40,6 +40,8 @@ Unreleased
value. :pr:`4645`
- View functions can return generators directly instead of wrapping
them in a ``Response``. :pr:`4629`
- Add ``stream_template`` and ``stream_template_string`` functions to
render a template as a stream of pieces. :pr:`4629`
Version 2.1.3

View file

@ -287,6 +287,10 @@ Template Rendering
.. autofunction:: render_template_string
.. autofunction:: stream_template
.. autofunction:: stream_template_string
.. autofunction:: get_template_attribute
Configuration

View file

@ -20,7 +20,7 @@ data and to then invoke that function and pass it to a response object::
def generate():
for row in iter_all_rows():
yield f"{','.join(row)}\n"
return app.response_class(generate(), mimetype='text/csv')
return generate(), {"Content-Type": "text/csv")
Each ``yield`` expression is directly sent to the browser. Note though
that some WSGI middlewares might break streaming, so be careful there in
@ -29,52 +29,57 @@ debug environments with profilers and other things you might have enabled.
Streaming from Templates
------------------------
The Jinja2 template engine also supports rendering templates piece by
piece. This functionality is not directly exposed by Flask because it is
quite uncommon, but you can easily do it yourself::
The Jinja2 template engine supports rendering a template piece by
piece, returning an iterator of strings. Flask provides the
:func:`~flask.stream_template` and :func:`~flask.stream_template_string`
functions to make this easier to use.
def stream_template(template_name, **context):
app.update_template_context(context)
t = app.jinja_env.get_template(template_name)
rv = t.stream(context)
rv.enable_buffering(5)
return rv
.. code-block:: python
@app.route('/my-large-page.html')
def render_large_template():
rows = iter_all_rows()
return app.response_class(stream_template('the_template.html', rows=rows))
from flask import stream_template
@app.get("/timeline")
def timeline():
return stream_template("timeline.html")
The parts yielded by the render stream tend to match statement blocks in
the template.
The trick here is to get the template object from the Jinja2 environment
on the application and to call :meth:`~jinja2.Template.stream` instead of
:meth:`~jinja2.Template.render` which returns a stream object instead of a
string. Since we're bypassing the Flask template render functions and
using the template object itself we have to make sure to update the render
context ourselves by calling :meth:`~flask.Flask.update_template_context`.
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
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.
Streaming with Context
----------------------
.. versionadded:: 0.9
The :data:`~flask.request` will not be active while the generator is
running, because the view has already returned at that point. If you try
to access ``request``, you'll get a ``RuntimeError``.
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::
If your generator function relies on data in ``request``, use the
:func:`~flask.stream_with_context` wrapper. This will keep the request
context active during the generator.
.. code-block:: python
from flask import stream_with_context, request
from markupsafe import escape
@app.route('/stream')
def streamed_response():
def generate():
yield 'Hello '
yield request.args['name']
yield '!'
return app.response_class(stream_with_context(generate()))
yield '<p>Hello '
yield escape(request.args['name'])
yield '!</p>'
return stream_with_context(generate())
Without the :func:`~flask.stream_with_context` function you would get a
:class:`RuntimeError` at that point.
It can also be used as a decorator.
.. code-block:: python
@stream_with_context
def generate():
...
return generate()
The :func:`~flask.stream_template` and
:func:`~flask.stream_template_string` functions automatically
use :func:`~flask.stream_with_context` if a request is active.

View file

@ -201,3 +201,29 @@ templates::
You could also build `format_price` as a template filter (see
:ref:`registering-filters`), but this demonstrates how to pass functions in a
context processor.
Streaming
---------
It can be useful to not render the whole template as one complete
string, instead render it as a stream, yielding smaller incremental
strings. This can be used for streaming HTML in chunks to speed up
initial page load, or to save memory when rendering a very large
template.
The Jinja2 template engine supports rendering a template piece
by piece, returning an iterator of strings. Flask provides the
:func:`~flask.stream_template` and :func:`~flask.stream_template_string`
functions to make this easier to use.
.. code-block:: python
from flask import stream_template
@app.get("/timeline")
def timeline():
return stream_template("timeline.html")
These functions automatically apply the
:func:`~flask.stream_with_context` wrapper if a request is active, so
that it remains available in the template.

View file

@ -41,5 +41,7 @@ from .signals import signals_available as signals_available
from .signals import template_rendered as template_rendered
from .templating import render_template as render_template
from .templating import render_template_string as render_template_string
from .templating import stream_template as stream_template
from .templating import stream_template_string as stream_template_string
__version__ = "2.2.0.dev0"

View file

@ -7,6 +7,9 @@ from jinja2 import TemplateNotFound
from .globals import _app_ctx_stack
from .globals import _request_ctx_stack
from .globals import current_app
from .globals import request
from .helpers import stream_with_context
from .signals import before_render_template
from .signals import template_rendered
@ -122,8 +125,6 @@ class DispatchingJinjaLoader(BaseLoader):
def _render(template: Template, context: dict, app: "Flask") -> str:
"""Renders the template and fires the signal"""
before_render_template.send(app, template=template, context=context)
rv = template.render(context)
template_rendered.send(app, template=template, context=context)
@ -164,3 +165,56 @@ def render_template_string(source: str, **context: t.Any) -> str:
ctx = _app_ctx_stack.top
ctx.app.update_template_context(context)
return _render(ctx.app.jinja_env.from_string(source), context, ctx.app)
def _stream(
app: "Flask", template: Template, context: t.Dict[str, t.Any]
) -> t.Iterator[str]:
app.update_template_context(context)
before_render_template.send(app, template=template, context=context)
def generate() -> t.Iterator[str]:
yield from template.generate(context)
template_rendered.send(app, template=template, context=context)
rv = generate()
# If a request context is active, keep it while generating.
if request:
rv = stream_with_context(rv)
return rv
def stream_template(
template_name_or_list: t.Union[str, Template, t.List[t.Union[str, Template]]],
**context: t.Any
) -> t.Iterator[str]:
"""Render a template by name with the given context as a stream.
This returns an iterator of strings, which can be used as a
streaming response from a view.
:param template_name_or_list: The name of the template to render. If
a list is given, the first name to exist will be rendered.
:param context: The variables to make available in the template.
.. versionadded:: 2.2
"""
app = current_app._get_current_object() # type: ignore[attr-defined]
template = app.jinja_env.get_or_select_template(template_name_or_list)
return _stream(app, template, context)
def stream_template_string(source: str, **context: t.Any) -> t.Iterator[str]:
"""Render a template from the given source string with the given
context as a stream. This returns an iterator of strings, which can
be used as a streaming response from a view.
:param source: The source code of the template to render.
:param context: The variables to make available in the template.
.. versionadded:: 2.2
"""
app = current_app._get_current_object() # type: ignore[attr-defined]
template = app.jinja_env.from_string(source)
return _stream(app, template, context)

View file

@ -29,6 +29,15 @@ def test_original_win(app, client):
assert rv.data == b"42"
def test_simple_stream(app, client):
@app.route("/")
def index():
return flask.stream_template_string("{{ config }}", config=42)
rv = client.get("/")
assert rv.data == b"42"
def test_request_less_rendering(app, app_ctx):
app.config["WORLD_NAME"] = "Special World"