Skip to content

Commit 46433e9

Browse files
pgjonesdavidism
authored andcommitted
add generate_template and generate_template_string functions
1 parent 762382e commit 46433e9

File tree

7 files changed

+142
-40
lines changed

7 files changed

+142
-40
lines changed

CHANGES.rst

+2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ Unreleased
4040
value. :pr:`4645`
4141
- View functions can return generators directly instead of wrapping
4242
them in a ``Response``. :pr:`4629`
43+
- Add ``stream_template`` and ``stream_template_string`` functions to
44+
render a template as a stream of pieces. :pr:`4629`
4345

4446

4547
Version 2.1.3

docs/api.rst

+4
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,10 @@ Template Rendering
287287

288288
.. autofunction:: render_template_string
289289

290+
.. autofunction:: stream_template
291+
292+
.. autofunction:: stream_template_string
293+
290294
.. autofunction:: get_template_attribute
291295

292296
Configuration

docs/patterns/streaming.rst

+43-38
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ data and to then invoke that function and pass it to a response object::
2020
def generate():
2121
for row in iter_all_rows():
2222
yield f"{','.join(row)}\n"
23-
return app.response_class(generate(), mimetype='text/csv')
23+
return generate(), {"Content-Type": "text/csv")
2424

2525
Each ``yield`` expression is directly sent to the browser. Note though
2626
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.
2929
Streaming from Templates
3030
------------------------
3131

32-
The Jinja2 template engine also supports rendering templates piece by
33-
piece. This functionality is not directly exposed by Flask because it is
34-
quite uncommon, but you can easily do it yourself::
35-
36-
def stream_template(template_name, **context):
37-
app.update_template_context(context)
38-
t = app.jinja_env.get_template(template_name)
39-
rv = t.stream(context)
40-
rv.enable_buffering(5)
41-
return rv
42-
43-
@app.route('/my-large-page.html')
44-
def render_large_template():
45-
rows = iter_all_rows()
46-
return app.response_class(stream_template('the_template.html', rows=rows))
47-
48-
The trick here is to get the template object from the Jinja2 environment
49-
on the application and to call :meth:`~jinja2.Template.stream` instead of
50-
:meth:`~jinja2.Template.render` which returns a stream object instead of a
51-
string. Since we're bypassing the Flask template render functions and
52-
using the template object itself we have to make sure to update the render
53-
context ourselves by calling :meth:`~flask.Flask.update_template_context`.
54-
The template is then evaluated as the stream is iterated over. Since each
55-
time you do a yield the server will flush the content to the client you
56-
might want to buffer up a few items in the template which you can do with
57-
``rv.enable_buffering(size)``. ``5`` is a sane default.
32+
The Jinja2 template engine supports rendering a template piece by
33+
piece, returning an iterator of strings. Flask provides the
34+
:func:`~flask.stream_template` and :func:`~flask.stream_template_string`
35+
functions to make this easier to use.
36+
37+
.. code-block:: python
38+
39+
from flask import stream_template
40+
41+
@app.get("/timeline")
42+
def timeline():
43+
return stream_template("timeline.html")
44+
45+
The parts yielded by the render stream tend to match statement blocks in
46+
the template.
47+
5848

5949
Streaming with Context
6050
----------------------
6151

62-
.. versionadded:: 0.9
52+
The :data:`~flask.request` will not be active while the generator is
53+
running, because the view has already returned at that point. If you try
54+
to access ``request``, you'll get a ``RuntimeError``.
6355

64-
Note that when you stream data, the request context is already gone the
65-
moment the function executes. Flask 0.9 provides you with a helper that
66-
can keep the request context around during the execution of the
67-
generator::
56+
If your generator function relies on data in ``request``, use the
57+
:func:`~flask.stream_with_context` wrapper. This will keep the request
58+
context active during the generator.
59+
60+
.. code-block:: python
6861
6962
from flask import stream_with_context, request
63+
from markupsafe import escape
7064
7165
@app.route('/stream')
7266
def streamed_response():
7367
def generate():
74-
yield 'Hello '
75-
yield request.args['name']
76-
yield '!'
77-
return app.response_class(stream_with_context(generate()))
68+
yield '<p>Hello '
69+
yield escape(request.args['name'])
70+
yield '!</p>'
71+
return stream_with_context(generate())
72+
73+
It can also be used as a decorator.
74+
75+
.. code-block:: python
76+
77+
@stream_with_context
78+
def generate():
79+
...
80+
81+
return generate()
7882
79-
Without the :func:`~flask.stream_with_context` function you would get a
80-
:class:`RuntimeError` at that point.
83+
The :func:`~flask.stream_template` and
84+
:func:`~flask.stream_template_string` functions automatically
85+
use :func:`~flask.stream_with_context` if a request is active.

docs/templating.rst

