From f2720b0c6b7172ebe88209853d0d59866d519246 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Sun, 15 Apr 2018 16:17:36 -0700 Subject: [PATCH] First working prototype of plugins, refs #14 Uses pluggy: https://pluggy.readthedocs.io/ Two example plugins - an uppercase template filter and a convert_units() SQL function. --- datasette/__init__.py | 2 ++ datasette/app.py | 11 +++++++++++ datasette/hookspecs.py | 15 +++++++++++++++ datasette/plugin_demo.py | 17 +++++++++++++++++ setup.py | 3 ++- 5 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 datasette/hookspecs.py create mode 100644 datasette/plugin_demo.py diff --git a/datasette/__init__.py b/datasette/__init__.py index 668a8c82..1ec88d90 100644 --- a/datasette/__init__.py +++ b/datasette/__init__.py @@ -1 +1,3 @@ from datasette.version import __version_info__, __version__ # noqa +from .hookspecs import hookimpl # noqa +from .hookspecs import hookspec # noqa diff --git a/datasette/app.py b/datasette/app.py index 3cd39b2f..dccd81ee 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -17,6 +17,7 @@ import jinja2 import hashlib import time import pint +import pluggy import traceback from .utils import ( Filters, @@ -38,6 +39,7 @@ from .utils import ( urlsafe_components, validate_sql_select, ) +from . import hookspecs from .version import __version__ app_root = Path(__file__).parent.parent @@ -49,6 +51,13 @@ connections = threading.local() ureg = pint.UnitRegistry() +pm = pluggy.PluginManager('datasette') +pm.add_hookspecs(hookspecs) +import datasette.plugin_demo +pm.register(datasette.plugin_demo) +pm.load_setuptools_entrypoints('datasette') + + class DatasetteError(Exception): def __init__(self, message, title=None, error_dict=None, status=500, template=None): self.message = message @@ -1100,6 +1109,7 @@ class Datasette: conn.enable_load_extension(True) for extension in self.sqlite_extensions: conn.execute("SELECT load_extension('{}')".format(extension)) + pm.hook.prepare_connection(conn=conn) def inspect(self): if not self._inspect: @@ -1226,6 +1236,7 @@ class Datasette: self.jinja_env.filters['quote_plus'] = lambda u: urllib.parse.quote_plus(u) self.jinja_env.filters['escape_sqlite'] = escape_sqlite self.jinja_env.filters['to_css_class'] = to_css_class + pm.hook.prepare_jinja2_environment(env=self.jinja_env) app.add_route(IndexView.as_view(self), '/') # TODO: /favicon.ico and /-/static/ deserve far-future cache expires app.add_route(favicon, '/favicon.ico') diff --git a/datasette/hookspecs.py b/datasette/hookspecs.py new file mode 100644 index 00000000..0e65a198 --- /dev/null +++ b/datasette/hookspecs.py @@ -0,0 +1,15 @@ +from pluggy import HookimplMarker +from pluggy import HookspecMarker + +hookspec = HookspecMarker('datasette') +hookimpl = HookimplMarker('datasette') + + +@hookspec +def prepare_connection(conn): + "Modify SQLite connection in some way e.g. register custom SQL functions" + + +@hookspec +def prepare_jinja2_environment(env): + "Modify Jinja2 template environment e.g. register custom template tags" diff --git a/datasette/plugin_demo.py b/datasette/plugin_demo.py new file mode 100644 index 00000000..a9c82475 --- /dev/null +++ b/datasette/plugin_demo.py @@ -0,0 +1,17 @@ +from datasette import hookimpl +import pint + +ureg = pint.UnitRegistry() + + +@hookimpl +def prepare_jinja2_environment(env): + env.filters['uppercase'] = lambda u: u.upper() + + +@hookimpl +def prepare_connection(conn): + def convert_units(amount, from_, to_): + "select convert_units(100, 'm', 'ft');" + return (amount * ureg(from_)).to(to_).to_tuple()[0] + conn.create_function('convert_units', 3, convert_units) diff --git a/setup.py b/setup.py index 362484c4..295058f3 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,8 @@ setup( 'Sanic==0.7.0', 'Jinja2==2.10', 'hupper==1.0', - 'pint==0.8.1' + 'pint==0.8.1', + 'pluggy>=0.1.0,<1.0', ], entry_points=''' [console_scripts]