Skip to content

Commit 1525da0

Browse files
fix(cursor): make close() idempotent on sync and async cursors
Add an early-return guard when the cursor is already closed so a second close() is a no-op. This matches Connection.close() and the convention used by sqlite3 / psycopg. The sync path also avoids a spurious _check_thread() call from a finalizer thread during interpreter shutdown. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f51aad8 commit 1525da0

File tree

4 files changed

+29
-2
lines changed

4 files changed

+29
-2
lines changed

src/dqlitedbapi/aio/cursor.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,12 @@ async def fetchall(self) -> list[tuple[Any, ...]]:
187187
return result
188188

189189
async def close(self) -> None:
190-
"""Close the cursor."""
190+
"""Close the cursor.
191+
192+
Idempotent: a second call is a no-op.
193+
"""
194+
if self._closed:
195+
return
191196
self._closed = True
192197
self._rows = []
193198
self._description = None

src/dqlitedbapi/cursor.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,14 @@ def fetchall(self) -> list[tuple[Any, ...]]:
309309
return result
310310

311311
def close(self) -> None:
312-
"""Close the cursor."""
312+
"""Close the cursor.
313+
314+
Idempotent: a second call is a no-op. PEP 249 mandates that
315+
operations on a closed cursor raise an Error, but the close
316+
itself is permitted to be repeated.
317+
"""
318+
if self._closed:
319+
return
313320
self._connection._check_thread()
314321
self._closed = True
315322
self._rows = []

tests/test_async_cursor.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ async def test_close_marks_cursor_closed(self) -> None:
4141
await cursor.close()
4242
assert cursor._closed
4343

44+
@pytest.mark.asyncio
45+
async def test_close_is_idempotent(self) -> None:
46+
conn = AsyncConnection("localhost:9001")
47+
cursor = AsyncCursor(conn)
48+
await cursor.close()
49+
await cursor.close() # must not raise
50+
assert cursor._closed
51+
4452
@pytest.mark.asyncio
4553
async def test_fetchone_on_closed_cursor_raises(self) -> None:
4654
conn = AsyncConnection("localhost:9001")

tests/test_cursor.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ def test_close_marks_cursor_closed(self) -> None:
3535
cursor.close()
3636
assert cursor._closed
3737

38+
def test_close_is_idempotent(self) -> None:
39+
conn = Connection("localhost:9001")
40+
cursor = Cursor(conn)
41+
cursor.close()
42+
cursor.close() # must not raise
43+
assert cursor._closed
44+
3845
def test_fetchone_on_closed_cursor_raises(self) -> None:
3946
conn = Connection("localhost:9001")
4047
cursor = Cursor(conn)

0 commit comments

Comments
 (0)