Skip to content

Commit 2679879

Browse files
Narrow AsyncAdaptedCursor.fetchone return type to Any | None
The method returns None when the buffered deque is empty, matching the PEP 249 contract, but the annotation said Any — which swallows the None case silently at call sites. Align with the sibling dqlitedbapi.Cursor and dqlitedbapi.aio.AsyncCursor, which both type fetchone as tuple[Any, ...] | None, and pin the annotation so a future widening is caught in tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent eee2b82 commit 2679879

2 files changed

Lines changed: 17 additions & 1 deletion

File tree

src/sqlalchemydqlite/aio.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,11 @@ def executemany(self, operation: str, seq_of_parameters: Iterable[Sequence[Any]]
133133
finally:
134134
await_only(cursor.close())
135135

136-
def fetchone(self) -> Any:
136+
def fetchone(self) -> Any | None:
137+
# Narrow from ``Any`` so callers understand None is a legitimate
138+
# return on exhaustion (PEP 249 contract, mirroring the
139+
# dqlitedbapi sync / async cursors that already type this as
140+
# ``tuple[Any, ...] | None``). Runtime behaviour unchanged.
137141
if self._rows:
138142
return self._rows.popleft()
139143
return None

tests/test_async_adapter.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,18 @@ def test_fetchone_pops_left(self) -> None:
380380
assert cursor.fetchone() == (3,) # type: ignore[attr-defined]
381381
assert cursor.fetchone() is None # type: ignore[attr-defined]
382382

383+
def test_fetchone_return_annotation_admits_none(self) -> None:
384+
"""Pin ``fetchone -> Any | None`` so a silent widening to
385+
``Any`` (which hides the None-on-exhaustion case from callers)
386+
is caught here rather than at a downstream type-check site.
387+
"""
388+
import typing
389+
390+
hints = typing.get_type_hints(AsyncAdaptedCursor.fetchone)
391+
# typing.get_type_hints resolves ``Any | None`` to the runtime
392+
# union; compare as a set so ordering is ignored.
393+
assert set(typing.get_args(hints["return"])) == {typing.Any, type(None)}
394+
383395
def test_fetchmany_default_uses_arraysize(self) -> None:
384396
cursor = self._cursor_with_rows([(1,), (2,), (3,), (4,)])
385397
cursor.arraysize = 2 # type: ignore[attr-defined]

0 commit comments

Comments
 (0)