Skip to content

Commit

Permalink
Added pluggy and first hook, register_commands - refs #569, #567
Browse files Browse the repository at this point in the history
  • Loading branch information
simonw committed Jul 22, 2023
1 parent bff2400 commit b379a2a
Show file tree
Hide file tree
Showing 11 changed files with 224 additions and 2 deletions.
17 changes: 17 additions & 0 deletions docs/cli-reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ This page lists the ``--help`` for every ``sqlite-utils`` CLI sub-command.

.. [[[cog
from sqlite_utils import cli
import sys
sys._called_from_test = True
from click.testing import CliRunner
import textwrap
commands = list(cli.cli.commands.keys())
Expand Down Expand Up @@ -1500,4 +1502,19 @@ See :ref:`cli_spatialite_indexes`.
-h, --help Show this message and exit.


.. _cli_ref_plugins:

plugins
=======

::

Usage: sqlite-utils plugins [OPTIONS]

List installed plugins

Options:
-h, --help Show this message and exit.


.. [[[end]]]
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Contents
installation
cli
python-api
plugins
reference
cli-reference
contributing
Expand Down
109 changes: 109 additions & 0 deletions docs/plugins.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
.. _plugins:

=========
Plugins
=========

``sqlite-utils`` supports plugins, which can be used to add extra features to the software.

Plugins can add new commands, for example ``sqlite-utils some-command ...``

Plugins can be installed using the ``sqlite-utils install`` command:

.. code-block:: bash
sqlite-utils install sqlite-utils-name-of-plugin
You can see a JSON list of plugins that have been installed by running this:

.. code-block:: bash
sqlite-utils plugins
.. _plugins_building:

Building a plugin
-----------------

Plugins are created in a directory named after the plugin. To create a "hello world" plugin, first create a ``hello-world`` directory:

.. code-block:: bash
mkdir hello-world
cd hello-world
In that folder create two files. The first is a ``pyproject.toml`` file describing the plugin:

.. code-block:: toml
[project]
name = "sqlite-utils-hello-world"
version = "0.1"
[project.entry-points.sqlite_utils]
hello_world = "sqlite_utils_hello_world"
The ```[project.entry-points.sqlite_utils]`` section tells ``sqlite-tils`` which module to load when executing the plugin.

Then create ``sqlite_utils_hello_world.py`` with the following content:

.. code-block:: python
import click
import sqlite_utils
@sqlite_utils.hookimpl
def register_commands(cli):
@cli.command()
def hello_world():
"Say hello world"
click.echo("Hello world!")
Install the plugin in "editable" mode - so you can make changes to the code and have them picked up instantly by ``sqlite-utils`` - like this:

.. code-block:: bash
sqlite-utils install -e .
Or pass the path to your plugin directory:

.. code-block:: bash
sqlite-utils install -e `/dev/sqlite-utils-hello-world
Now, running this should execute your new command:
.. code-block:: bash
sqlite-utils hello-world
Your command will also be listed in the output of ``sqlite-utils --help``.
.. _plugins_hooks:
Plugin hooks
------------
Plugin hooks allow ``sqlite-utils`` to be customized. There is currently one hook.
.. _plugins_hooks_register_commands:
register_commands(cli)
~~~~~~~~~~~~~~~~~~~~~~
This hook can be used to register additional commands with the ``sqlite-utils`` CLI. It is called with the ``cli`` object, which is a ``click.Group`` instance.
Example implementation:
.. code-block:: python
import click
import sqlite_utils
@sqlite_utils.hookimpl
def register_commands(cli):
@cli.command()
def hello_world():
"Say hello world"
click.echo("Hello world!")
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def get_long_description():
"click-default-group-wheel",
"tabulate",
"python-dateutil",
"pluggy",
],
extras_require={
"test": ["pytest", "black", "hypothesis", "cogapp"],
Expand All @@ -45,6 +46,7 @@ def get_long_description():
"types-click",
"types-tabulate",
"types-python-dateutil",
"types-pluggy",
"data-science-types",
],
"flake8": ["flake8"],
Expand Down
4 changes: 3 additions & 1 deletion sqlite_utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from .db import Database
from .utils import suggest_column_types
from .hookspecs import hookimpl
from .hookspecs import hookspec

__all__ = ["Database", "suggest_column_types"]
__all__ = ["Database", "suggest_column_types", "hookimpl", "hookspec"]
10 changes: 10 additions & 0 deletions sqlite_utils/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from runpy import run_module
import sqlite_utils
from sqlite_utils.db import AlterError, BadMultiValues, DescIndex, NoTable
from sqlite_utils.plugins import pm, get_plugins
from sqlite_utils.utils import maximize_csv_field_size_limit
from sqlite_utils import recipes
import textwrap
Expand Down Expand Up @@ -3078,6 +3079,15 @@ def create_spatial_index(db_path, table, column_name, load_extension):
db[table].create_spatial_index(column_name)


@cli.command(name="plugins")
def plugins_list():
"List installed plugins"
click.echo(json.dumps(get_plugins(), indent=2))


pm.hook.register_commands(cli=cli)


def _render_common(title, values):
if values is None:
return ""
Expand Down
10 changes: 10 additions & 0 deletions sqlite_utils/hookspecs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from pluggy import HookimplMarker
from pluggy import HookspecMarker

hookspec = HookspecMarker("sqlite_utils")
hookimpl = HookimplMarker("sqlite_utils")


@hookspec
def register_commands(cli):
"""Register additional CLI commands, e.g. 'sqlite-utils mycommand ...'"""
26 changes: 26 additions & 0 deletions sqlite_utils/plugins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import pluggy
import sys
from . import hookspecs

pm = pluggy.PluginManager("sqlite_utils")
pm.add_hookspecs(hookspecs)

if not hasattr(sys, "_called_from_test"):
# Only load plugins if not running tests
pm.load_setuptools_entrypoints("sqlite_utils")


def get_plugins():
plugins = []
plugin_to_distinfo = dict(pm.list_plugin_distinfo())
for plugin in pm.get_plugins():
plugin_info = {
"name": plugin.__name__,
"hooks": [h.name for h in pm.get_hookcallers(plugin)],
}
distinfo = plugin_to_distinfo.get(plugin)
if distinfo:
plugin_info["version"] = distinfo.version
plugin_info["name"] = distinfo.project_name
plugins.append(plugin_info)
return plugins
6 changes: 6 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
"""


def pytest_configure(config):
import sys

sys._called_from_test = True


@pytest.fixture
def fresh_db():
return Database(memory=True)
Expand Down
4 changes: 3 additions & 1 deletion tests/test_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@

@pytest.fixture(scope="session")
def documented_commands():
rst = (docs_path / "cli.rst").read_text()
rst = ""
for doc in ("cli.rst", "plugins.rst"):
rst += (docs_path / doc).read_text()
return {
command
for command in commands_re.findall(rst)
Expand Down
37 changes: 37 additions & 0 deletions tests/test_plugins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from click.testing import CliRunner
import click
import importlib
from sqlite_utils import cli, hookimpl, plugins


def test_register_commands():
importlib.reload(cli)
assert plugins.get_plugins() == []

class HelloWorldPlugin:
__name__ = "HelloWorldPlugin"

@hookimpl
def register_commands(self, cli):
@cli.command(name="hello-world")
def hello_world():
"Print hello world"
click.echo("Hello world!")

try:
plugins.pm.register(HelloWorldPlugin(), name="HelloWorldPlugin")
importlib.reload(cli)

assert plugins.get_plugins() == [
{"name": "HelloWorldPlugin", "hooks": ["register_commands"]}
]

runner = CliRunner()
result = runner.invoke(cli.cli, ["hello-world"])
assert result.exit_code == 0
assert result.output == "Hello world!\n"

finally:
plugins.pm.unregister(name="HelloWorldPlugin")
importlib.reload(cli)
assert plugins.get_plugins() == []

0 comments on commit b379a2a

Please sign in to comment.