From c9d8a12a7e52c61fdeeb7dfc65096630b5334d13 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 13 Aug 2026 16:37:06 -0700 Subject: [PATCH 01/14] Test that should fail Refs https://github.com/simonw/sqlite-utils/issues/842#issuecomment-5287583061 --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6c720a1..920ec18 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,6 +53,9 @@ jobs: run: | pip install uv uv run ty check sqlite_utils + - name: Check no accidental dev= dependencies needed + run: | + uv run --no-default-groups sqlite-utils --help - name: Check formatting run: black . --check - name: Check if cog needs to be run From cf6a12568bacbfde56a70c7a10ec6c0bf239b20b Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 13 Aug 2026 16:38:58 -0700 Subject: [PATCH 02/14] Remove from typing_extensions import Self, refs #842 --- sqlite_utils/db.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sqlite_utils/db.py b/sqlite_utils/db.py index 37825bb..c011d9b 100644 --- a/sqlite_utils/db.py +++ b/sqlite_utils/db.py @@ -24,8 +24,6 @@ from typing import ( ) from sqlite_fts4 import rank_bm25 -from typing_extensions import Self - from sqlite_utils.plugins import ensure_plugins_loaded, pm from .create_table_parser import ( @@ -637,7 +635,7 @@ class Database: pm.hook.prepare_connection(conn=self.conn) self.strict = strict - def __enter__(self) -> Self: + def __enter__(self): return self def __exit__( From 8541829c2d28fe20c0afd6c0d15cff313b26685a Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 13 Aug 2026 16:39:19 -0700 Subject: [PATCH 03/14] Revert "Remove from typing_extensions import Self, refs #842" This reverts commit cf6a12568bacbfde56a70c7a10ec6c0bf239b20b. --- sqlite_utils/db.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sqlite_utils/db.py b/sqlite_utils/db.py index c011d9b..37825bb 100644 --- a/sqlite_utils/db.py +++ b/sqlite_utils/db.py @@ -24,6 +24,8 @@ from typing import ( ) from sqlite_fts4 import rank_bm25 +from typing_extensions import Self + from sqlite_utils.plugins import ensure_plugins_loaded, pm from .create_table_parser import ( @@ -635,7 +637,7 @@ class Database: pm.hook.prepare_connection(conn=self.conn) self.strict = strict - def __enter__(self): + def __enter__(self) -> Self: return self def __exit__( From d7b39f739389f2268145d78d8c5e1070ef60594c Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 13 Aug 2026 16:39:38 -0700 Subject: [PATCH 04/14] install uv first --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 920ec18..a252861 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -55,6 +55,7 @@ jobs: uv run ty check sqlite_utils - name: Check no accidental dev= dependencies needed run: | + pip install uv uv run --no-default-groups sqlite-utils --help - name: Check formatting run: black . --check From 829aa1438c6398d65d97ce68d5a24d68168c2efd Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 13 Aug 2026 16:38:58 -0700 Subject: [PATCH 05/14] Remove from typing_extensions import Self, refs #842 --- sqlite_utils/db.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sqlite_utils/db.py b/sqlite_utils/db.py index 37825bb..c011d9b 100644 --- a/sqlite_utils/db.py +++ b/sqlite_utils/db.py @@ -24,8 +24,6 @@ from typing import ( ) from sqlite_fts4 import rank_bm25 -from typing_extensions import Self - from sqlite_utils.plugins import ensure_plugins_loaded, pm from .create_table_parser import ( @@ -637,7 +635,7 @@ class Database: pm.hook.prepare_connection(conn=self.conn) self.strict = strict - def __enter__(self) -> Self: + def __enter__(self): return self def __exit__( From bcf8b564eea75cee06e34296fb0957684a043e24 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 13 Aug 2026 16:46:31 -0700 Subject: [PATCH 06/14] Only run uv smoke test on Ubuntu Refs https://github.com/simonw/sqlite-utils/pull/843#issuecomment-5287666426 --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a252861..5924fd8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -54,6 +54,7 @@ jobs: pip install uv uv run ty check sqlite_utils - name: Check no accidental dev= dependencies needed + if: matrix.os == 'ubuntu-latest' run: | pip install uv uv run --no-default-groups sqlite-utils --help From f6d73112c8368cd6eb2ac596966e8148747c7b4e Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 13 Aug 2026 16:52:03 -0700 Subject: [PATCH 07/14] Fix for sqlite-utils 4.2 crashing bug (#843) - Remove from typing_extensions import Self - Smoke test: uv run --no-default-groups sqlite-utils --help Closes #842 --- .github/workflows/test.yml | 5 +++++ sqlite_utils/db.py | 4 +--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6c720a1..5924fd8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,6 +53,11 @@ jobs: run: | pip install uv uv run ty check sqlite_utils + - name: Check no accidental dev= dependencies needed + if: matrix.os == 'ubuntu-latest' + run: | + pip install uv + uv run --no-default-groups sqlite-utils --help - name: Check formatting run: black . --check - name: Check if cog needs to be run diff --git a/sqlite_utils/db.py b/sqlite_utils/db.py index 37825bb..c011d9b 100644 --- a/sqlite_utils/db.py +++ b/sqlite_utils/db.py @@ -24,8 +24,6 @@ from typing import ( ) from sqlite_fts4 import rank_bm25 -from typing_extensions import Self - from sqlite_utils.plugins import ensure_plugins_loaded, pm from .create_table_parser import ( @@ -637,7 +635,7 @@ class Database: pm.hook.prepare_connection(conn=self.conn) self.strict = strict - def __enter__(self) -> Self: + def __enter__(self): return self def __exit__( From 28dc6278cc03a9245325d056e6986818544abc68 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 13 Aug 2026 16:52:30 -0700 Subject: [PATCH 08/14] Release 4.2.1 Refs #842, #843 --- docs/changelog.rst | 7 +++++++ pyproject.toml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 31bd961..5d024e1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,13 @@ Changelog =========== +.. _v4_2_1: + +4.2.1 (2026-08-13) +------------------ + +- Fix for ``No module named 'typing_extensions'`` crashing bug accidentally shipped in version 4.2. (:issue:`842`) + .. _v4_2: 4.2 (2026-08-13) diff --git a/pyproject.toml b/pyproject.toml index 6dac11c..92650e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sqlite-utils" -version = "4.2" +version = "4.2.1" description = "CLI tool and Python library for manipulating SQLite databases" readme = { file = "README.md", content-type = "text/markdown" } authors = [ From 56dd09702fdb9e899f577ffd51693c1f2176cb08 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 13 Aug 2026 17:01:47 -0700 Subject: [PATCH 09/14] Run no-default-groups smoke test from Justfile Refs #842 I had to add --isolated because otherwise this test would pass if a .venv folder already existed with the dev dependencies installed in it. --- Justfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Justfile b/Justfile index e93075f..7347534 100644 --- a/Justfile +++ b/Justfile @@ -2,9 +2,12 @@ @default: test lint # Run pytest with supplied options -@test *options: +@test *options: test-no-dev-dependencies uv run pytest {{options}} +@test-no-dev-dependencies: + uv run --isolated --no-default-groups sqlite-utils --help > /dev/null + @run *options: uv run -- {{options}} From b97295271c0794200b2a35d4ed838759ee88a49f Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 1 Sep 2026 13:42:27 -0700 Subject: [PATCH 10/14] Test against 3.15 RCs See https://simonwillison.net/2026/Sep/1/python-315-rc-2/ --- .github/workflows/test.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5924fd8..3ce5fa3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,18 +10,19 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15-dev"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"] numpy: [0, 1] os: [ubuntu-latest, macos-latest, windows-latest, macos-14] steps: - uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} allow-prereleases: true cache: pip cache-dependency-path: pyproject.toml + check-latest: true - name: Install dependencies run: | pip install . --group dev From f7e3174401ce6059c49051aecfe283cbbd2d712a Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 1 Sep 2026 21:35:28 -0700 Subject: [PATCH 11/14] Fix for SpatialLite installation failure Fable 5.1 explains: > apt on the runner image has stale package lists. It tried to download libminizip1t64_1.3.dfsg-3.1ubuntu2.1 from security.ubuntu.com and got a 404, because Ubuntu has since published a newer build of that package and pulled the old .deb from the mirror. --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3ce5fa3..3832dd9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,7 +31,7 @@ jobs: run: pip install numpy - name: Install SpatiaLite if: matrix.os == 'ubuntu-latest' - run: sudo apt-get install libsqlite3-mod-spatialite + run: sudo apt-get update && sudo apt-get install -y libsqlite3-mod-spatialite - name: Build extension for --load-extension test if: matrix.os == 'ubuntu-latest' run: |- From 85b1be10c81d9dd3567e36faf8dd411e4a8789bd Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 1 Sep 2026 21:39:41 -0700 Subject: [PATCH 12/14] Same fix for test-coverage.yml Refs https://github.com/simonw/sqlite-utils/pull/852/changes/ef50b31a21104351445c4856ff3a80b81b52e847 --- .github/workflows/test-coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml index 7668f1b..c7b05c4 100644 --- a/.github/workflows/test-coverage.yml +++ b/.github/workflows/test-coverage.yml @@ -20,7 +20,7 @@ jobs: cache: pip cache-dependency-path: pyproject.toml - name: Install SpatiaLite - run: sudo apt-get install libsqlite3-mod-spatialite + run: sudo apt-get update && sudo apt-get install -y libsqlite3-mod-spatialite - name: Install Python dependencies run: | python -m pip install --upgrade pip From 6bc1d33d583c54bd69fbdd2071117e2d38c354a1 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 22 Sep 2026 11:34:27 -0700 Subject: [PATCH 13/14] Fix resource leaks caused by buffered readers Spotted running 'just test -Wall' on the LLM project. --- sqlite_utils/utils.py | 6 +++- tests/test_rows_from_file.py | 70 ++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/sqlite_utils/utils.py b/sqlite_utils/utils.py index ee6695b..91fd24d 100644 --- a/sqlite_utils/utils.py +++ b/sqlite_utils/utils.py @@ -380,10 +380,14 @@ def rows_from_file( "rows_from_file() requires a file-like object that supports peek(), such as io.BytesIO" ) if not first_bytes: + buffered.close() return (), Format.CSV if first_bytes.startswith((b"[", b"{")): + # JSON is read eagerly, so the detection wrapper can close now, + # including when parsing raises an error. # TODO: Detect newline-JSON - return rows_from_file(buffered, format=Format.JSON) + with buffered: + return rows_from_file(buffered, format=Format.JSON) else: dialect = csv.Sniffer().sniff( first_bytes.decode(encoding or "utf-8-sig", "ignore") diff --git a/tests/test_rows_from_file.py b/tests/test_rows_from_file.py index 3de3582..a8e7f9d 100644 --- a/tests/test_rows_from_file.py +++ b/tests/test_rows_from_file.py @@ -1,3 +1,5 @@ +import io +import json from io import BytesIO, StringIO import pytest @@ -61,3 +63,71 @@ def test_rows_from_file_error_on_string_io(): assert ex.value.args == ( "rows_from_file() requires a file-like object that supports peek(), such as io.BytesIO", ) + + +@pytest.fixture +def buffered_readers(monkeypatch): + # Keep wrappers alive so these checks cannot pass due to garbage collection. + readers = [] + original = io.BufferedReader + + def buffered_reader(*args, **kwargs): + reader = original(*args, **kwargs) + readers.append(reader) + return reader + + monkeypatch.setattr(io, "BufferedReader", buffered_reader) + yield readers + for reader in readers: + reader.close() + + +@pytest.mark.parametrize( + "content, expected_format, expected_rows", + [ + (b'[{"id": 1}]', Format.JSON, [{"id": 1}]), + (b'{"id": 1}', Format.JSON, [{"id": 1}]), + (b"[]", Format.JSON, []), + (b"", Format.CSV, []), + (b" \n\t", Format.CSV, []), + ], +) +def test_detect_format_closes_eager_reader( + tmp_path, buffered_readers, content, expected_format, expected_rows +): + path = tmp_path / "input" + path.write_bytes(content) + with path.open("rb") as fp: + rows, detected = rows_from_file(fp) + assert detected == expected_format + assert list(rows) == expected_rows + assert len(buffered_readers) == 1 + assert buffered_readers[0].closed + + +@pytest.mark.parametrize("content", [b"[", b'{"id":']) +def test_detect_format_closes_reader_on_invalid_json( + tmp_path, buffered_readers, content +): + path = tmp_path / "input.json" + path.write_bytes(content) + with path.open("rb") as fp: + with pytest.raises(json.JSONDecodeError): + rows_from_file(fp) + assert len(buffered_readers) == 1 + assert buffered_readers[0].closed + + +@pytest.mark.parametrize( + "delimiter, expected_format", [(b",", Format.CSV), (b"\t", Format.TSV)] +) +def test_detect_format_keeps_streaming_reader_open( + buffered_readers, delimiter, expected_format +): + content = b"id" + delimiter + b"name\n" + (b"1" + delimiter + b"Cleo\n") * 2000 + rows, detected = rows_from_file(BytesIO(content)) + assert detected == expected_format + assert len(buffered_readers) == 1 + assert not buffered_readers[0].closed + assert len(list(rows)) == 2000 + assert buffered_readers[0].closed From 9c66bafa52ae23d40499c74b0db3b635bcddb876 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Sat, 10 Oct 2026 17:15:54 -0700 Subject: [PATCH 14/14] Remove allow-prereleases option from Python setup Not needed now Python 3.15 is out. --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3832dd9..a1cb09b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,6 @@ jobs: uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} - allow-prereleases: true cache: pip cache-dependency-path: pyproject.toml check-latest: true