+26
Original file line numberDiff line numberDiff line change
@@ -201,3 +201,29 @@ templates::
201201
You could also build `format_price` as a template filter (see
202202
:ref:`registering-filters`), but this demonstrates how to pass functions in a
203203
context processor.
204+
205+
Streaming
206+
---------
207+
208+
It can be useful to not render the whole template as one complete
209+
string, instead render it as a stream, yielding smaller incremental
210+
strings. This can be used for streaming HTML in chunks to speed up
211+
initial page load, or to save memory when rendering a very large
212+
template.
213+
214+
The Jinja2 template engine supports rendering a template piece
215+
by piece, returning an iterator of strings. Flask provides the
216+
:func:`~flask.stream_template` and :func:`~flask.stream_template_string`
217+
functions to make this easier to use.
218+
219+
.. code-block:: python
220+
221+
from flask import stream_template
222+
223+
@app.get("/timeline")
224+
def timeline():
225+
return stream_template("timeline.html")
226+
227+
These functions automatically apply the
228+
:func:`~flask.stream_with_context` wrapper if a request is active, so
229+
that it remains available in the template.

src/flask/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,7 @@
4141
from .signals import template_rendered as template_rendered
4242
from .templating import render_template as render_template
4343
from .templating import render_template_string as render_template_string
44+
from .templating import stream_template as stream_template
45+
from .templating import stream_template_string as stream_template_string
4446

4547
__version__ = "2.2.0.dev0"

src/flask/templating.py

+56-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88
from .globals import _app_ctx_stack
99
from .globals import _request_ctx_stack
10+
from .globals import current_app
11+
from .globals import request
12+
from .helpers import stream_with_context
1013
from .signals import before_render_template
1114
from .signals import template_rendered
1215

@@ -122,8 +125,6 @@ def list_templates(self) -> t.List[str]:
122125

123126

124127
def _render(template: Template, context: dict, app: "Flask") -> str:
125-
"""Renders the template and fires the signal"""
126-
127128
before_render_template.send(app, template=template, context=context)
128129
rv = template.render(context)
129130
template_rendered.send(app, template=template, context=context)
@@ -164,3 +165,56 @@ def render_template_string(source: str, **context: t.Any) -> str:
164165
ctx = _app_ctx_stack.top
165166
ctx.app.update_template_context(context)
166167
return _render(ctx.app.jinja_env.from_string(source), context, ctx.app)
168+
169+
170+
def _stream(
171+
app: "Flask", template: Template, context: t.Dict[str, t.Any]
172+
) -> t.Iterator[str]:
173+
app.update_template_context(context)
174+
before_render_template.send(app, template=template, context=context)
175+
176+
def generate() -> t.Iterator[str]:
177+
yield from template.generate(context)
178+
template_rendered.send(app, template=template, context=context)
179+
180+
rv = generate()
181+
182+
# If a request context is active, keep it while generating.
183+
if request:
184+
rv = stream_with_context(rv)
185+
186+
return rv
187+
188+
189+
def stream_template(
190+
template_name_or_list: t.Union[str, Template, t.List[t.Union[str, Template]]],
191+
**context: t.Any
192+
) -> t.Iterator[str]:
193+
"""Render a template by name with the given context as a stream.
194+
This returns an iterator of strings, which can be used as a
195+
streaming response from a view.
196+
197+
:param template_name_or_list: The name of the template to render. If
198+
a list is given, the first name to exist will be rendered.
199+
:param context: The variables to make available in the template.
200+
201+
.. versionadded:: 2.2
202+
"""
203+
app = current_app._get_current_object() # type: ignore[attr-defined]
204+
template = app.jinja_env.get_or_select_template(template_name_or_list)
205+
return _stream(app, template, context)
206+
207+
208+
def stream_template_string(source: str, **context: t.Any) -> t.Iterator[str]:
209+
"""Render a template from the given source string with the given
210+
context as a stream. This returns an iterator of strings, which can
211+
be used as a streaming response from a view.
212+
213+
:param source: The source code of the template to render.
214+
:param context: The variables to make available in the template.
215+
216+
.. versionadded:: 2.2
217+
"""
218+
app = current_app._get_current_object() # type: ignore[attr-defined]
219+
template = app.jinja_env.from_string(source)
220+
return _stream(app, template, context)

tests/test_templating.py

+9
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ def index():
2929
assert rv.data == b"42"
3030

3131

32+
def test_simple_stream(app, client):
33+
@app.route("/")
34+
def index():
35+
return flask.stream_template_string("{{ config }}", config=42)
36+
37+
rv = client.get("/")
38+
assert rv.data == b"42"
39+
40+
3241
def test_request_less_rendering(app, app_ctx):
3342
app.config["WORLD_NAME"] = "Special World"
3443

0 commit comments

Comments
 (0)