TableContext - table page now renders a documented Context dataclass

The table HTML view constructs a TableContext instead of an ad-hoc
dict, matching how the database and query pages already work. Fields
resolved by registered extras are declared with extra_field() so their
documentation lives on the Extra classes in table_extras.py; fields
added by the view code carry help metadata next to the view.

render_template() now converts Context dataclasses shallowly instead
of via dataclasses.asdict(), which deep-copied every value and would
fail on values like sqlite3.Row.

Keys not declared on TableContext - extras requested with ?_extra= on
the HTML page, or extra filter context from filters_from_request
plugins - are now dropped from the HTML template context rather than
passed through undocumented.

Refs #2127

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Simon Willison 2026-06-11 06:57:51 -07:00
commit 8b89a3aca8
4 changed files with 172 additions and 3 deletions

View file

@ -344,3 +344,28 @@ async def test_datasette_close_continues_past_db_error():
ds.close()
assert good._closed
assert ds._internal_database._closed
@pytest.mark.asyncio
async def test_datasette_render_template_dataclass_values_not_deep_copied():
# display_rows can contain values like sqlite3.Row that cannot be
# deep-copied, so render_template must convert Context dataclasses
# shallowly - https://github.com/simonw/datasette/issues/2127
class RefusesDeepCopy:
def __deepcopy__(self, memo):
raise RuntimeError("deepcopy not supported")
def __str__(self):
return "shallow-copied-value"
@dataclasses.dataclass
class ExampleContext(Context):
title: str
status: int
error: RefusesDeepCopy
context = ExampleContext(title="Hello", status=200, error=RefusesDeepCopy())
ds = Datasette(memory=True)
await ds.invoke_startup()
rendered = await ds.render_template("error.html", context)
assert "shallow-copied-value" in rendered

View file

@ -178,6 +178,14 @@ async def test_template_context_matches_documented_contract(
)
def test_table_context_fields_match_documented_contract():
from datasette.views.table import TableContext
assert {f.name for f in TableContext.documented_fields()} == {
key.name for key in PAGES["table"].documented_keys()
}
def test_base_context_keys_all_have_docs():
for key in BASE_CONTEXT_KEYS:
assert key.doc, "Base context key {} is missing docs".format(key.name)