From 757ce92bafb91bc40c74f41fffd9c3d3c6fffdec Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 6 Jan 2026 07:58:18 -0800 Subject: [PATCH] datasette.utils.StartupError() now becomes a click exception, closes #2624 --- datasette/cli.py | 10 ++++++++-- docs/plugin_hooks.rst | 8 +++++--- tests/test_cli.py | 26 ++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/datasette/cli.py b/datasette/cli.py index 21420491..1d0cb022 100644 --- a/datasette/cli.py +++ b/datasette/cli.py @@ -666,7 +666,10 @@ def serve( return ds # Run the "startup" plugin hooks - run_sync(ds.invoke_startup) + try: + run_sync(ds.invoke_startup) + except StartupError as e: + raise click.ClickException(e.args[0]) # Run async soundness checks - but only if we're not under pytest run_sync(lambda: check_databases(ds)) @@ -815,7 +818,10 @@ def create_token( ds = Datasette(secret=secret, plugins_dir=plugins_dir) # Run ds.invoke_startup() in an event loop - run_sync(ds.invoke_startup) + try: + run_sync(ds.invoke_startup) + except StartupError as e: + raise click.ClickException(e.args[0]) # Warn about any unknown actions actions = [] diff --git a/docs/plugin_hooks.rst b/docs/plugin_hooks.rst index 118a6bde..da49811a 100644 --- a/docs/plugin_hooks.rst +++ b/docs/plugin_hooks.rst @@ -965,12 +965,13 @@ Here is an example that validates required plugin configuration. The server will .. code-block:: python + from datasette.utils import StartupError + @hookimpl def startup(datasette): config = datasette.plugin_config("my-plugin") or {} - assert ( - "required-setting" in config - ), "my-plugin requires setting required-setting" + if "required-setting" not in config: + raise StartupError("my-plugin requires setting required-setting") You can also return an async function, which will be awaited on startup. Use this option if you need to execute any database queries, for example this function which creates the ``my_table`` database table if it does not yet exist: @@ -994,6 +995,7 @@ Potential use-cases: * Run some initialization code for the plugin * Create database tables that a plugin needs on startup * Validate the configuration for a plugin on startup, and raise an error if it is invalid +* Raise a ``datasette.utils.StartupError("message")`` exception to prevent Datasette from starting and display that message to the user. .. note:: diff --git a/tests/test_cli.py b/tests/test_cli.py index 21b86569..36d90e82 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -304,6 +304,32 @@ def test_plugin_s_overwrite(): ) +def test_startup_error_from_plugin_is_click_exception(tmp_path): + plugins_dir = tmp_path / "plugins" + plugins_dir.mkdir() + (plugins_dir / "startup_error.py").write_text( + "from datasette import hookimpl\n" + "from datasette.utils import StartupError\n" + "\n" + "@hookimpl\n" + "def startup(datasette):\n" + ' raise StartupError("boom")\n', + "utf-8", + ) + runner = CliRunner() + result = runner.invoke( + cli, + [ + "--plugins-dir", + str(plugins_dir), + "--get", + "/", + ], + ) + assert result.exit_code == 1 + assert "Error: boom" in result.output + + def test_setting_type_validation(): runner = CliRunner() result = runner.invoke(cli, ["--setting", "default_page_size", "dog"])