From 6aa5886379dd9017215904fb28567b80018902f9 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Mon, 19 Oct 2020 15:37:31 -0700 Subject: [PATCH] --load-extension=spatialite shortcut, closes #1028 --- datasette/app.py | 11 ++++++++++- datasette/cli.py | 8 +++++--- datasette/utils/__init__.py | 18 ++++++++++++++++++ docs/installation.rst | 2 +- docs/spatialite.rst | 10 +++++++++- tests/test_cli.py | 11 +++++++++++ 6 files changed, 54 insertions(+), 6 deletions(-) diff --git a/datasette/app.py b/datasette/app.py index e6ece8ad..b768a298 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -49,12 +49,14 @@ from .utils import ( display_actor, escape_css_string, escape_sqlite, + find_spatialite, format_bytes, module_from_path, parse_metadata, resolve_env_secrets, sqlite3, to_css_class, + SpatialiteNotFound, ) from .utils.asgi import ( AsgiLifespan, @@ -242,7 +244,14 @@ class Datasette: metadata = parse_metadata(fp.read()) self._metadata = metadata or {} self.sqlite_functions = [] - self.sqlite_extensions = sqlite_extensions or [] + self.sqlite_extensions = [] + for extension in sqlite_extensions or []: + # Resolve spatialite, if requested + if extension == "spatialite": + # Could raise SpatialiteNotFound + self.sqlite_extensions.append(find_spatialite()) + else: + self.sqlite_extensions.append(extension) if config_dir and (config_dir / "templates").is_dir() and not template_dir: template_dir = str((config_dir / "templates").resolve()) self.template_dir = template_dir diff --git a/datasette/cli.py b/datasette/cli.py index 55576013..ab24cb12 100644 --- a/datasette/cli.py +++ b/datasette/cli.py @@ -19,6 +19,7 @@ from .utils import ( SpatialiteConnectionProblem, temporary_docker_directory, value_as_boolean, + SpatialiteNotFound, StaticMount, ValueAsBooleanError, ) @@ -78,7 +79,6 @@ def cli(): "--load-extension", envvar="SQLITE_EXTENSIONS", multiple=True, - type=click.Path(exists=True, resolve_path=True), help="Path to a SQLite extension to load", ) def inspect(files, inspect_file, sqlite_extensions): @@ -299,7 +299,6 @@ def uninstall(packages, yes): "--load-extension", envvar="SQLITE_EXTENSIONS", multiple=True, - type=click.Path(exists=True, resolve_path=True), help="Path to a SQLite extension to load", ) @click.option( @@ -433,7 +432,10 @@ def serve( kwargs["config_dir"] = pathlib.Path(files[0]) files = [] - ds = Datasette(files, **kwargs) + try: + ds = Datasette(files, **kwargs) + except SpatialiteNotFound: + raise click.ClickException("Could not find SpatiaLite extension") if return_instance: # Private utility mechanism for writing unit tests diff --git a/datasette/utils/__init__.py b/datasette/utils/__init__.py index 7b8918a5..7026bdf0 100644 --- a/datasette/utils/__init__.py +++ b/datasette/utils/__init__.py @@ -54,6 +54,12 @@ RUN apt-get update && \ ENV SQLITE_EXTENSIONS /usr/lib/x86_64-linux-gnu/mod_spatialite.so """ +# Can replace with sqlite-utils when I add that dependency +SPATIALITE_PATHS = ( + "/usr/lib/x86_64-linux-gnu/mod_spatialite.so", + "/usr/local/lib/mod_spatialite.dylib", +) + # Can replace this with Column from sqlite_utils when I add that dependency Column = namedtuple( "Column", ("cid", "name", "type", "notnull", "default_value", "is_pk") @@ -971,3 +977,15 @@ def display_actor(actor): if actor.get(key): return actor[key] return str(actor) + + +class SpatialiteNotFound(Exception): + pass + + +# Can replace with sqlite-utils when I add that dependency +def find_spatialite(): + for path in SPATIALITE_PATHS: + if os.path.exists(path): + return path + raise SpatialiteNotFound diff --git a/docs/installation.rst b/docs/installation.rst index dcae738a..6ac67f59 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -170,7 +170,7 @@ module, use the following command:: docker run -p 8001:8001 -v `pwd`:/mnt \ datasetteproject/datasette \ datasette -p 8001 -h 0.0.0.0 /mnt/fixtures.db \ - --load-extension=/usr/local/lib/mod_spatialite.so + --load-extension=spatialite You can confirm that SpatiaLite is successfully loaded by visiting http://127.0.0.1:8001/-/versions diff --git a/docs/spatialite.rst b/docs/spatialite.rst index e71bf340..05c5c667 100644 --- a/docs/spatialite.rst +++ b/docs/spatialite.rst @@ -8,6 +8,14 @@ The `SpatiaLite module `_ fo To use it with Datasette, you need to install the ``mod_spatialite`` dynamic library. This can then be loaded into Datasette using the ``--load-extension`` command-line option. +Datasette can look for SpatiaLite in common installation locations if you run it like this:: + + datasette --load-extension=spatialite + +If SpatiaLite is in another location, use the full path to the extension instead:: + + datasette --load-extension=/usr/local/lib/mod_spatialite.dylib + Installation ============ @@ -25,7 +33,7 @@ This will install the ``spatialite`` command-line tool and the ``mod_spatialite` You can now run Datasette like so:: - datasette --load-extension=/usr/local/lib/mod_spatialite.dylib + datasette --load-extension=spatialite Installing SpatiaLite on Linux ------------------------------ diff --git a/tests/test_cli.py b/tests/test_cli.py index 0e1745c2..76f94aa1 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -59,6 +59,17 @@ def test_spatialite_error_if_attempt_to_open_spatialite(): assert "trying to load a SpatiaLite database" in result.output +@mock.patch("datasette.utils.SPATIALITE_PATHS", ["/does/not/exist"]) +def test_spatialite_error_if_cannot_find_load_extension_spatialite(): + runner = CliRunner() + result = runner.invoke( + cli, ["serve", str(pathlib.Path(__file__).parent / "spatialite.db"), + "--load-extension", "spatialite"] + ) + assert result.exit_code != 0 + assert "Could not find SpatiaLite extension" in result.output + + def test_plugins_cli(app_client): runner = CliRunner() result1 = runner.invoke(cli, ["plugins"])