diff --git a/datasette/cli.py b/datasette/cli.py index 99075078..9e696aa8 100644 --- a/datasette/cli.py +++ b/datasette/cli.py @@ -2,6 +2,7 @@ import asyncio import uvicorn import click from click import formatting +from click.types import CompositeParamType from click_default_group import DefaultGroup import json import os @@ -29,6 +30,7 @@ from .version import __version__ class Config(click.ParamType): + # This will be removed in Datasette 1.0 in favour of class Setting name = "config" def convert(self, config, param, ctx): @@ -63,6 +65,39 @@ class Config(click.ParamType): self.fail("Invalid option") +class Setting(CompositeParamType): + name = "setting" + arity = 2 + + def convert(self, config, param, ctx): + name, value = config + if name not in DEFAULT_CONFIG: + self.fail( + f"{name} is not a valid option (--help-config to see all)", + param, + ctx, + ) + return + # Type checking + default = DEFAULT_CONFIG[name] + if isinstance(default, bool): + try: + return name, value_as_boolean(value) + except ValueAsBooleanError: + self.fail(f'"{name}" should be on/off/true/false/1/0', param, ctx) + return + elif isinstance(default, int): + if not value.isdigit(): + self.fail(f'"{name}" should be an integer', param, ctx) + return + return name, int(value) + elif isinstance(default, str): + return name, value + else: + # Should never happen: + self.fail("Invalid option") + + @click.group(cls=DefaultGroup, default="serve", default_if_no_args=True) @click.version_option(version=__version__) def cli(): @@ -330,7 +365,14 @@ def uninstall(packages, yes): @click.option( "--config", type=Config(), - help="Set config option using configname:value docs.datasette.io/en/stable/config.html", + help="Deprecated: set config option using configname:value. Use --setting instead.", + multiple=True, +) +@click.option( + "--setting", + "settings", + type=Setting(), + help="Setting, see docs.datasette.io/en/stable/config.html", multiple=True, ) @click.option( @@ -372,6 +414,7 @@ def serve( static, memory, config, + settings, secret, root, get, @@ -410,6 +453,15 @@ def serve( if metadata: metadata_data = parse_metadata(metadata.read()) + combined_config = {} + if config: + click.echo( + "--config name:value will be deprecated in Datasette 1.0, use --setting name value instead", + err=True, + ) + combined_config.update(config) + combined_config.update(settings) + kwargs = dict( immutables=immutable, cache_headers=not reload, @@ -420,7 +472,7 @@ def serve( template_dir=template_dir, plugins_dir=plugins_dir, static_mounts=static, - config=dict(config), + config=combined_config, memory=memory, secret=secret, version_note=version_note, diff --git a/docs/datasette-serve-help.txt b/docs/datasette-serve-help.txt index 5a63d4c4..bdaf0894 100644 --- a/docs/datasette-serve-help.txt +++ b/docs/datasette-serve-help.txt @@ -25,9 +25,10 @@ Options: --plugins-dir DIRECTORY Path to directory containing custom plugins --static MOUNT:DIRECTORY Serve static files from this directory at /MOUNT/... --memory Make :memory: database available - --config CONFIG Set config option using configname:value - docs.datasette.io/en/stable/config.html + --config CONFIG Deprecated: set config option using configname:value. Use + --setting instead. + --setting SETTING... Setting, see docs.datasette.io/en/stable/config.html --secret TEXT Secret used for signing secure values, such as signed cookies diff --git a/tests/test_cli.py b/tests/test_cli.py index aa39b0ee..99aea053 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -4,6 +4,7 @@ from .fixtures import ( TestClient as _TestClient, EXPECTED_PLUGINS, ) +import asyncio from datasette.plugins import DEFAULT_PLUGINS from datasette.cli import cli, serve from datasette.version import __version__ @@ -17,6 +18,13 @@ import textwrap from unittest import mock +@pytest.fixture +def ensure_eventloop(): + # Workaround for "Event loop is closed" error + if asyncio.get_event_loop().is_closed(): + asyncio.set_event_loop(asyncio.new_event_loop()) + + def test_inspect_cli(app_client): runner = CliRunner() result = runner.invoke(cli, ["inspect", "fixtures.db"]) @@ -115,6 +123,7 @@ def test_metadata_yaml(): static=[], memory=False, config=[], + settings=[], secret=None, root=False, version_note=None, @@ -163,3 +172,30 @@ def test_version(): runner = CliRunner() result = runner.invoke(cli, ["--version"]) assert result.output == f"cli, version {__version__}\n" + + +def test_setting(ensure_eventloop): + runner = CliRunner() + result = runner.invoke( + cli, ["--setting", "default_page_size", "5", "--get", "/-/config.json"] + ) + assert result.exit_code == 0, result.output + assert json.loads(result.output)["default_page_size"] == 5 + + +def test_setting_type_validation(ensure_eventloop): + runner = CliRunner(mix_stderr=False) + result = runner.invoke(cli, ["--setting", "default_page_size", "dog"]) + assert result.exit_code == 2 + assert '"default_page_size" should be an integer' in result.stderr + + +def test_config_deprecated(ensure_eventloop): + # The --config option should show a deprecation message + runner = CliRunner(mix_stderr=False) + result = runner.invoke( + cli, ["--config", "allow_download:off", "--get", "/-/config.json"] + ) + assert result.exit_code == 0 + assert not json.loads(result.output)["allow_download"] + assert "will be deprecated in" in result.stderr