mirror of
https://github.com/simonw/datasette.git
synced 2026-06-24 17:54:35 +02:00
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:
parent
4e9556cc24
commit
435ff7fa88
4 changed files with 99 additions and 0 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
32
tests/test_template_context.py
Normal file
32
tests/test_template_context.py
Normal 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
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue