diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 232a34c7..fb390a44 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -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 diff --git a/.github/workflows/test-pyodide.yml b/.github/workflows/test-pyodide.yml index 449855f3..35dc8542 100644 --- a/.github/workflows/test-pyodide.yml +++ b/.github/workflows/test-pyodide.yml @@ -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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8176a630..6dd07b19 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/README.md b/README.md index 1f79778f..6d7b7276 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/datasette/app.py b/datasette/app.py index 001ce9b3..b2cb24b7 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -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( diff --git a/datasette/background_tasks.py b/datasette/background_tasks.py index 6b34fd85..6777a163 100644 --- a/datasette/background_tasks.py +++ b/datasette/background_tasks.py @@ -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: diff --git a/datasette/default_magic_parameters.py b/datasette/default_magic_parameters.py index bff5f0a7..239637e7 100644 --- a/datasette/default_magic_parameters.py +++ b/datasette/default_magic_parameters.py @@ -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 diff --git a/datasette/events.py b/datasette/events.py index 5f3fd06e..8746d7ac 100644 --- a/datasette/events.py +++ b/datasette/events.py @@ -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): diff --git a/datasette/utils/sql_analysis.py b/datasette/utils/sql_analysis.py index 6325e890..5b636404 100644 --- a/datasette/utils/sql_analysis.py +++ b/datasette/utils/sql_analysis.py @@ -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"} diff --git a/docs/contributing.rst b/docs/contributing.rst index 2de1fce0..bbd47865 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -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 `__) 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 `__) 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 `__ under your own GitHub account. diff --git a/docs/installation.rst b/docs/installation.rst index ceec7f23..52e72c0f 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -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 `__ page has instructions for getting started. +Datasette requires Python 3.11 or higher. The `Python.org Python For Beginners `__ 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) diff --git a/pyproject.toml b/pyproject.toml index cd8b5513..271d695b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = [ diff --git a/ruff.toml b/ruff.toml index 3c4345bf..b9ee4612 100644 --- a/ruff.toml +++ b/ruff.toml @@ -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"] \ No newline at end of file +extend-immutable-calls = ["datasette.views.from_extra"] diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 1084d270..58e154a2 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -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 diff --git a/tests/test_utils_sql_analysis.py b/tests/test_utils_sql_analysis.py index 363814d7..4201fb0d 100644 --- a/tests/test_utils_sql_analysis.py +++ b/tests/test_utils_sql_analysis.py @@ -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 diff --git a/tests/test_write_wrapper.py b/tests/test_write_wrapper.py index 45eea483..b65e54b2 100644 --- a/tests/test_write_wrapper.py +++ b/tests/test_write_wrapper.py @@ -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