From 894c424b90f03963dc09f1c820f93add2b45eec5 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 25 Jul 2019 17:09:13 +0300 Subject: [PATCH] New plugin hook: extra_serve_options() --- datasette/app.py | 2 ++ datasette/cli.py | 12 +++++++++++- datasette/hookspecs.py | 5 +++++ docs/plugins.rst | 42 ++++++++++++++++++++++++++++++++++++++++++ tests/fixtures.py | 5 ++++- tests/test_plugins.py | 14 ++++++++++++++ 6 files changed, 78 insertions(+), 2 deletions(-) diff --git a/datasette/app.py b/datasette/app.py index 501d1467..032c244a 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -151,6 +151,7 @@ class Datasette: memory=False, config=None, version_note=None, + plugin_extra_options=None, ): immutables = immutables or [] self.files = tuple(files) + tuple(immutables) @@ -159,6 +160,7 @@ class Datasette: self.files = [MEMORY] elif memory: self.files = (MEMORY,) + self.files + self.plugin_extra_options = plugin_extra_options or {} self.databases = {} self.inspect_data = inspect_data for file in self.files: diff --git a/datasette/cli.py b/datasette/cli.py index 181b281c..68104aee 100644 --- a/datasette/cli.py +++ b/datasette/cli.py @@ -186,7 +186,7 @@ def package( install, spatialite, version_note, - **extra_metadata + **extra_metadata, ): "Package specified SQLite files into a new datasette Docker container" if not shutil.which("docker"): @@ -220,6 +220,13 @@ def package( call(args) +def extra_serve_options(serve): + for options in pm.hook.extra_serve_options(): + for option in reversed(options): + serve = option(serve) + return serve + + @cli.command() @click.argument("files", type=click.Path(exists=True), nargs=-1) @click.option( @@ -286,6 +293,7 @@ def package( ) @click.option("--version-note", help="Additional note to show on /-/versions") @click.option("--help-config", is_flag=True, help="Show available config options") +@extra_serve_options def serve( files, immutable, @@ -304,6 +312,7 @@ def serve( config, version_note, help_config, + **plugin_extra_options, ): """Serve up specified SQLite database files with a web UI""" if help_config: @@ -350,6 +359,7 @@ def serve( config=dict(config), memory=memory, version_note=version_note, + plugin_extra_options=plugin_extra_options, ) # Run async sanity checks - but only if we're not under pytest asyncio.get_event_loop().run_until_complete(ds.run_sanity_checks()) diff --git a/datasette/hookspecs.py b/datasette/hookspecs.py index 3c6726b7..bca47990 100644 --- a/datasette/hookspecs.py +++ b/datasette/hookspecs.py @@ -58,3 +58,8 @@ def register_output_renderer(datasette): @hookspec def register_facet_classes(): "Register Facet subclasses" + + +@hookspec +def extra_serve_options(): + "Return list of extra click.option decorators to be applied to 'datasette serve'" diff --git a/docs/plugins.rst b/docs/plugins.rst index 1d4f1e1a..ead0c569 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -812,3 +812,45 @@ This example plugin adds a ``x-databases`` HTTP header listing the currently att await app(scope, recieve, wrapped_send) return add_x_databases_header return wrap_with_databases_header + +.. _plugin_hook_extra_serve_options: + +extra_serve_options() +~~~~~~~~~~~~~~~~~~~~~ + +Add extra Click options to the ``datasette serve`` command. Options you add here will be displayed in ``datasette serve --help`` and their values will be available to your plugin anywhere it can access the ``datasette`` object by reading from ``datasette.plugin_extra_options``. + +.. code-block:: python + + from datasette import hookimpl + import click + + @hookimpl + def extra_serve_options(): + return [ + click.option( + "--my-plugin-paths", + type=click.Path(exists=True, file_okay=False, dir_okay=True), + help="Directories to use with my-plugin", + multiple=True, + ), + click.option( + "--my-plugin-enable", + is_flag=True, + help="Enable functionality from my-plugin", + ), + ] + +Your other plugin hooks can then access these settings like so: + +.. code-block:: python + + from datasette import hookimpl + + @hookimpl + def extra_template_vars(datasette): + return { + "my_plugin_paths": datasette.plugin_extra_options.get("my_plugin_paths") or [] + } + +Be careful not to define an option which clashes with a Datasette default option, or with options provided by another plugin. For this reason we recommend using a common prefix for your plugin, as shown above. diff --git a/tests/fixtures.py b/tests/fixtures.py index dac28dc0..5a272e12 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -108,6 +108,7 @@ def make_app_client( inspect_data=None, static_mounts=None, template_dir=None, + plugin_extra_options=None, ): with tempfile.TemporaryDirectory() as tmpdir: filepath = os.path.join(tmpdir, filename) @@ -151,6 +152,7 @@ def make_app_client( inspect_data=inspect_data, static_mounts=static_mounts, template_dir=template_dir, + plugin_extra_options=plugin_extra_options, ) ds.sqlite_functions.append(("sleep", 1, lambda n: time.sleep(float(n)))) client = TestClient(ds.app()) @@ -386,7 +388,8 @@ def extra_template_vars(template, database, table, view_name, request, datasette return { "extra_template_vars": json.dumps({ "template": template, - "scope_path": request.scope["path"] + "scope_path": request.scope["path"], + "plugin_extra_options": datasette.plugin_extra_options, }, default=lambda b: b.decode("utf8")) } """ diff --git a/tests/test_plugins.py b/tests/test_plugins.py index b1c7fd9a..939b4627 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -203,6 +203,7 @@ def test_plugins_extra_template_vars(restore_working_directory): assert { "template": "show_json.html", "scope_path": "/-/metadata", + "plugin_extra_options": {}, } == extra_template_vars extra_template_vars_from_awaitable = json.loads( Soup(response.body, "html.parser") @@ -214,3 +215,16 @@ def test_plugins_extra_template_vars(restore_working_directory): "awaitable": True, "scope_path": "/-/metadata", } == extra_template_vars_from_awaitable + + +def test_plugin_extra_options_available_on_datasette(restore_working_directory): + for client in make_app_client( + template_dir=str(pathlib.Path(__file__).parent / "test_templates"), + plugin_extra_options={"foo": "bar"}, + ): + response = client.get("/-/metadata") + assert response.status == 200 + extra_template_vars = json.loads( + Soup(response.body, "html.parser").select("pre.extra_template_vars")[0].text + ) + assert {"foo": "bar"} == extra_template_vars["plugin_extra_options"]