can_render mechanism for register_output_renderer, closes #770

pull/783/head
Simon Willison 2020-05-27 22:57:05 -07:00
rodzic 75cd432e5a
commit 5ab411c733
6 zmienionych plików z 108 dodań i 18 usunięć

Wyświetl plik

@ -228,7 +228,7 @@ class Datasette:
if config_dir and (config_dir / "config.json").exists() and not config:
config = json.load((config_dir / "config.json").open())
self._config = dict(DEFAULT_CONFIG, **(config or {}))
self.renderers = {} # File extension -> renderer function
self.renderers = {} # File extension -> (renderer, can_render) functions
self.version_note = version_note
self.executor = futures.ThreadPoolExecutor(
max_workers=self.config("num_sql_threads")
@ -574,7 +574,7 @@ class Datasette:
def register_renderers(self):
""" Register output renderers which output data in custom formats. """
# Built-in renderers
self.renderers["json"] = json_renderer
self.renderers["json"] = (json_renderer, lambda: True)
# Hooks
hook_renderers = []
@ -588,8 +588,8 @@ class Datasette:
for renderer in hook_renderers:
self.renderers[renderer["extension"]] = (
# It used to be called "callback" - remove this in Datasette 1.0
renderer.get("render")
or renderer["callback"]
renderer.get("render") or renderer["callback"],
renderer.get("can_render") or (lambda: True),
)
async def render_template(

Wyświetl plik

@ -811,6 +811,10 @@ def call_with_supported_arguments(fn, **kwargs):
call_with = []
for parameter in parameters:
if parameter not in kwargs:
raise TypeError("{} requires parameters {}".format(fn, tuple(parameters)))
raise TypeError(
"{} requires parameters {}, missing: {}".format(
fn, tuple(parameters), set(parameters) - set(kwargs.keys())
)
)
call_with.append(kwargs[parameter])
return fn(*call_with)

Wyświetl plik

@ -389,7 +389,7 @@ class DataView(BaseView):
# Dispatch request to the correct output format renderer
# (CSV is not handled here due to streaming)
result = call_with_supported_arguments(
self.ds.renderers[_format],
self.ds.renderers[_format][0],
datasette=self.ds,
columns=data.get("columns") or [],
rows=data.get("rows") or [],
@ -426,10 +426,27 @@ class DataView(BaseView):
if data.get("expandable_columns"):
url_labels_extra = {"_labels": "on"}
renderers = {
key: path_with_format(request, key, {**url_labels_extra})
for key in self.ds.renderers.keys()
}
renderers = {}
for key, (_, can_render) in self.ds.renderers.items():
it_can_render = call_with_supported_arguments(
can_render,
datasette=self.ds,
columns=data.get("columns") or [],
rows=data.get("rows") or [],
sql=data.get("query", {}).get("sql", None),
query_name=data.get("query_name"),
database=database,
table=data.get("table"),
request=request,
view_name=self.name,
)
if asyncio.iscoroutine(it_can_render):
it_can_render = await it_can_render
if it_can_render:
renderers[key] = path_with_format(
request, key, {**url_labels_extra}
)
url_csv_args = {"_size": "max", **url_labels_extra}
url_csv = path_with_format(request, "csv", url_csv_args)
url_csv_path = url_csv.split("?")[0]

Wyświetl plik

@ -744,14 +744,17 @@ Registers a new output renderer, to output data in a custom format. The hook fun
def register_output_renderer(datasette):
return {
"extension": "test",
"render": render_test
"render": render_demo,
"can_render": can_render_demo, # Optional
}
This will register ``render_test`` to be called when paths with the extension ``.test`` (for example ``/database.test``, ``/database/table.test``, or ``/database/table/row.test``) are requested.
This will register ``render_demo`` to be called when paths with the extension ``.test`` (for example ``/database.test``, ``/database/table.test``, or ``/database/table/row.test``) are requested.
``render_test`` is a Python function. It can be a regular function or an ``async def render_test()`` awaitable function, depending on if it needs to make any asynchronous calls.
``render_demo`` is a Python function. It can be a regular function or an ``async def render_demo()`` awaitable function, depending on if it needs to make any asynchronous calls.
When a request is received, the callback function is called with zero or more of the following arguments. Datasette will inspect your callback function and pass arguments that match its function signature.
``can_render_demo`` is a Python function (or ``async def`` function) which acepts the same arguments as ``render_demo`` but just returns ``True`` or ``False``. It lets Datasette know if the current SQL query can be represented by the plugin - and hence influnce if a link to this output format is displayed in the user interface. If you omit the ``"can_render"`` key from the dictionary every query will be treated as being supported by the plugin.
When a request is received, the ``"render"`` callback function is called with zero or more of the following arguments. Datasette will inspect your callback function and pass arguments that match its function signature.
``datasette`` - :ref:`internals_datasette`
For accessing plugin configuration and executing queries.
@ -798,7 +801,7 @@ A simple example of an output renderer callback function:
.. code-block:: python
def render_test():
def render_demo():
return {
"body": "Hello World"
}
@ -807,7 +810,7 @@ Here is a more complex example:
.. code-block:: python
async def render_test(datasette, columns, rows):
async def render_demo(datasette, columns, rows):
db = next(iter(datasette.databases.values()))
result = await db.execute("select sqlite_version()")
first_row = " | ".join(columns)
@ -821,6 +824,13 @@ Here is a more complex example:
"headers": {"x-sqlite-version": result.first()[0]},
}
And here is an example ``can_render`` function which returns ``True`` only if the query results contain the columns ``atom_id``, ``atom_title`` and ``atom_updated``:
.. code-block:: python
def can_render_demo(columns):
return {"atom_id", "atom_title", "atom_updated"}.issubset(columns)
Examples: `datasette-atom <https://github.com/simonw/datasette-atom>`_, `datasette-ics <https://github.com/simonw/datasette-ics>`_
.. _plugin_register_facet_classes:

Wyświetl plik

@ -2,6 +2,26 @@ from datasette import hookimpl
import json
async def can_render(
datasette, columns, rows, sql, query_name, database, table, request, view_name
):
# We stash this on datasette so the calling unit test can see it
datasette._can_render_saw = {
"datasette": datasette,
"columns": columns,
"rows": rows,
"sql": sql,
"query_name": query_name,
"database": database,
"table": table,
"request": request,
"view_name": view_name,
}
if request.args.get("_no_can_render"):
return False
return True
async def render_test_all_parameters(
datasette, columns, rows, sql, query_name, database, table, request, view_name, data
):
@ -39,6 +59,10 @@ def render_test_no_parameters():
@hookimpl
def register_output_renderer(datasette):
return [
{"extension": "testall", "render": render_test_all_parameters},
{
"extension": "testall",
"render": render_test_all_parameters,
"can_render": can_render,
},
{"extension": "testnone", "callback": render_test_no_parameters},
]

Wyświetl plik

@ -9,7 +9,7 @@ from .fixtures import (
from datasette.app import Datasette
from datasette import cli
from datasette.plugins import get_plugins, DEFAULT_PLUGINS, pm
from datasette.utils import sqlite3
from datasette.utils import sqlite3, CustomRow
from jinja2.environment import Template
import base64
import json
@ -411,6 +411,41 @@ def test_register_output_renderer_custom_headers(app_client):
assert "2" == response.headers["x-gosh"]
def test_register_output_renderer_can_render(app_client):
response = app_client.get("/fixtures/facetable?_no_can_render=1")
assert response.status == 200
links = (
Soup(response.body, "html.parser")
.find("p", {"class": "export-links"})
.findAll("a")
)
actual = [l["href"].split("/")[-1] for l in links]
# Should not be present because we sent ?_no_can_render=1
assert "facetable.testall?_labels=on" not in actual
# Check that it was passed the values we expected
assert hasattr(app_client.ds, "_can_render_saw")
assert {
"datasette": app_client.ds,
"columns": [
"pk",
"created",
"planet_int",
"on_earth",
"state",
"city_id",
"neighborhood",
"tags",
"complex_array",
"distinct_some_null",
],
"sql": "select pk, created, planet_int, on_earth, state, city_id, neighborhood, tags, complex_array, distinct_some_null from facetable order by pk limit 51",
"query_name": None,
"database": "fixtures",
"table": "facetable",
"view_name": "table",
}.items() <= app_client.ds._can_render_saw.items()
@pytest.mark.asyncio
async def test_prepare_jinja2_environment(app_client):
template = app_client.ds.jinja_env.from_string(