From cb0159e6251141d1454728a351dce89ca42de386 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 22:05:39 +0000 Subject: [PATCH] Row JSON: drop truncated, render_cell is a single object The default row shape no longer includes "truncated", since a single row lookup cannot be truncated, and ?_extra=render_cell returns one object to match "row". With a ?_shape= both behave as they do for tables. Refs #2612 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GaqG8H8ajwxizKXdbGrtve --- datasette/renderer.py | 7 ++++++- datasette/views/table_extras.py | 9 +++++---- docs/json_api.rst | 10 ++++------ tests/test_api.py | 19 ++++++++++++------- tests/test_column_types.py | 2 +- 5 files changed, 28 insertions(+), 19 deletions(-) diff --git a/datasette/renderer.py b/datasette/renderer.py index 1d957b8d..757ba594 100644 --- a/datasette/renderer.py +++ b/datasette/renderer.py @@ -58,7 +58,8 @@ def json_renderer(request, args, data, error, truncated=None, view_name=None): status_code = 400 data.update(error_body(error, status_code)) - if truncated is not None: + # A single row lookup cannot be truncated + if truncated is not None and not single_row: data["truncated"] = truncated if shape == "arrayfirst": # Rows can be dicts, sqlite3.Row or lists (from remove_infinites) @@ -78,6 +79,10 @@ def json_renderer(request, args, data, error, truncated=None, view_name=None): ("row", row) if key == "rows" else (key, value) for key, value in data.items() ) + # Likewise the render_cell extra becomes a single object + if "render_cell" in data: + cells = data["render_cell"] + data["render_cell"] = cells[0] if cells else {} if shape == "object": shape_error = None if "primary_keys" not in data: diff --git a/datasette/views/table_extras.py b/datasette/views/table_extras.py index 48eaa35f..bd56857b 100644 --- a/datasette/views/table_extras.py +++ b/datasette/views/table_extras.py @@ -648,12 +648,13 @@ class RenderCellExtra(Extra): ExtraScope.ROW: ExtraExample( value={ "row": {"id": 4, "content": "RENDER_CELL_DEMO"}, - "render_cell": [{"content": "Custom rendered HTML"}], + "render_cell": {"content": "Custom rendered HTML"}, }, note=( - "The ``render_cell`` array has one item for the requested row. " - "The object is keyed by column name. Only columns whose rendered " - "value differs from the default are included." + "The ``render_cell`` object is keyed by column name. Only columns " + "whose rendered value differs from the default are included. If a " + "``?_shape=`` is specified ``render_cell`` is instead a list with " + "one item, matching the ``rows`` list." ), ), ExtraScope.QUERY: ExtraExample( diff --git a/docs/json_api.rst b/docs/json_api.rst index 02bd6d9e..8627a974 100644 --- a/docs/json_api.rst +++ b/docs/json_api.rst @@ -960,7 +960,7 @@ The following extras are available for row JSON responses. ``render_cell`` Rendered HTML for each cell using the render_cell plugin hook (See the :ref:`render_cell() plugin hook ` documentation.) - The ``render_cell`` array has one item for the requested row. The object is keyed by column name. Only columns whose rendered value differs from the default are included. + The ``render_cell`` object is keyed by column name. Only columns whose rendered value differs from the default are included. If a ``?_shape=`` is specified ``render_cell`` is instead a list with one item, matching the ``rows`` list. .. code-block:: json @@ -969,11 +969,9 @@ The following extras are available for row JSON responses. "id": 4, "content": "RENDER_CELL_DEMO" }, - "render_cell": [ - { - "content": "Custom rendered HTML" - } - ] + "render_cell": { + "content": "Custom rendered HTML" + } } ``debug`` diff --git a/tests/test_api.py b/tests/test_api.py index dd2365b6..8b02aeb9 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -369,6 +369,7 @@ async def test_row(ds_client): assert data["ok"] is True assert data["row"] == {"id": 1, "content": "hello"} assert "rows" not in data + assert "truncated" not in data @pytest.mark.asyncio @@ -391,6 +392,7 @@ async def test_row_shape(ds_client, shape, expected): data = response.json() if shape in ("objects", "arrays"): assert "row" not in data + assert data["truncated"] is False data = data["rows"] assert data == expected @@ -650,20 +652,23 @@ async def test_row_extra_render_cell(): assert "render_cell" in data assert "row" in data - # render_cell should be a list with one row (since this is a row page) + # render_cell is a single object to match "row" # Only columns modified by plugins are included (sparse output) - render_cell = data["render_cell"] - assert len(render_cell) == 1 - - # The row: id=1, name='Alice' # The 'name' column should be rendered by our plugin as Alice - assert render_cell[0]["name"] == "Alice" # The 'id' column is not included since no plugin modified it - assert "id" not in render_cell[0] + assert data["render_cell"] == {"name": "Alice"} # The regular row should still contain raw values assert data["row"] == {"id": 1, "name": "Alice"} + # With a ?_shape= render_cell is a list, matching "rows" + response = await ds.client.get( + "/test_row_render/test_render/1.json?_extra=render_cell&_shape=objects" + ) + data = response.json() + assert data["render_cell"] == [{"name": "Alice"}] + assert data["rows"] == [{"id": 1, "name": "Alice"}] + finally: ds.pm.unregister(name="TestRenderCellPlugin") diff --git a/tests/test_column_types.py b/tests/test_column_types.py index d8dfc627..df20db00 100644 --- a/tests/test_column_types.py +++ b/tests/test_column_types.py @@ -748,7 +748,7 @@ async def test_row_endpoint_render_cell_with_column_types(ds_ct): response = await ds_ct.client.get("/data/posts/1.json?_extra=render_cell") assert response.status_code == 200 data = response.json() - rendered = data["render_cell"][0] + rendered = data["render_cell"] assert "mailto:" in rendered["author_email"] assert "href" in rendered["website"]