Require Python 3.11, remove SQLite authorizer workaround

PR #2998
This commit is contained in:
Simon Willison 2026-10-12 10:36:07 -07:00 • committed by GitHub
commit e1a4e03635
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 24 additions and 47 deletions

View file

@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"]
steps:
- uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
@ -62,7 +62,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.10'
python-version: '3.11'
cache: pip
cache-dependency-path: pyproject.toml
- name: Install dependencies

View file

@ -19,10 +19,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.10
- name: Set up Python 3.11
uses: actions/setup-python@v6
with:
python-version: "3.10"
python-version: "3.11"
cache: 'pip'
cache-dependency-path: '**/pyproject.toml'
- name: Cache Playwright browsers

View file

@ -20,7 +20,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"]
include:
- python-version: "3.14"
coverage: true

View file

@ -36,7 +36,7 @@ You can also install it using `pip` or `pipx`:
pip install datasette
Datasette requires Python 3.10 or higher. We also have [detailed installation instructions](https://docs.datasette.io/en/stable/installation.html) covering other options such as Docker.
Datasette requires Python 3.11 or higher. We also have [detailed installation instructions](https://docs.datasette.io/en/stable/installation.html) covering other options such as Docker.
## Basic usage

View file

@ -2212,7 +2212,7 @@ ORDER BY allowed.parent, allowed.child
# Log every check (including cache hits) for the debug page,
# dependencies before the actions that required them
when = datetime.datetime.now(datetime.timezone.utc).isoformat()
when = datetime.datetime.now(datetime.UTC).isoformat()
for name in reversed(expanded):
self._permission_checks.append(
PermissionCheck(

View file

@ -36,7 +36,7 @@ logger = logging.getLogger("datasette.background_tasks")
def _utcnow_iso() -> str:
return datetime.datetime.now(datetime.timezone.utc).isoformat()
return datetime.datetime.now(datetime.UTC).isoformat()
def _function_path(func: Callable) -> str:

View file

@ -25,12 +25,9 @@ def now(key, request):
if key == "epoch":
return int(time.time())
elif key == "date_utc":
return datetime.datetime.now(datetime.timezone.utc).date().isoformat()
return datetime.datetime.now(datetime.UTC).date().isoformat()
elif key == "datetime_utc":
return (
datetime.datetime.now(datetime.timezone.utc).strftime(r"%Y-%m-%dT%H:%M:%S")
+ "Z"
)
return datetime.datetime.now(datetime.UTC).strftime(r"%Y-%m-%dT%H:%M:%S") + "Z"
else:
raise KeyError

View file

@ -1,6 +1,6 @@
from abc import ABC, abstractproperty
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from datetime import UTC, datetime
from datasette.hookspecs import hookimpl
@ -11,9 +11,7 @@ class Event(ABC):
def name(self):
pass
created: datetime = field(
init=False, default_factory=lambda: datetime.now(timezone.utc)
)
created: datetime = field(init=False, default_factory=lambda: datetime.now(UTC))
actor: dict | None
def properties(self):

View file

@ -1,4 +1,3 @@
import sys
from dataclasses import dataclass
from typing import Literal
@ -193,20 +192,6 @@ _AUTHORIZER_ACTION_NAMES = {
}
def _allow_authorizer_action(*args):
return sqlite3.SQLITE_OK
def _disable_authorizer(conn):
# Python 3.11 added support for unregistering an authorizer using None.
# On Python 3.10, None is installed as the callback instead, and the next
# statement fails with "not authorized" when sqlite3 tries to call it.
if sys.version_info >= (3, 11):
conn.set_authorizer(None)
else:
conn.set_authorizer(_allow_authorizer_action)
def analyze_sql_tables(
conn,
sql: str,
@ -451,9 +436,7 @@ def analyze_sql_tables(
explain_rows = conn.execute(
"EXPLAIN " + sql, params if params is not None else {}
).fetchall()
# Passing None before these lookups leaves a failing callback installed
# on Python 3.10, so use a permissive callback until they are complete.
conn.set_authorizer(_allow_authorizer_action)
conn.set_authorizer(None)
if not operations:
vacuum_row = next((row for row in explain_rows if row[1] == "Vacuum"), None)
@ -495,7 +478,7 @@ def analyze_sql_tables(
conn, key.table, schema=key.sqlite_schema
)
finally:
_disable_authorizer(conn)
conn.set_authorizer(None)
has_schema_operation = any(
key.target_type in {"table", "index", "view", "trigger", "virtual-table"}

View file

@ -20,7 +20,7 @@ General guidelines
Setting up a development environment
------------------------------------
If you have Python 3.10 or higher installed on your computer (on OS X the quickest way to do this `is using homebrew <https://docs.python-guide.org/starting/install3/osx/>`__) you can install an editable copy of Datasette using the following steps.
If you have Python 3.11 or higher installed on your computer (on OS X the quickest way to do this `is using homebrew <https://docs.python-guide.org/starting/install3/osx/>`__) you can install an editable copy of Datasette using the following steps.
If you want to use GitHub to publish your changes, first `create a fork of datasette <https://github.com/simonw/datasette/fork>`__ under your own GitHub account.

View file

@ -47,7 +47,7 @@ If the latest packaged release of Datasette has not yet been made available thro
Using pip
---------
Datasette requires Python 3.10 or higher. The `Python.org Python For Beginners <https://www.python.org/about/gettingstarted/>`__ page has instructions for getting started.
Datasette requires Python 3.11 or higher. The `Python.org Python For Beginners <https://www.python.org/about/gettingstarted/>`__ page has instructions for getting started.
You can install Datasette and its dependencies using ``pip``::
@ -274,7 +274,7 @@ Use ``which datasette`` to confirm that ``datasette`` will run that version. The
/usr/local/opt/datasette/bin/datasette
If you get a different location here such as ``/Library/Frameworks/Python.framework/Versions/3.10/bin/datasette`` you can run the following command to cause ``datasette`` to execute the Homebrew version instead::
If you get a different location here such as ``/Library/Frameworks/Python.framework/Versions/3.11/bin/datasette`` you can run the following command to cause ``datasette`` to execute the Homebrew version instead::
alias datasette=$(echo $(brew --prefix datasette)/bin/datasette)

View file

@ -7,7 +7,7 @@ authors = [
{ name = "Simon Willison" },
]
license = "Apache-2.0"
requires-python = ">=3.10"
requires-python = ">=3.11"
classifiers = [
"Development Status :: 4 - Beta",
"Framework :: Datasette",
@ -15,11 +15,11 @@ classifiers = [
"Intended Audience :: Science/Research",
"Intended Audience :: End Users/Desktop",
"Topic :: Database",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3.15",
]
dependencies = [

View file

@ -1,7 +1,7 @@
line-length = 160
target-version = "py310"
target-version = "py311"
[lint.flake8-bugbear]
# from_extra() returns a dataclasses.field(), so it is safe as a dataclass
# default - ruff cannot see through the wrapper (RUF009)
extend-immutable-calls = ["datasette.views.from_extra"]
extend-immutable-calls = ["datasette.views.from_extra"]

View file

@ -1727,7 +1727,7 @@ async def test_hook_track_event():
# Should have a recent created as well
created = event.created
assert isinstance(created, datetime.datetime)
assert created.tzinfo == datetime.timezone.utc
assert created.tzinfo == datetime.UTC
@pytest.mark.asyncio

View file

@ -455,5 +455,4 @@ def test_analyze_disables_authorizer_on_error():
with pytest.raises(sqlite3.OperationalError):
analyze_sql_tables(conn, "bad SQL")
final_authorizer = conn.authorizers[-1]
assert final_authorizer is None or final_authorizer() == sqlite3.SQLITE_OK
assert conn.authorizers[-1] is None

View file

@ -523,7 +523,7 @@ async def test_write_wrapper_set_authorizer(datasette, actor, table, should_deny
try:
yield
finally:
conn.set_authorizer(lambda *args: sqlite3.SQLITE_OK)
conn.set_authorizer(None)
return wrapper