Context.documented_fields() and extras doc-metadata enforcement

- Context dataclasses now expose documented_fields(), returning
  ContextField(name, type_name, help) for each field
- ExtraRegistry.internal_classes_for_scope() returns the Extra classes
  that are available to HTML templates but excluded from JSON
- Tests enforce that every registered Extra has a description and every
  DatabaseContext/QueryContext field has help metadata

Refs #1510, #2127

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Simon Willison 2026-06-10 23:51:09 -07:00
commit 435ff7fa88
4 changed files with 99 additions and 0 deletions

View file

@ -81,6 +81,16 @@ class ExtraRegistry:
def public_classes_for_scope(self, scope):
return self.classes_for_scope(scope, include_internal=False)
def internal_classes_for_scope(self, scope):
# Extras that are available to HTML templates but excluded from
# JSON responses - plain Providers are dependency plumbing and
# never surface as keys, so they are not included
return [
cls
for cls in self.classes_for_scope(scope)
if issubclass(cls, Extra) and not cls.public
]
def _registry_for_scope(self, scope):
registry = self._scope_registries.get(scope)
if registry is None:

View file

@ -1,2 +1,27 @@
from dataclasses import dataclass
import dataclasses
@dataclass(frozen=True)
class ContextField:
name: str
type_name: str
help: str
class Context:
"Base class for all documented contexts"
@classmethod
def documented_fields(cls):
"List of ContextField describing the documented fields of this context"
documented = []
for f in dataclasses.fields(cls):
documented.append(
ContextField(
name=f.name,
type_name=getattr(f.type, "__name__", str(f.type)),
help=f.metadata.get("help", ""),
)
)
return documented

View file

@ -23,6 +23,38 @@ class DependentExtra(Extra):
return slow_value + 1
class InternalOnlyExtra(Extra):
description = "Internal extra for HTML templates only"
scopes = {ExtraScope.TABLE}
public = False
async def resolve(self, context):
return "internal"
def test_internal_classes_for_scope():
registry = ExtraRegistry([SlowValueExtra, DependentExtra, InternalOnlyExtra])
assert registry.internal_classes_for_scope(ExtraScope.TABLE) == [InternalOnlyExtra]
assert registry.public_classes_for_scope(ExtraScope.TABLE) == [
SlowValueExtra,
DependentExtra,
]
def _registered_extra_classes():
# Plain Providers are internal dependency plumbing, only Extra
# subclasses surface as documented JSON/template keys
from datasette.views.table_extras import table_extra_registry
return [cls for cls in table_extra_registry.classes if issubclass(cls, Extra)]
@pytest.mark.parametrize("cls", _registered_extra_classes(), ids=lambda cls: cls.key())
def test_registered_extras_have_descriptions(cls):
# Every registered extra is part of the documented template/JSON contract
assert cls.description, "{} is missing a description".format(cls.__name__)
def test_registry_is_built_once_per_scope():
registry = ExtraRegistry([SlowValueExtra, DependentExtra])
first = registry._registry_for_scope(ExtraScope.TABLE)

View file

@ -0,0 +1,32 @@
"""
Tests for the documented template context - the contract that custom
template authors can rely on for Datasette 1.0.
"""
from dataclasses import dataclass, field
import pytest
from datasette.views import Context
from datasette.views.database import DatabaseContext, QueryContext
def test_documented_fields():
@dataclass
class DemoContext(Context):
name: str = field(metadata={"help": "The name"})
count: int = field(metadata={"help": "How many there are"})
fields = DemoContext.documented_fields()
assert [(f.name, f.type_name, f.help) for f in fields] == [
("name", "str", "The name"),
("count", "int", "How many there are"),
]
@pytest.mark.parametrize("klass", (DatabaseContext, QueryContext))
def test_context_dataclass_fields_all_have_help(klass):
for context_field in klass.documented_fields():
assert context_field.help, "{}.{} is missing help metadata".format(
klass.__name__, context_field.name
)