Skip to content

Commit 5af9f59

Browse files
Accept any sequence shape for AsyncAdaptedCursor.description
PEP 249 specifies cursor.description as a sequence of sequences, not a strict list. Pinning _Description to ``list[...] | None`` would reject a dbapi cursor returning tuple-of-tuples (which SQLAlchemy's own aiosqlite adapter accepts). Widen the alias to Sequence[_DescriptionTuple] | None so the adapter passes the description through verbatim.
1 parent 6d97a52 commit 5af9f59

2 files changed

Lines changed: 36 additions & 1 deletion

File tree

src/sqlalchemydqlite/aio.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,14 @@
2626
# Re-declared here (not imported) to keep the sqlalchemy-dqlite
2727
# runtime contract explicit to type-checkers even if the dbapi layer
2828
# later exposes a named alias.
29+
# PEP 249 specifies ``cursor.description`` as a sequence of sequences —
30+
# a ``list[tuple]`` is the canonical shape but a strict type alias of
31+
# ``list`` would reject a dbapi cursor that returns a tuple-of-tuples
32+
# (which sqlalchemy's own aiosqlite adapter accepts). Keep the alias
33+
# permissive so the adapter passes through whatever the underlying
34+
# cursor returns without copying.
2935
_DescriptionTuple = tuple[str, int | None, None, None, None, None, None]
30-
_Description = list[_DescriptionTuple] | None
36+
_Description = Sequence[_DescriptionTuple] | None
3137

3238

3339
class AsyncAdaptedCursor:

tests/test_async_adapter.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,3 +461,32 @@ def test_cursor_returns_async_adapted_cursor_wrapping_inner(self) -> None:
461461

462462
cursor = adapter.cursor()
463463
assert isinstance(cursor, AsyncAdaptedCursor)
464+
465+
466+
class TestAsyncAdaptedCursorDescriptionType:
467+
"""``cursor.description`` is a PEP 249 sequence of sequences. The
468+
adapter must accept (and pass through) any sequence the underlying
469+
dbapi cursor returns — list, tuple, or other — without
470+
converting."""
471+
472+
def test_description_passes_through_tuple_of_tuples(self) -> None:
473+
cursor = _make_cursor()
474+
475+
mock_inner = MagicMock()
476+
# Underlying cursor returns a tuple-of-tuples description.
477+
mock_inner.description = (
478+
("id", 1, None, None, None, None, None),
479+
("name", 3, None, None, None, None, None),
480+
)
481+
mock_inner.lastrowid = None
482+
mock_inner.rowcount = 0
483+
mock_inner.execute.return_value = None
484+
mock_inner.fetchall.return_value = []
485+
mock_inner.close.return_value = None
486+
cursor._connection.cursor.return_value = mock_inner
487+
488+
with patch("sqlalchemydqlite.aio.await_only", side_effect=_run_sync):
489+
cursor.execute("SELECT id, name FROM t")
490+
491+
# Adapter must preserve the description untouched.
492+
assert cursor.description == mock_inner.description

0 commit comments

Comments
 (0)