From 59ab0c0ca0b2d86913102dace6813f42f947d772 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 23 Jun 2026 11:30:30 -0700 Subject: [PATCH] Clarify template context metadata names --- datasette/template_contexts.py | 2 +- datasette/views/__init__.py | 14 ++++---- datasette/views/database.py | 4 +-- datasette/views/row.py | 20 +++++------ datasette/views/table.py | 62 +++++++++++++++++----------------- docs/template_context_doc.py | 2 +- tests/test_template_context.py | 24 +++++++------ 7 files changed, 65 insertions(+), 63 deletions(-) diff --git a/datasette/template_contexts.py b/datasette/template_contexts.py index 5fe6a80e..232eb051 100644 --- a/datasette/template_contexts.py +++ b/datasette/template_contexts.py @@ -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 diff --git a/datasette/views/__init__.py b/datasette/views/__init__.py index e503ed35..6fba1c90 100644 --- a/datasette/views/__init__.py +++ b/datasette/views/__init__.py @@ -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 ) diff --git a/datasette/views/database.py b/datasette/views/database.py index 7409e7d3..0b4ca647 100644 --- a/datasette/views/database.py +++ b/datasette/views/database.py @@ -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"}) diff --git a/datasette/views/row.py b/datasette/views/row.py index ef39dce6..129216b9 100644 --- a/datasette/views/row.py +++ b/datasette/views/row.py @@ -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( diff --git a/datasette/views/table.py b/datasette/views/table.py index 1e182937..449c6216 100644 --- a/datasette/views/table.py +++ b/datasette/views/table.py @@ -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( diff --git a/docs/template_context_doc.py b/docs/template_context_doc.py index c3ec77e2..a5f4fb6f 100644 --- a/docs/template_context_doc.py +++ b/docs/template_context_doc.py @@ -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: diff --git a/tests/test_template_context.py b/tests/test_template_context.py index 8d78bf77..4c452774 100644 --- a/tests/test_template_context.py +++ b/tests/test_template_context.py @@ -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()