From 799ecae94824640bdff21f86997f69844048d5c3 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 27 Aug 2020 21:02:50 -0700 Subject: [PATCH] register_output_renderer can now return Response, closes #953 --- datasette/views/base.py | 18 +++++++++++------- docs/plugin_hooks.rst | 18 +++++++++--------- tests/plugins/register_output_renderer.py | 8 ++++++++ tests/test_html.py | 2 ++ tests/test_plugins.py | 12 ++++++++++++ 5 files changed, 42 insertions(+), 16 deletions(-) diff --git a/datasette/views/base.py b/datasette/views/base.py index a1f38f21..fa730af8 100644 --- a/datasette/views/base.py +++ b/datasette/views/base.py @@ -455,13 +455,17 @@ class DataView(BaseView): result = await result if result is None: raise NotFound("No data") - - r = Response( - body=result.get("body"), - status=result.get("status_code", 200), - content_type=result.get("content_type", "text/plain"), - headers=result.get("headers"), - ) + if isinstance(result, dict): + r = Response( + body=result.get("body"), + status=result.get("status_code", 200), + content_type=result.get("content_type", "text/plain"), + headers=result.get("headers"), + ) + elif isinstance(result, Response): + r = result + else: + assert False, "{} should be dict or Response".format(result) else: extras = {} if callable(extra_template_data): diff --git a/docs/plugin_hooks.rst b/docs/plugin_hooks.rst index 96a1cd7f..fc710a2b 100644 --- a/docs/plugin_hooks.rst +++ b/docs/plugin_hooks.rst @@ -455,7 +455,9 @@ When a request is received, the ``"render"`` callback function is called with ze ``view_name`` - string The name of the current view being called. ``index``, ``database``, ``table``, and ``row`` are the most important ones. -The callback function can return ``None``, if it is unable to render the data, or a dictionary with the following keys: +The callback function can return ``None``, if it is unable to render the data, or a :ref:`internals_response` that will be returned to the caller. + +It can also return a dictionary with the following keys. This format is **deprecated** as-of Datasette 0.49 and will be removed by Datasette 1.0. ``body`` - string or bytes, optional The response body, default empty @@ -474,9 +476,7 @@ A simple example of an output renderer callback function: .. code-block:: python def render_demo(): - return { - "body": "Hello World" - } + return Response.text("Hello World") Here is a more complex example: @@ -490,11 +490,11 @@ Here is a more complex example: lines.append("=" * len(first_row)) for row in rows: lines.append(" | ".join(row)) - return { - "body": "\n".join(lines), - "content_type": "text/plain; charset=utf-8", - "headers": {"x-sqlite-version": result.first()[0]}, - } + return Response( + "\n".join(lines), + content_type="text/plain; charset=utf-8", + 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``: diff --git a/tests/plugins/register_output_renderer.py b/tests/plugins/register_output_renderer.py index 82b60d01..cfe15215 100644 --- a/tests/plugins/register_output_renderer.py +++ b/tests/plugins/register_output_renderer.py @@ -1,4 +1,5 @@ from datasette import hookimpl +from datasette.utils.asgi import Response import json @@ -56,6 +57,12 @@ def render_test_no_parameters(): return {"body": "Hello"} +async def render_response(request): + if request.args.get("_broken"): + return "this should break" + return Response.json({"this_is": "json"}) + + @hookimpl def register_output_renderer(datasette): return [ @@ -65,4 +72,5 @@ def register_output_renderer(datasette): "can_render": can_render, }, {"extension": "testnone", "callback": render_test_no_parameters}, + {"extension": "testresponse", "render": render_response}, ] diff --git a/tests/test_html.py b/tests/test_html.py index 1a12b3ce..aec4db1d 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -559,6 +559,7 @@ def test_table_csv_json_export_interface(app_client): "simple_primary_key.json?id__gt=2", "simple_primary_key.testall?id__gt=2", "simple_primary_key.testnone?id__gt=2", + "simple_primary_key.testresponse?id__gt=2", "simple_primary_key.csv?id__gt=2&_size=max", "#export", ] @@ -597,6 +598,7 @@ def test_csv_json_export_links_include_labels_if_foreign_keys(app_client): "facetable.json?_labels=on", "facetable.testall?_labels=on", "facetable.testnone?_labels=on", + "facetable.testresponse?_labels=on", "facetable.csv?_labels=on&_size=max", "#export", ] diff --git a/tests/test_plugins.py b/tests/test_plugins.py index c535810c..f2017f07 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -479,6 +479,18 @@ def test_hook_register_output_renderer_custom_headers(app_client): assert "2" == response.headers["x-gosh"] +def test_hook_register_output_renderer_returning_response(app_client): + response = app_client.get("/fixtures/facetable.testresponse") + assert 200 == response.status + assert response.json == {"this_is": "json"} + + +def test_hook_register_output_renderer_returning_broken_value(app_client): + response = app_client.get("/fixtures/facetable.testresponse?_broken=1") + assert 500 == response.status + assert "this should break should be dict or Response" in response.text + + def test_hook_register_output_renderer_can_render(app_client): response = app_client.get("/fixtures/facetable?_no_can_render=1") assert response.status == 200