datasette/docs/upgrade-1.0a20.md
Simon Willison 5d4dfcec6b Fix for link from changelog not working
Annoyingly we now get a warning in the docs build about a duplicate label,
but it seems harmless enough.
2025-11-03 14:38:57 -08:00

289 lines
8.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
orphan: true
---
(upgrade_guide_v1_a20)=
# Datasette 1.0a20 plugin upgrade guide
Datasette 1.0a20 makes some breaking changes to Datasette's permission system. Plugins need to be updated if they use **any of the following**:
- The `register_permissions()` plugin hook - this should be replaced with `register_actions`
- The `permission_allowed()` plugin hook - this should be upgraded to use `permission_resources_sql()`.
- The `datasette.permission_allowed()` internal method - this should be replaced with `datasette.allowed()`
- Logic that grants access to the `"root"` actor can be removed.
## Permissions are now actions
The `register_permissions()` hook shoud be replaced with `register_actions()`.
Old code:
```python
@hookimpl
def register_permissions(datasette):
return [
Permission(
name="explain-sql",
abbr=None,
description="Can explain SQL queries",
takes_database=True,
takes_resource=False,
default=False,
),
Permission(
name="annotate-rows",
abbr=None,
description="Can annotate rows",
takes_database=True,
takes_resource=True,
default=False,
),
Permission(
name="view-debug-info",
abbr=None,
description="Can view debug information",
takes_database=False,
takes_resource=False,
default=False,
),
]
```
The new `Action` does not have a `default=` parameter.
Here's the equivalent new code:
```python
from datasette import hookimpl
from datasette.permissions import Action
from datasette.resources import DatabaseResource, TableResource
@hookimpl
def register_actions(datasette):
return [
Action(
name="explain-sql",
description="Explain SQL queries",
resource_class=DatabaseResource,
),
Action(
name="annotate-rows",
description="Annotate rows",
resource_class=TableResource,
),
Action(
name="view-debug-info",
description="View debug information",
),
]
```
The `abbr=` is now optional and defaults to `None`.
For actions that apply to specific resources (like databases or tables), specify the `resource_class` instead of `takes_parent` and `takes_child`. Note that `view-debug-info` does not specify a `resource_class` because it applies globally.
## permission_allowed() hook is replaced by permission_resources_sql()
The following old code:
```python
@hookimpl
def permission_allowed(action):
if action == "permissions-debug":
return True
```
Can be replaced by:
```python
from datasette.permissions import PermissionSQL
@hookimpl
def permission_resources_sql(action):
return PermissionSQL.allow(reason="datasette-allow-permissions-debug")
```
A `.deny(reason="")` class method is also available.
For more complex permission checks consult the documentation for that plugin hook:
<https://docs.datasette.io/en/latest/plugin_hooks.html#permission-resources-sql-datasette-actor-action>
## Using datasette.allowed() to check permissions instead of datasette.permission_allowed()
The internal method `datasette.permission_allowed()` has been replaced by `datasette.allowed()`.
The old method looked like this:
```python
can_debug = await datasette.permission_allowed(
request.actor,
"view-debug-info",
)
can_explain_sql = await datasette.permission_allowed(
request.actor,
"explain-sql",
resource="database_name",
)
can_annotate_rows = await datasette.permission_allowed(
request.actor,
"annotate-rows",
resource=(database_name, table_name),
)
```
Note the confusing design here where `resource` could be either a string or a tuple depending on the permission being checked.
The new keyword-only design makes this a lot more clear:
```python
from datasette.resources import DatabaseResource, TableResource
can_debug = await datasette.allowed(
actor=request.actor,
action="view-debug-info",
)
can_explain_sql = await datasette.allowed(
actor=request.actor,
action="explain-sql",
resource=DatabaseResource(database_name),
)
can_annotate_rows = await datasette.allowed(
actor=request.actor,
action="annotate-rows",
resource=TableResource(database_name, table_name),
)
```
## Root user checks are no longer necessary
Some plugins would introduce their own custom permission and then ensure the `"root"` actor had access to it using a pattern like this:
```python
@hookimpl
def register_permissions(datasette):
return [
Permission(
name="upload-dbs",
abbr=None,
description="Upload SQLite database files",
takes_database=False,
takes_resource=False,
default=False,
)
]
@hookimpl
def permission_allowed(actor, action):
if action == "upload-dbs" and actor and actor.get("id") == "root":
return True
```
This is no longer necessary in Datasette 1.0a20 - the `"root"` actor automatically has all permissions when Datasette is started with the `datasette --root` option.
The `permission_allowed()` hook in this example can be entirely removed.
### Root-enabled instances during testing
When writing tests that exercise root-only functionality, make sure to set `datasette.root_enabled = True` on the `Datasette` instance. Root permissions are only granted automatically when Datasette is started with `datasette --root` or when the flag is enabled directly in tests.
## Target the new APIs exclusively
Datasette 1.0a20s permission system is substantially different from previous releases. Attempting to keep plugin code compatible with both the old `permission_allowed()` and the new `allowed()` interfaces leads to brittle workarounds. Prefer to adopt the 1.0a20 APIs (`register_actions`, `permission_resources_sql()`, and `datasette.allowed()`) outright and drop legacy fallbacks.
## Fixing async with httpx.AsyncClient(app=app)
Some older plugins may use the following pattern in their tests, which is no longer supported:
```python
app = Datasette([], memory=True).app()
async with httpx.AsyncClient(app=app) as client:
response = await client.get("http://localhost/path")
```
The new pattern is to use `ds.client` like this:
```python
ds = Datasette([], memory=True)
response = await ds.client.get("/path")
```
## Migrating from metadata= to config=
Datasette 1.0 separates metadata (titles, descriptions, licenses) from configuration (settings, plugins, queries, permissions). Plugin tests and code need to be updated accordingly.
### Update test constructors
Old code:
```python
ds = Datasette(
memory=True,
metadata={
"databases": {
"_memory": {"queries": {"my_query": {"sql": "select 1", "title": "My Query"}}}
},
"plugins": {
"my-plugin": {"setting": "value"}
}
}
)
```
New code:
```python
ds = Datasette(
memory=True,
config={
"databases": {
"_memory": {"queries": {"my_query": {"sql": "select 1", "title": "My Query"}}}
},
"plugins": {
"my-plugin": {"setting": "value"}
}
}
)
```
### Update datasette.metadata() calls
The `datasette.metadata()` method has been removed. Use these methods instead:
Old code:
```python
try:
title = datasette.metadata(database=database)["queries"][query_name]["title"]
except (KeyError, TypeError):
pass
```
New code:
```python
try:
query_info = await datasette.get_canned_query(database, query_name, request.actor)
if query_info and "title" in query_info:
title = query_info["title"]
except (KeyError, TypeError):
pass
```
### Update render functions to async
If your plugin's render function needs to call `datasette.get_canned_query()` or other async Datasette methods, it must be declared as async:
Old code:
```python
def render_atom(datasette, request, sql, columns, rows, database, table, query_name, view_name, data):
# ...
if query_name:
title = datasette.metadata(database=database)["queries"][query_name]["title"]
```
New code:
```python
async def render_atom(datasette, request, sql, columns, rows, database, table, query_name, view_name, data):
# ...
if query_name:
query_info = await datasette.get_canned_query(database, query_name, request.actor)
if query_info and "title" in query_info:
title = query_info["title"]
```
### Update query URLs in tests
Datasette now redirects `?sql=` parameters from database pages to the query view:
Old code:
```python
response = await ds.client.get("/_memory.atom?sql=select+1")
```
New code:
```python
response = await ds.client.get("/_memory/-/query.atom?sql=select+1")
```