register_commands() plugin hook, closes #1449

This commit is contained in:
Simon Willison 2021-08-27 18:39:42 -07:00
commit 30c18576d6
4 changed files with 109 additions and 1 deletions

View file

@ -595,6 +595,9 @@ def serve(
uvicorn.run(ds.app(), **uvicorn_kwargs) uvicorn.run(ds.app(), **uvicorn_kwargs)
pm.hook.register_commands(cli=cli)
async def check_databases(ds): async def check_databases(ds):
# Run check_connection against every connected database # Run check_connection against every connected database
# to confirm they are all usable # to confirm they are all usable

View file

@ -79,6 +79,11 @@ def register_routes(datasette):
"""Register URL routes: return a list of (regex, view_function) pairs""" """Register URL routes: return a list of (regex, view_function) pairs"""
@hookspec
def register_commands(cli):
"""Register additional CLI commands, e.g. 'datasette mycommand ...'"""
@hookspec @hookspec
def actor_from_request(datasette, request): def actor_from_request(datasette, request):
"""Return an actor dictionary based on the incoming request""" """Return an actor dictionary based on the incoming request"""

View file

@ -587,6 +587,51 @@ See :ref:`writing_plugins_designing_urls` for tips on designing the URL routes u
Examples: `datasette-auth-github <https://datasette.io/plugins/datasette-auth-github>`__, `datasette-psutil <https://datasette.io/plugins/datasette-psutil>`__ Examples: `datasette-auth-github <https://datasette.io/plugins/datasette-auth-github>`__, `datasette-psutil <https://datasette.io/plugins/datasette-psutil>`__
.. _plugin_register_commands:
register_commands(cli)
----------------------
``cli`` - the root Datasette `Click command group <https://click.palletsprojects.com/en/latest/commands/#callback-invocation>`__
Use this to register additional CLI commands
Register additional CLI commands that can be run using ``datsette yourcommand ...``. This provides a mechanism by which plugins can add new CLI commands to Datasette.
This example registers a new ``datasette verify file1.db file2.db`` command that checks if the provided file paths are valid SQLite databases:
.. code-block:: python
from datasette import hookimpl
import click
import sqlite3
@hookimpl
def register_commands(cli):
@cli.command()
@click.argument("files", type=click.Path(exists=True), nargs=-1)
def verify(files):
"Verify that files can be opened by Datasette"
for file in files:
conn = sqlite3.connect(str(file))
try:
conn.execute("select * from sqlite_master")
except sqlite3.DatabaseError:
raise click.ClickException("Invalid database: {}".format(file))
The new command can then be executed like so::
datasette verify fixtures.db
Help text (from the docstring for the function plus any defined Click arguments or options) will become available using::
datasette verify --help
Plugins can register multiple commands by making multiple calls to the ``@cli.command()`` decorator.Consult the `Click documentation <https://click.palletsprojects.com/>`__ for full details on how to build a CLI command, including how to define arguments and options.
Note that ``register_commands()`` plugins cannot used with the :ref:`--plugins-dir mechanism <writing_plugins_one_off>` - they need to be installed into the same virtual environment as Datasette using ``pip install``. Provided it has a ``setup.py`` file (see :ref:`writing_plugins_packaging`) you can run ``pip install`` directly against the directory in which you are developing your plugin like so::
pip install -e path/to/my/datasette-plugin
.. _plugin_register_facet_classes: .. _plugin_register_facet_classes:
register_facet_classes() register_facet_classes()

View file

@ -6,13 +6,15 @@ from .fixtures import (
TEMP_PLUGIN_SECRET_FILE, TEMP_PLUGIN_SECRET_FILE,
TestClient as _TestClient, TestClient as _TestClient,
) # noqa ) # noqa
from click.testing import CliRunner
from datasette.app import Datasette from datasette.app import Datasette
from datasette import cli from datasette import cli, hookimpl
from datasette.plugins import get_plugins, DEFAULT_PLUGINS, pm from datasette.plugins import get_plugins, DEFAULT_PLUGINS, pm
from datasette.utils.sqlite import sqlite3 from datasette.utils.sqlite import sqlite3
from datasette.utils import CustomRow from datasette.utils import CustomRow
from jinja2.environment import Template from jinja2.environment import Template
import base64 import base64
import importlib
import json import json
import os import os
import pathlib import pathlib
@ -902,3 +904,56 @@ def test_hook_get_metadata(app_client):
assert "Hello from local metadata" == meta["databases"]["from-local"]["title"] assert "Hello from local metadata" == meta["databases"]["from-local"]["title"]
assert "Hello from the plugin hook" == meta["databases"]["from-hook"]["title"] assert "Hello from the plugin hook" == meta["databases"]["from-hook"]["title"]
pm.hook.get_metadata = og_pm_hook_get_metadata pm.hook.get_metadata = og_pm_hook_get_metadata
def _extract_commands(output):
lines = output.split("Commands:\n", 1)[1].split("\n")
return {line.split()[0].replace("*", "") for line in lines if line.strip()}
def test_hook_register_commands():
# Without the plugin should have seven commands
runner = CliRunner()
result = runner.invoke(cli.cli, "--help")
commands = _extract_commands(result.output)
assert commands == {
"serve",
"inspect",
"install",
"package",
"plugins",
"publish",
"uninstall",
}
# Now install a plugin
class VerifyPlugin:
__name__ = "VerifyPlugin"
@hookimpl
def register_commands(self, cli):
@cli.command()
def verify():
pass
@cli.command()
def unverify():
pass
pm.register(VerifyPlugin(), name="verify")
importlib.reload(cli)
result2 = runner.invoke(cli.cli, "--help")
commands2 = _extract_commands(result2.output)
assert commands2 == {
"serve",
"inspect",
"install",
"package",
"plugins",
"publish",
"uninstall",
"verify",
"unverify",
}
pm.unregister(name="verify")
importlib.reload(cli)