From 68223784167fdec4e7ebfca56002a6548ba7b423 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 27 Jun 2023 19:00:33 -0700 Subject: [PATCH] Prototype of rst_docs_for_dataclass mechanism, refs #1510 --- datasette/context.py | 97 +++++++++++++++++++++++++++++++++++++++ docs/conf.py | 7 ++- docs/index.rst | 1 + docs/jsoncontext.py | 28 +++++++++++ docs/template_context.rst | 29 ++++++++++++ 5 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 datasette/context.py create mode 100644 docs/jsoncontext.py create mode 100644 docs/template_context.rst diff --git a/datasette/context.py b/datasette/context.py new file mode 100644 index 00000000..c5927d1d --- /dev/null +++ b/datasette/context.py @@ -0,0 +1,97 @@ +from typing import List, Dict, Optional, Any +from dataclasses import dataclass, field + + +def doc(documentation): + return field(metadata={"doc": documentation}) + + +def is_builtin_type(obj): + return isinstance( + obj, + tuple( + x.__class__ + for x in (int, float, str, bool, bytes, list, tuple, dict, set, frozenset) + ), + ) + + +def rst_docs_for_dataclass(klass: Any) -> str: + """Generate reStructuredText (reST) docs for a dataclass.""" + docs = [] + + # Class name and docstring + docs.append(klass.__name__) + docs.append("-" * len(klass.__name__)) + docs.append("") + if klass.__doc__: + docs.append(klass.__doc__) + docs.append("") + + # Dataclass fields + docs.append("Fields") + docs.append("~~~~~~") + docs.append("") + + for name, field_info in klass.__dataclass_fields__.items(): + if is_builtin_type(field_info.type): + # + type_name = field_info.type.__name__ + else: + # List[str] + type_name = str(field_info.type).replace("typing.", "") + docs.append(f':{name} - ``{type_name}``: {field_info.metadata.get("doc", "")}') + + return "\n".join(docs) + + +@dataclass +class ForeignKey: + incoming: List[Dict] + outgoing: List[Dict] + + +@dataclass +class Table: + "A table is a useful thing" + name: str = doc("The name of the table") + columns: List[str] = doc("List of column names in the table") + primary_keys: List[str] = doc("List of column names that are primary keys") + count: int = doc("Number of rows in the table") + hidden: bool = doc( + "Should this table default to being hidden in the main database UI?" + ) + fts_table: Optional[str] = doc( + "If this table has FTS support, the accompanying FTS table name" + ) + foreign_keys: ForeignKey = doc("List of foreign keys for this table") + private: bool = doc("Private tables are not visible to signed-out anonymous users") + + +@dataclass +class View: + name: str + private: bool + + +@dataclass +class Query: + title: str + sql: str + name: str + private: bool + + +@dataclass +class Database: + content: str + private: bool + path: str + size: int + tables: List[Table] + hidden_count: int + views: List[View] + queries: List[Query] + allow_execute_sql: bool + table_columns: Dict[str, List[str]] + query_ms: float diff --git a/docs/conf.py b/docs/conf.py index c25d8a95..5423fa2a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,7 +31,12 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ["sphinx.ext.extlinks", "sphinx.ext.autodoc", "sphinx_copybutton"] +extensions = [ + "sphinx.ext.extlinks", + "sphinx.ext.autodoc", + "sphinx_copybutton", + "jsoncontext", +] extlinks = { "issue": ("https://github.com/simonw/datasette/issues/%s", "#%s"), diff --git a/docs/index.rst b/docs/index.rst index 5a9cc7ed..254ed3da 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -57,6 +57,7 @@ Contents settings introspection custom_templates + template_context plugins writing_plugins plugin_hooks diff --git a/docs/jsoncontext.py b/docs/jsoncontext.py new file mode 100644 index 00000000..d759f89f --- /dev/null +++ b/docs/jsoncontext.py @@ -0,0 +1,28 @@ +from docutils import nodes +from sphinx.util.docutils import SphinxDirective +from importlib import import_module +import json + + +class JSONContextDirective(SphinxDirective): + required_arguments = 1 + + def run(self): + module_path, class_name = self.arguments[0].rsplit(".", 1) + try: + module = import_module(module_path) + dataclass = getattr(module, class_name) + except ImportError: + warning = f"Unable to import {self.arguments[0]}" + return [nodes.error(None, nodes.paragraph(text=warning))] + + doc = json.dumps( + dataclass.__annotations__, indent=4, sort_keys=True, default=repr + ) + doc_node = nodes.literal_block(text=doc) + + return [doc_node] + + +def setup(app): + app.add_directive("jsoncontext", JSONContextDirective) diff --git a/docs/template_context.rst b/docs/template_context.rst new file mode 100644 index 00000000..f1de4f9e --- /dev/null +++ b/docs/template_context.rst @@ -0,0 +1,29 @@ +.. _template_context: + +Template context +================ + +This page describes the variables made available to templates used by Datasette to render different pages of the application. + + +.. [[[cog + from datasette.context import rst_docs_for_dataclass, Table + cog.out(rst_docs_for_dataclass(Table)) +.. ]]] +Table +----- + +A table is a useful thing + +Fields +~~~~~~ + +:name - ``str``: The name of the table +:columns - ``List[str]``: List of column names in the table +:primary_keys - ``List[str]``: List of column names that are primary keys +:count - ``int``: Number of rows in the table +:hidden - ``bool``: Should this table default to being hidden in the main database UI? +:fts_table - ``Optional[str]``: If this table has FTS support, the accompanying FTS table name +:foreign_keys - ``ForeignKey``: List of foreign keys for this table +:private - ``bool``: Private tables are not visible to signed-out anonymous users +.. [[[end]]]