Clarify template context metadata names

This commit is contained in:
Simon Willison 2026-06-23 11:30:30 -07:00
commit 59ab0c0ca0
7 changed files with 65 additions and 63 deletions

View file

@ -7,7 +7,7 @@ the documentation lives next to the code it describes:
- Every page renders a Context dataclass defined in its view module
(DatabaseContext, QueryContext in views/database.py, TableContext in
views/table.py, RowContext in views/row.py). Fields added by view code
carry ``help`` metadata; fields declared with extra_field() take their
carry ``help`` metadata; fields declared with from_extra() take their
documentation from the description on the matching Extra class in
views/table_extras.py.
- The keys render_template() adds to every page are documented in

View file

@ -10,7 +10,7 @@ class ContextField:
from_extra: bool = False
def extra_field():
def from_extra():
"""
Declare a Context dataclass field whose value comes from a registered
Extra of the same name - its documentation is the Extra description,
@ -23,7 +23,7 @@ def extra_field():
class Context:
"Base class for all documented contexts"
# Set on subclasses whose extra_field() fields should be resolved
# Set on subclasses whose from_extra() fields should be resolved
# against the extras registry for this scope
extras_scope = None
@ -34,8 +34,8 @@ class Context:
for f in dataclasses.fields(cls):
if f.name.startswith("_"):
continue
from_extra = bool(f.metadata.get("from_extra"))
if from_extra:
is_from_extra = bool(f.metadata.get("from_extra"))
if is_from_extra:
help_text = cls._extra_description(f.name)
else:
help_text = f.metadata.get("help", "")
@ -44,7 +44,7 @@ class Context:
name=f.name,
type_name=getattr(f.type, "__name__", str(f.type)),
help=help_text,
from_extra=from_extra,
from_extra=is_from_extra,
)
)
return documented
@ -59,14 +59,14 @@ class Context:
extra_class = table_extra_registry.classes_by_name[name]
except KeyError:
raise KeyError(
"{}.{} is declared with extra_field() but there is no "
"{}.{} is declared with from_extra() but there is no "
"registered extra of that name".format(cls.__name__, name)
)
if cls.extras_scope is not None and not extra_class.available_for(
cls.extras_scope
):
raise ValueError(
"{}.{} is declared with extra_field() but the {} extra is "
"{}.{} is declared with from_extra() but the {} extra is "
"not available for scope {}".format(
cls.__name__, name, name, cls.extras_scope
)

View file

@ -258,7 +258,7 @@ class DatabaseView(View):
class DatabaseContext(Context):
"The page listing the tables, views and queries in a database, e.g. /fixtures."
template = "database.html"
documented_template = "database.html"
database: str = field(metadata={"help": "The name of the database"})
private: bool = field(
@ -341,7 +341,7 @@ class DatabaseContext(Context):
class QueryContext(Context):
"The page for arbitrary SQL queries (/database/-/query?sql=...) and stored queries (/database/query-name)."
template = "query.html"
documented_template = "query.html"
database: str = field(metadata={"help": "The name of the database being queried"})
database_color: str = field(metadata={"help": "The color of the database"})

View file

@ -30,7 +30,7 @@ from datasette.utils import (
)
from datasette.plugins import pm
from datasette.extras import extra_names_from_request, ExtraScope
from . import Context, extra_field
from . import Context, from_extra
from .table import (
display_columns_and_rows,
_table_page_data,
@ -43,19 +43,19 @@ from .table_extras import RowExtraContext, resolve_row_extras, table_extra_regis
class RowContext(Context):
"The page showing an individual row, e.g. /fixtures/facetable/1."
template = "row.html"
documented_template = "row.html"
extras_scope = ExtraScope.ROW
# Fields resolved by registered extras - their documentation comes
# from the description on each Extra class in table_extras.py
columns: list = extra_field()
database: str = extra_field()
database_color: str = extra_field()
foreign_key_tables: list = extra_field()
metadata: dict = extra_field()
primary_keys: list = extra_field()
private: bool = extra_field()
table: str = extra_field()
columns: list = from_extra()
database: str = from_extra()
database_color: str = from_extra()
foreign_key_tables: list = from_extra()
metadata: dict = from_extra()
primary_keys: list = from_extra()
private: bool = from_extra()
table: str = from_extra()
# Fields added by the view code
ok: bool = field(

View file

@ -49,7 +49,7 @@ import sqlite_utils
from dataclasses import dataclass, field, fields
from datasette.extras import ExtraScope
from . import Context, extra_field
from . import Context, from_extra
from .base import BaseView, DatasetteError, _error, stream_csv
from .database import QueryView
from .table_create_alter import (
@ -73,40 +73,40 @@ from .table_extras import (
class TableContext(Context):
"The page showing the rows in a table or SQL view, e.g. /fixtures/facetable."
template = "table.html"
documented_template = "table.html"
extras_scope = ExtraScope.TABLE
# Fields resolved by registered extras - their documentation comes
# from the description on each Extra class in table_extras.py
actions: callable = extra_field()
all_columns: list = extra_field()
columns: list = extra_field()
count: int = extra_field()
count_sql: str = extra_field()
custom_table_templates: list = extra_field()
database: str = extra_field()
database_color: str = extra_field()
display_columns: list = extra_field()
display_rows: list = extra_field()
expandable_columns: list = extra_field()
facet_results: dict = extra_field()
facets_timed_out: list = extra_field()
filters: Filters = extra_field()
form_hidden_args: list = extra_field()
human_description_en: str = extra_field()
is_view: bool = extra_field()
metadata: dict = extra_field()
next_url: str = extra_field()
primary_keys: list = extra_field()
private: bool = extra_field()
query: dict = extra_field()
renderers: dict = extra_field()
set_column_type_ui: dict = extra_field()
sorted_facet_results: list = extra_field()
suggested_facets: list = extra_field()
table: str = extra_field()
table_definition: str = extra_field()
view_definition: str = extra_field()
actions: callable = from_extra()
all_columns: list = from_extra()
columns: list = from_extra()
count: int = from_extra()
count_sql: str = from_extra()
custom_table_templates: list = from_extra()
database: str = from_extra()
database_color: str = from_extra()
display_columns: list = from_extra()
display_rows: list = from_extra()
expandable_columns: list = from_extra()
facet_results: dict = from_extra()
facets_timed_out: list = from_extra()
filters: Filters = from_extra()
form_hidden_args: list = from_extra()
human_description_en: str = from_extra()
is_view: bool = from_extra()
metadata: dict = from_extra()
next_url: str = from_extra()
primary_keys: list = from_extra()
private: bool = from_extra()
query: dict = from_extra()
renderers: dict = from_extra()
set_column_type_ui: dict = from_extra()
sorted_facet_results: list = from_extra()
suggested_facets: list = from_extra()
table: str = from_extra()
table_definition: str = from_extra()
view_definition: str = from_extra()
# Fields added by the view code
ok: bool = field(

View file

@ -27,7 +27,7 @@ def template_context(cog):
for klass in PAGES.values():
title = "{} page".format(klass.__name__.removesuffix("Context"))
intro = "{} Rendered using the ``{}`` template.".format(
klass.__doc__, klass.template
klass.__doc__, klass.documented_template
)
_section(cog, title, intro)
if klass.extras_scope is not None:

View file

@ -40,20 +40,22 @@ def test_context_class_fields_all_have_help(klass):
@pytest.mark.parametrize("klass", PAGES.values(), ids=lambda klass: klass.__name__)
def test_context_class_has_docstring_and_template(klass):
def test_context_class_has_docstring_and_documented_template(klass):
assert klass.__doc__, "{} is missing a docstring".format(klass.__name__)
assert klass.template, "{} is missing a template".format(klass.__name__)
assert klass.documented_template, "{} is missing a documented_template".format(
klass.__name__
)
def test_extra_field_documentation_comes_from_the_extra_class():
from datasette.views import extra_field
def test_from_extra_documentation_comes_from_the_extra_class():
from datasette.views import from_extra
from datasette.views.table_extras import CountExtra
@dataclass
class DemoContext(Context):
extras_scope = ExtraScope.TABLE
count: int = extra_field()
count: int = from_extra()
name: str = field(metadata={"help": "The name"})
fields = {f.name: f for f in DemoContext.documented_fields()}
@ -63,28 +65,28 @@ def test_extra_field_documentation_comes_from_the_extra_class():
assert not fields["name"].from_extra
def test_extra_field_must_match_a_registered_extra():
from datasette.views import extra_field
def test_from_extra_must_match_a_registered_extra():
from datasette.views import from_extra
@dataclass
class BadContext(Context):
extras_scope = ExtraScope.TABLE
not_a_real_extra: str = extra_field()
not_a_real_extra: str = from_extra()
with pytest.raises(KeyError):
BadContext.documented_fields()
def test_extra_field_must_be_available_for_the_scope():
from datasette.views import extra_field
def test_from_extra_must_be_available_for_the_scope():
from datasette.views import from_extra
@dataclass
class WrongScopeContext(Context):
extras_scope = ExtraScope.ROW
# count is a TABLE-scope extra, not available for ROW
count: int = extra_field()
count: int = from_extra()
with pytest.raises(ValueError):
WrongScopeContext.documented_fields()