Skip to content

Commit

Permalink
Treat plugins in metadata as if they were in config, closes #2248
Browse files Browse the repository at this point in the history
  • Loading branch information
simonw committed Feb 1, 2024
1 parent d4bc2b2 commit be4f023
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 0 deletions.
6 changes: 6 additions & 0 deletions datasette/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
find_spatialite,
format_bytes,
module_from_path,
move_plugins,
parse_metadata,
resolve_env_secrets,
resolve_routes,
Expand Down Expand Up @@ -341,6 +342,11 @@ def __init__(
with config_files[0].open() as fp:
config = parse_metadata(fp.read())

# Move any "plugins" settings from metadata to config - updates them in place
metadata = metadata or {}
config = config or {}
move_plugins(metadata, config)

self._metadata_local = metadata or {}
self.sqlite_extensions = []
for extension in sqlite_extensions or []:
Expand Down
40 changes: 40 additions & 0 deletions datasette/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1287,3 +1287,43 @@ async def inner():
return markupsafe.Markup("".join(html_bits))

return inner


def move_plugins(source, destination):
"""
Move 'plugins' keys from source to destination dictionary. Creates hierarchy in destination if needed.
After moving, recursively remove any keys in the source that are left empty.
"""

def recursive_move(src, dest, path=None):
if path is None:
path = []
for key, value in list(src.items()):
new_path = path + [key]
if key == "plugins":
# Navigate and create the hierarchy in destination if needed
d = dest
for step in path:
d = d.setdefault(step, {})
# Move the plugins
d[key] = value
# Remove the plugins from source
src.pop(key, None)
elif isinstance(value, dict):
recursive_move(value, dest, new_path)
# After moving, check if the current dictionary is empty and remove it if so
if not value:
src.pop(key, None)

def prune_empty_dicts(d):
"""
Recursively prune all empty dictionaries from a given dictionary.
"""
for key, value in list(d.items()):
if isinstance(value, dict):
prune_empty_dicts(value)
if value == {}:
d.pop(key, None)

recursive_move(source, destination)
prune_empty_dicts(source)
51 changes: 51 additions & 0 deletions tests/test_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -1458,3 +1458,54 @@ async def test_hook_register_events():
datasette = Datasette(memory=True)
await datasette.invoke_startup()
assert any(k.__name__ == "OneEvent" for k in datasette.event_classes)


@pytest.mark.parametrize(
"metadata,config,expected_metadata,expected_config",
(
(
# Instance level
{"plugins": {"datasette-foo": "bar"}},
{},
{},
{"plugins": {"datasette-foo": "bar"}},
),
(
# Database level
{"databases": {"foo": {"plugins": {"datasette-foo": "bar"}}}},
{},
{},
{"databases": {"foo": {"plugins": {"datasette-foo": "bar"}}}},
),
(
# Table level
{
"databases": {
"foo": {"tables": {"bar": {"plugins": {"datasette-foo": "bar"}}}}
}
},
{},
{},
{
"databases": {
"foo": {"tables": {"bar": {"plugins": {"datasette-foo": "bar"}}}}
}
},
),
(
# Keep other keys
{"plugins": {"datasette-foo": "bar"}, "other": "key"},
{"original_config": "original"},
{"other": "key"},
{"original_config": "original", "plugins": {"datasette-foo": "bar"}},
),
),
)
def test_metadata_plugin_config_treated_as_config(
metadata, config, expected_metadata, expected_config
):
ds = Datasette(metadata=metadata, config=config)
actual_metadata = ds.metadata()
assert "plugins" not in actual_metadata
assert actual_metadata == expected_metadata
assert ds.config == expected_config

0 comments on commit be4f023

Please sign in to comment.