forked from orbit-oss/flask
add generate_template and generate_template_string functions
This commit is contained in:
parent
762382e436
commit
46433e9807
7 changed files with 139 additions and 37 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -287,6 +287,10 @@ Template Rendering
|
|||
|
||||
.. autofunction:: render_template_string
|
||||
|
||||
.. autofunction:: stream_template
|
||||
|
||||
.. autofunction:: stream_template_string
|
||||
|
||||
.. autofunction:: get_template_attribute
|
||||
|
||||
Configuration
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue