Render templates using Jinja async mode

Closes #628
pool
Simon Willison 2019-11-14 15:14:22 -08:00 zatwierdzone przez GitHub
rodzic b51f258d00
commit 8c642f04e0
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
6 zmienionych plików z 43 dodań i 15 usunięć

Wyświetl plik

@ -583,7 +583,9 @@ class Datasette:
), ),
] ]
) )
self.jinja_env = Environment(loader=template_loader, autoescape=True) self.jinja_env = Environment(
loader=template_loader, autoescape=True, enable_async=True
)
self.jinja_env.filters["escape_css_string"] = escape_css_string self.jinja_env.filters["escape_css_string"] = escape_css_string
self.jinja_env.filters["quote_plus"] = lambda u: urllib.parse.quote_plus(u) self.jinja_env.filters["quote_plus"] = lambda u: urllib.parse.quote_plus(u)
self.jinja_env.filters["escape_sqlite"] = escape_sqlite self.jinja_env.filters["escape_sqlite"] = escape_sqlite
@ -730,5 +732,5 @@ class DatasetteRouter(AsgiRouter):
else: else:
template = self.ds.jinja_env.select_template(templates) template = self.ds.jinja_env.select_template(templates)
await asgi_send_html( await asgi_send_html(
send, template.render(info), status=status, headers=headers send, await template.render_async(info), status=status, headers=headers
) )

Wyświetl plik

@ -139,7 +139,7 @@ class BaseView(AsgiView):
extra_template_vars.update(extra_vars) extra_template_vars.update(extra_vars)
return Response.html( return Response.html(
template.render( await template.render_async(
{ {
**context, **context,
**{ **{

Wyświetl plik

@ -629,7 +629,9 @@ Function that returns a dictionary
If you return a function it will be executed. If it returns a dictionary those values will will be merged into the template context. If you return a function it will be executed. If it returns a dictionary those values will will be merged into the template context.
Function that returns an awaitable function that returns a dictionary Function that returns an awaitable function that returns a dictionary
You can also return a function which returns an awaitable function which returns a dictionary. This means you can execute additional SQL queries using ``datasette.execute()``. You can also return a function which returns an awaitable function which returns a dictionary.
Datasette runs Jinja2 in `async mode <https://jinja.palletsprojects.com/en/2.10.x/api/#async-support>`__, which means you can add awaitable functions to the template scope and they will be automatically awaited when they are rendered by the template.
Here's an example plugin that returns an authentication object from the ASGI scope: Here's an example plugin that returns an authentication object from the ASGI scope:
@ -641,20 +643,19 @@ Here's an example plugin that returns an authentication object from the ASGI sco
"auth": request.scope.get("auth") "auth": request.scope.get("auth")
} }
And here's an example which returns the current version of SQLite: And here's an example which adds a ``sql_first(sql_query)`` function which executes a SQL statement and returns the first column of the first row of results:
.. code-block:: python .. code-block:: python
@hookimpl @hookimpl
def extra_template_vars(datasette): def extra_template_vars(datasette, database):
async def inner(): async def sql_first(sql, dbname=None):
first_db = list(datasette.databases.keys())[0] dbname = dbname or database or next(iter(datasette.databases.keys()))
return { return (await datasette.execute(dbname, sql)).rows[0][0]
"sqlite_version": (
await datasette.execute(first_db, "select sqlite_version()") You can then use the new function in a template like so::
).rows[0][0]
} SQLite version: {{ sql_first("select sqlite_version()") }}
return inner
.. _plugin_register_output_renderer: .. _plugin_register_output_renderer:

Wyświetl plik

@ -446,13 +446,19 @@ def render_cell(value, database):
@hookimpl @hookimpl
def extra_template_vars(template, database, table, view_name, request, datasette): def extra_template_vars(template, database, table, view_name, request, datasette):
async def query_database(sql):
first_db = list(datasette.databases.keys())[0]
return (
await datasette.execute(first_db, sql)
).rows[0][0]
async def inner(): async def inner():
return { return {
"extra_template_vars_from_awaitable": json.dumps({ "extra_template_vars_from_awaitable": json.dumps({
"template": template, "template": template,
"scope_path": request.scope["path"], "scope_path": request.scope["path"],
"awaitable": True, "awaitable": True,
}, default=lambda b: b.decode("utf8")) }, default=lambda b: b.decode("utf8")),
"query_database": query_database,
} }
return inner return inner

Wyświetl plik

@ -1,5 +1,6 @@
from bs4 import BeautifulSoup as Soup from bs4 import BeautifulSoup as Soup
from .fixtures import app_client, make_app_client, TEMP_PLUGIN_SECRET_FILE # noqa from .fixtures import app_client, make_app_client, TEMP_PLUGIN_SECRET_FILE # noqa
from datasette.utils import sqlite3
import base64 import base64
import json import json
import os import os
@ -214,3 +215,20 @@ def test_plugins_extra_template_vars(restore_working_directory):
"awaitable": True, "awaitable": True,
"scope_path": "/-/metadata", "scope_path": "/-/metadata",
} == extra_template_vars_from_awaitable } == extra_template_vars_from_awaitable
def test_plugins_async_template_function(restore_working_directory):
for client in make_app_client(
template_dir=str(pathlib.Path(__file__).parent / "test_templates")
):
response = client.get("/-/metadata")
assert response.status == 200
extra_from_awaitable_function = (
Soup(response.body, "html.parser")
.select("pre.extra_from_awaitable_function")[0]
.text
)
expected = (
sqlite3.connect(":memory:").execute("select sqlite_version()").fetchone()[0]
)
assert expected == extra_from_awaitable_function

Wyświetl plik

@ -5,4 +5,5 @@
Test data for extra_template_vars: Test data for extra_template_vars:
<pre class="extra_template_vars">{{ extra_template_vars|safe }}</pre> <pre class="extra_template_vars">{{ extra_template_vars|safe }}</pre>
<pre class="extra_template_vars_from_awaitable">{{ extra_template_vars_from_awaitable|safe }}</pre> <pre class="extra_template_vars_from_awaitable">{{ extra_template_vars_from_awaitable|safe }}</pre>
<pre class="extra_from_awaitable_function">{{ query_database("select sqlite_version();") }}</pre>
{% endblock %} {% endblock %}