""" Tests for the documented template context - the contract that custom template authors can rely on for Datasette 1.0. """ import html import json import pathlib from dataclasses import dataclass, field import pytest from datasette.app import TEMPLATE_BASE_CONTEXT, Datasette from datasette.extras import ExtraScope from datasette.fixtures import write_fixture_database from datasette.template_contexts import PAGES, documented_context_keys from datasette.views import Context def test_documented_fields(): @dataclass class DemoNested: name: str @dataclass class DemoContext(Context): name: str = field(metadata={"help": "The name"}) _internal: str = field() count: int = field(metadata={"help": "How many there are"}) items: list[DemoNested] = field(metadata={"help": "Nested items"}) 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"), ("items", "list[DemoNested]", "Nested items"), ] @pytest.mark.parametrize("klass", PAGES.values(), ids=lambda klass: klass.__name__) def test_context_class_fields_all_have_help(klass): for context_field in klass.documented_fields(): assert ( context_field.help ), f"{klass.__name__}.{context_field.name} is missing documentation" @pytest.mark.parametrize("klass", PAGES.values(), ids=lambda klass: klass.__name__) def test_context_class_has_docstring_and_documented_template(klass): assert klass.__doc__, f"{klass.__name__} is missing a docstring" assert ( klass.documented_template ), f"{klass.__name__} is missing a documented_template" 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 = from_extra() 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_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 = from_extra() with pytest.raises(KeyError): BadContext.documented_fields() 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 = from_extra() 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 # ds_client fixture) register their plugins on the global plugin manager # for the rest of the process. The contract documents plugin-free # Datasette core, so unregister any non-default plugin that adds # template variables via the extra_template_vars hook from datasette.plugins import DEFAULT_PLUGINS, pm hook_plugins = {impl.plugin for impl in pm.hook.extra_template_vars.get_hookimpls()} removed = [] for plugin in list(pm.get_plugins()): name = pm.get_name(plugin) if name not in DEFAULT_PLUGINS and plugin in hook_plugins: pm.unregister(plugin) removed.append((plugin, name)) yield for plugin, name in removed: pm.register(plugin, name) @pytest.fixture(scope="module") def context_ds(tmp_path_factory): db_path = tmp_path_factory.mktemp("template-context") / "fixtures.db" write_fixture_database(db_path) ds = Datasette( [str(db_path)], settings={"num_sql_threads": 1, "template_debug": True}, config={ "databases": { "fixtures": { "queries": { "neighborhood_search": { "sql": ( "select _neighborhood from facetable " "where _neighborhood like '%' || :text || '%'" ), "title": "Search neighborhoods", } } } } }, ) yield ds for db in ds.databases.values(): if not db.is_memory: db.close() async def get_template_context(ds, path): sep = "&" if "?" in path else "?" response = await ds.client.get(path + sep + "_context=1") assert response.status_code == 200, path body = html.unescape(response.text.removeprefix("
").removesuffix(""))
return json.loads(body)
@pytest.mark.asyncio
@pytest.mark.parametrize(
"page_name,path",
(
("database", "/fixtures"),
("table", "/fixtures/facetable"),
("table", "/fixtures/facetable?_city_id__exact=1"),
("row", "/fixtures/facetable/1"),
("query", "/fixtures/-/query?sql=select+*+from+facetable"),
("query", "/fixtures/neighborhood_search?text=cork"),
),
)
async def test_template_context_matches_documented_contract(
context_ds, isolate_extra_template_vars_plugins, page_name, path
):
# The full contract: every key in the rendered template context is
# documented, and every documented key is present in the context
documented = documented_context_keys(page_name)
actual = {
key
for key in await get_template_context(context_ds, path)
if not key.startswith("_")
}
undocumented = actual - documented
no_longer_present = documented - actual
assert not undocumented, (
f"Undocumented keys in {page_name} template context: {sorted(undocumented)} - add them to the "
"page's Context class"
)
assert not no_longer_present, (
f"Documented keys missing from {page_name} template context: {sorted(no_longer_present)} - this would "
"break custom templates"
)
def test_base_context_keys_all_have_docs():
for name, doc in TEMPLATE_BASE_CONTEXT.items():
assert doc, f"Base context key {name} is missing docs"
def test_template_context_docs_cover_every_documented_key():
docs_path = pathlib.Path(__file__).parent.parent / "docs" / "template_context.rst"
assert docs_path.exists(), "docs/template_context.rst is missing"
docs = docs_path.read_text()
for name in TEMPLATE_BASE_CONTEXT:
assert f"``{name}``" in docs, name
for page_name, klass in PAGES.items():
title = "{} page".format(klass.__name__.removesuffix("Context"))
assert title in docs, title
for context_field in klass.documented_fields():
assert (
f"``{context_field.name}``" in docs
), f"{context_field.name} ({page_name} page)"
assert (
f"``{context_field.name}`` - ``{context_field.type_name}``" in docs
), f"{context_field.name} type ({page_name} page)"