Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial plugin framework and register_commands(cli) hook, refs #49
- Loading branch information
Showing
8 changed files
with
150 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
# Plugins | ||
|
||
LLM plugins can provide extra features to the tool. | ||
|
||
## Installing plugins | ||
|
||
Plugins can be installed by running `pip install` in the same virtual environment as `llm` itself: | ||
```bash | ||
pip install llm-hello-world | ||
``` | ||
The [llm-hello-world](https://github.com/simonw/llm-hello-world) plugin is the current best example of how to build and package a plugin. | ||
|
||
## Listing installed plugins | ||
|
||
Run `llm plugins` to list installed plugins: | ||
|
||
```bash | ||
llm plugins | ||
``` | ||
```json | ||
[ | ||
{ | ||
"name": "llm-hello-world", | ||
"hooks": [ | ||
"register_commands" | ||
], | ||
"version": "0.1" | ||
} | ||
] | ||
``` | ||
|
||
## Plugin hooks | ||
|
||
Plugins use **plugin hooks** to customize LLM's behavior. These hooks are powered by the [Pluggy plugin system](https://pluggy.readthedocs.io/). | ||
|
||
Each plugin can implement one or more hooks using the @hookimpl decorator against one of the hook function names described on this page. | ||
|
||
LLM imitates the Datasette plugin system. The [Datasette plugin documentation](https://docs.datasette.io/en/stable/writing_plugins.html) describes how plugins work. | ||
|
||
### register_commands(cli) | ||
|
||
This hook adds new commands to the `llm` CLI tool - for example `llm extra-command`. | ||
|
||
This example plugin adds a new `hello-world` command that prints "Hello world!": | ||
|
||
```python | ||
from llm import hookimpl | ||
import click | ||
|
||
@hookimpl | ||
def register_commands(cli): | ||
@cli.command(name="hello-world") | ||
def hello_world(): | ||
"Print hello world" | ||
click.echo("Hello world!") | ||
``` | ||
This new command will be added to `llm --help` and can be run using `llm hello-world`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
from pluggy import HookimplMarker | ||
from pluggy import HookspecMarker | ||
|
||
hookspec = HookspecMarker("llm") | ||
hookimpl = HookimplMarker("llm") | ||
|
||
|
||
@hookspec | ||
def register_commands(cli): | ||
"""Register additional CLI commands, e.g. 'llm mycommand ...'""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import pluggy | ||
import sys | ||
from . import hookspecs | ||
|
||
pm = pluggy.PluginManager("llm") | ||
pm.add_hookspecs(hookspecs) | ||
|
||
if not hasattr(sys, "_called_from_test"): | ||
# Only load plugins if not running tests | ||
pm.load_setuptools_entrypoints("llm") | ||
|
||
|
||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
from click.testing import CliRunner | ||
import click | ||
import importlib | ||
from llm import cli, hookimpl, plugins | ||
import pytest | ||
|
||
|
||
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() == [] |