Skip to content

Commit ee91768

Browse files
Narrow return types on the async dialect adapter surface
Swap several Any return annotations for the precise types the bodies already return: execute/executemany -> None, __iter__ -> Iterator[Any], import_dbapi -> types.ModuleType, and _DqliteDate.result_processor -> Callable[[Any], Any] | None. AsyncAdaptedConnection.__init__ now types its parameter as the concrete AsyncConnection for callers while the stored attribute stays Any to satisfy SQLAlchemy's wider Protocol. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 859c2fe commit ee91768

3 files changed

Lines changed: 44 additions & 10 deletions

File tree

src/sqlalchemydqlite/aio.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
"""Async dqlite dialect for SQLAlchemy."""
22

33
import contextlib
4+
import types
45
from collections import deque
5-
from collections.abc import Sequence
6-
from typing import Any
6+
from collections.abc import Iterator, Sequence
7+
from typing import TYPE_CHECKING, Any
78

89
from sqlalchemy import pool
910
from sqlalchemy.engine import URL, AdaptedConnection
@@ -14,6 +15,9 @@
1415
from dqlitedbapi.exceptions import InterfaceError, NotSupportedError, OperationalError
1516
from sqlalchemydqlite.base import DqliteDialect
1617

18+
if TYPE_CHECKING:
19+
from dqlitedbapi.aio import AsyncConnection
20+
1721
__all__ = ["AsyncAdaptedConnection", "AsyncAdaptedCursor", "DqliteDialect_aio"]
1822

1923

@@ -42,7 +46,7 @@ async def _async_soft_close(self) -> None:
4246
def close(self) -> None:
4347
self._rows.clear()
4448

45-
def execute(self, operation: str, parameters: Any = None) -> Any:
49+
def execute(self, operation: str, parameters: Any = None) -> None:
4650
# Clear buffered state FIRST so a CancelledError (or any other
4751
# exception) during execute/fetchall leaves the adapter in a
4852
# "no active result" state rather than carrying stale rows
@@ -68,7 +72,7 @@ def execute(self, operation: str, parameters: Any = None) -> Any:
6872
finally:
6973
await_only(cursor.close())
7074

71-
def executemany(self, operation: str, seq_of_parameters: Any) -> Any:
75+
def executemany(self, operation: str, seq_of_parameters: Any) -> None:
7276
# Clear state up-front so cancellation mid-call doesn't leak
7377
# a previous execution's buffered rows.
7478
self.description = None
@@ -139,7 +143,7 @@ def nextset(self) -> bool | None:
139143
def scroll(self, value: int, mode: str = "relative") -> None:
140144
raise NotSupportedError("dqlite cursors are not scrollable")
141145

142-
def __iter__(self) -> Any:
146+
def __iter__(self) -> Iterator[Any]:
143147
while self._rows:
144148
yield self._rows.popleft()
145149

@@ -158,8 +162,13 @@ class AsyncAdaptedConnection(AdaptedConnection):
158162
greenlet context.
159163
"""
160164

161-
def __init__(self, connection: Any) -> None:
162-
self._connection = connection
165+
def __init__(self, connection: "AsyncConnection") -> None:
166+
# ``_connection`` is the concrete ``dqlitedbapi.aio.AsyncConnection``
167+
# this adapter wraps; SQLAlchemy's parent ``AdaptedConnection``
168+
# declares the attribute with a wider Protocol type, so we keep
169+
# the store on ``Any`` and rely on the annotation here to document
170+
# the intended input shape.
171+
self._connection: Any = connection
163172

164173
def cursor(self) -> AsyncAdaptedCursor:
165174
return AsyncAdaptedCursor(self)
@@ -225,7 +234,7 @@ def get_pool_class(cls, url: URL) -> type[pool.Pool]:
225234
return AsyncAdaptedQueuePool
226235

227236
@classmethod
228-
def import_dbapi(cls) -> Any:
237+
def import_dbapi(cls) -> types.ModuleType:
229238
from dqlitedbapi import aio
230239

231240
return aio

src/sqlalchemydqlite/base.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import contextlib
44
import datetime
55
import math
6+
import types
67
import warnings
78
from collections.abc import Callable
89
from typing import Any
@@ -49,7 +50,7 @@ class _DqliteDate(sqltypes.Date):
4950
def bind_processor(self, dialect: Any) -> None:
5051
return None
5152

52-
def result_processor(self, dialect: Any, coltype: Any) -> Any:
53+
def result_processor(self, dialect: Any, coltype: Any) -> Callable[[Any], Any] | None:
5354
def process(value: Any) -> Any:
5455
if isinstance(value, datetime.datetime):
5556
# Deliberate: tzinfo is dropped. See class docstring.
@@ -148,7 +149,7 @@ class DqliteDialect(SQLiteDialect):
148149
}
149150

150151
@classmethod
151-
def import_dbapi(cls) -> Any:
152+
def import_dbapi(cls) -> types.ModuleType:
152153
import dqlitedbapi
153154

154155
return dqlitedbapi

tests/test_async_adapter.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,27 @@ def test_extra_positional_argument_rejected(self) -> None:
230230
cursor = AsyncAdaptedCursor.__new__(AsyncAdaptedCursor)
231231
with pytest.raises(TypeError):
232232
cursor.setinputsizes([10], 20) # type: ignore[call-arg]
233+
234+
235+
class TestAioAdapterReturnAnnotations:
236+
"""Lock in the narrower return annotations on the async adapter surface."""
237+
238+
def test_execute_and_executemany_return_none(self) -> None:
239+
import inspect
240+
241+
from sqlalchemydqlite.aio import AsyncAdaptedCursor
242+
243+
sig = inspect.signature(AsyncAdaptedCursor.execute)
244+
assert sig.return_annotation is None or sig.return_annotation == "None"
245+
246+
sig = inspect.signature(AsyncAdaptedCursor.executemany)
247+
assert sig.return_annotation is None or sig.return_annotation == "None"
248+
249+
def test_iter_returns_iterator(self) -> None:
250+
import inspect
251+
252+
from sqlalchemydqlite.aio import AsyncAdaptedCursor
253+
254+
sig = inspect.signature(AsyncAdaptedCursor.__iter__)
255+
# Accept either the Iterator annotation object or the stringified form.
256+
assert "Iterator" in str(sig.return_annotation)

0 commit comments

Comments
 (0)