mirror of
https://github.com/simonw/datasette.git
synced 2026-06-25 10:14:34 +02:00
extra_field() - Context fields documented by their Extra class
A Context dataclass field declared with extra_field() takes its documentation from the description on the registered Extra of the same name, validated against the class's extras_scope. This keeps doc strings next to the resolve() code instead of duplicating them on the dataclass, ahead of introducing TableContext and RowContext. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
3ea7ed8606
commit
63995ce823
2 changed files with 91 additions and 1 deletions
|
|
@ -7,21 +7,66 @@ class ContextField:
|
|||
name: str
|
||||
type_name: str
|
||||
help: str
|
||||
from_extra: bool = False
|
||||
|
||||
|
||||
def extra_field():
|
||||
"""
|
||||
Declare a Context dataclass field whose value comes from a registered
|
||||
Extra of the same name - its documentation is the Extra description,
|
||||
so the doc string lives next to the resolve() code rather than being
|
||||
duplicated on the dataclass.
|
||||
"""
|
||||
return dataclasses.field(metadata={"from_extra": True})
|
||||
|
||||
|
||||
class Context:
|
||||
"Base class for all documented contexts"
|
||||
|
||||
# Set on subclasses whose extra_field() fields should be resolved
|
||||
# against the extras registry for this scope
|
||||
extras_scope = None
|
||||
|
||||
@classmethod
|
||||
def documented_fields(cls):
|
||||
"List of ContextField describing the documented fields of this context"
|
||||
documented = []
|
||||
for f in dataclasses.fields(cls):
|
||||
from_extra = bool(f.metadata.get("from_extra"))
|
||||
if from_extra:
|
||||
help_text = cls._extra_description(f.name)
|
||||
else:
|
||||
help_text = f.metadata.get("help", "")
|
||||
documented.append(
|
||||
ContextField(
|
||||
name=f.name,
|
||||
type_name=getattr(f.type, "__name__", str(f.type)),
|
||||
help=f.metadata.get("help", ""),
|
||||
help=help_text,
|
||||
from_extra=from_extra,
|
||||
)
|
||||
)
|
||||
return documented
|
||||
|
||||
@classmethod
|
||||
def _extra_description(cls, name):
|
||||
# Imported lazily - table_extras is not needed just to define
|
||||
# Context subclasses
|
||||
from datasette.views.table_extras import table_extra_registry
|
||||
|
||||
try:
|
||||
extra_class = table_extra_registry.classes_by_name[name]
|
||||
except KeyError:
|
||||
raise KeyError(
|
||||
"{}.{} is declared with extra_field() 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 "
|
||||
"not available for scope {}".format(
|
||||
cls.__name__, name, name, cls.extras_scope
|
||||
)
|
||||
)
|
||||
return extra_class.description or ""
|
||||
|
|
|
|||
|
|
@ -44,6 +44,51 @@ def test_context_dataclass_fields_all_have_help(klass):
|
|||
)
|
||||
|
||||
|
||||
def test_extra_field_documentation_comes_from_the_extra_class():
|
||||
from datasette.views import extra_field
|
||||
from datasette.views.table_extras import CountExtra
|
||||
|
||||
@dataclass
|
||||
class DemoContext(Context):
|
||||
extras_scope = ExtraScope.TABLE
|
||||
|
||||
count: int = extra_field()
|
||||
name: str = field(metadata={"help": "The name"})
|
||||
|
||||
fields = {f.name: f for f in DemoContext.documented_fields()}
|
||||
assert fields["count"].help == CountExtra.description
|
||||
assert fields["count"].from_extra
|
||||
assert fields["name"].help == "The name"
|
||||
assert not fields["name"].from_extra
|
||||
|
||||
|
||||
def test_extra_field_must_match_a_registered_extra():
|
||||
from datasette.views import extra_field
|
||||
|
||||
@dataclass
|
||||
class BadContext(Context):
|
||||
extras_scope = ExtraScope.TABLE
|
||||
|
||||
not_a_real_extra: str = extra_field()
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
BadContext.documented_fields()
|
||||
|
||||
|
||||
def test_extra_field_must_be_available_for_the_scope():
|
||||
from datasette.views import extra_field
|
||||
|
||||
@dataclass
|
||||
class WrongScopeContext(Context):
|
||||
extras_scope = ExtraScope.ROW
|
||||
|
||||
# count is a TABLE-scope extra, not available for ROW
|
||||
count: int = extra_field()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
WrongScopeContext.documented_fields()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def isolate_extra_template_vars_plugins():
|
||||
# Datasette instances created with plugins_dir (e.g. the session-scoped
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue