Skip to content

Commit 543c4e5

Browse files
authored
PYTHON-1357 - Refactor Cursor and CommandCursor (#2691)
1 parent 182d8e2 commit 543c4e5

16 files changed

Lines changed: 380 additions & 587 deletions

doc/api/pymongo/asynchronous/command_cursor.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
.. automodule:: pymongo.asynchronous.command_cursor
66
:synopsis: Tools for iterating over MongoDB command results
77
:members:
8+
:inherited-members:

doc/api/pymongo/asynchronous/cursor.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
.. autoclass:: pymongo.asynchronous.cursor.AsyncCursor(collection, filter=None, projection=None, skip=0, limit=0, no_cursor_timeout=False, cursor_type=CursorType.NON_TAILABLE, sort=None, allow_partial_results=False, oplog_replay=False, batch_size=0, collation=None, hint=None, max_scan=None, max_time_ms=None, max=None, min=None, return_key=False, show_record_id=False, snapshot=False, comment=None, session=None, allow_disk_use=None)
99
:members:
10+
:inherited-members:
11+
1012

1113
.. describe:: c[index]
1214

doc/api/pymongo/command_cursor.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
.. automodule:: pymongo.command_cursor
55
:synopsis: Tools for iterating over MongoDB command results
66
:members:
7+
:inherited-members:

doc/api/pymongo/cursor.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
.. autoclass:: pymongo.cursor.Cursor(collection, filter=None, projection=None, skip=0, limit=0, no_cursor_timeout=False, cursor_type=CursorType.NON_TAILABLE, sort=None, allow_partial_results=False, oplog_replay=False, batch_size=0, collation=None, hint=None, max_scan=None, max_time_ms=None, max=None, min=None, return_key=False, show_record_id=False, snapshot=False, comment=None, session=None, allow_disk_use=None)
1919
:members:
20+
:inherited-members:
2021

2122
.. describe:: c[index]
2223

pymongo/asynchronous/client_session.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@
157157
from bson.int64 import Int64
158158
from bson.timestamp import Timestamp
159159
from pymongo import _csot
160-
from pymongo.asynchronous.cursor import _ConnectionManager
160+
from pymongo.asynchronous.cursor_base import _ConnectionManager
161161
from pymongo.errors import (
162162
ConfigurationError,
163163
ConnectionFailure,

pymongo/asynchronous/command_cursor.py

Lines changed: 8 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
TYPE_CHECKING,
2121
Any,
2222
AsyncIterator,
23-
Generic,
2423
Mapping,
2524
NoReturn,
2625
Optional,
@@ -29,17 +28,10 @@
2928
)
3029

3130
from bson import CodecOptions, _convert_raw_document_lists_to_streams
32-
from pymongo import _csot
33-
from pymongo.asynchronous.cursor import _ConnectionManager
31+
from pymongo.asynchronous.cursor_base import _AsyncCursorBase, _ConnectionManager
3432
from pymongo.cursor_shared import _CURSOR_CLOSED_ERRORS
3533
from pymongo.errors import ConnectionFailure, InvalidOperation, OperationFailure
36-
from pymongo.message import (
37-
_CursorAddress,
38-
_GetMore,
39-
_OpMsg,
40-
_OpReply,
41-
_RawBatchGetMore,
42-
)
34+
from pymongo.message import _GetMore, _OpMsg, _OpReply, _RawBatchGetMore
4335
from pymongo.response import PinnedResponse
4436
from pymongo.typings import _Address, _DocumentOut, _DocumentType
4537

@@ -51,7 +43,7 @@
5143
_IS_SYNC = False
5244

5345

54-
class AsyncCommandCursor(Generic[_DocumentType]):
46+
class AsyncCommandCursor(_AsyncCursorBase[_DocumentType]):
5547
"""An asynchronous cursor / iterator over command cursors."""
5648

5749
_getmore_class = _GetMore
@@ -98,8 +90,8 @@ def __init__(
9890
f"max_await_time_ms must be an integer or None, not {type(max_await_time_ms)}"
9991
)
10092

101-
def __del__(self) -> None:
102-
self._die_no_lock()
93+
def _get_namespace(self) -> str:
94+
return self._ns
10395

10496
def batch_size(self, batch_size: int) -> AsyncCommandCursor[_DocumentType]:
10597
"""Limits the number of documents returned in one batch. Each batch
@@ -161,94 +153,12 @@ def _unpack_response(
161153
) -> Sequence[_DocumentOut]:
162154
return response.unpack_response(cursor_id, codec_options, user_fields, legacy_response)
163155

164-
@property
165-
def alive(self) -> bool:
166-
"""Does this cursor have the potential to return more data?
167-
168-
Even if :attr:`alive` is ``True``, :meth:`next` can raise
169-
:exc:`StopIteration`. Best to use a for loop::
170-
171-
async for doc in collection.aggregate(pipeline):
172-
print(doc)
173-
174-
.. note:: :attr:`alive` can be True while iterating a cursor from
175-
a failed server. In this case :attr:`alive` will return False after
176-
:meth:`next` fails to retrieve the next batch of results from the
177-
server.
178-
"""
179-
return bool(len(self._data) or (not self._killed))
180-
181-
@property
182-
def cursor_id(self) -> int:
183-
"""Returns the id of the cursor."""
184-
return self._id
185-
186-
@property
187-
def address(self) -> Optional[_Address]:
188-
"""The (host, port) of the server used, or None.
189-
190-
.. versionadded:: 3.0
191-
"""
192-
return self._address
193-
194-
@property
195-
def session(self) -> Optional[AsyncClientSession]:
196-
"""The cursor's :class:`~pymongo.asynchronous.client_session.AsyncClientSession`, or None.
197-
198-
.. versionadded:: 3.6
199-
"""
200-
if self._session and not self._session._implicit:
201-
return self._session
202-
return None
203-
204-
def _prepare_to_die(self) -> tuple[int, Optional[_CursorAddress]]:
205-
already_killed = self._killed
206-
self._killed = True
207-
if self._id and not already_killed:
208-
cursor_id = self._id
209-
assert self._address is not None
210-
address = _CursorAddress(self._address, self._ns)
211-
else:
212-
# Skip killCursors.
213-
cursor_id = 0
214-
address = None
215-
return cursor_id, address
216-
217-
def _die_no_lock(self) -> None:
218-
"""Closes this cursor without acquiring a lock."""
219-
cursor_id, address = self._prepare_to_die()
220-
self._collection.database.client._cleanup_cursor_no_lock(
221-
cursor_id, address, self._sock_mgr, self._session
222-
)
223-
if self._session and self._session._implicit:
224-
self._session._attached_to_cursor = False
225-
self._session = None
226-
self._sock_mgr = None
227-
228-
async def _die_lock(self) -> None:
229-
"""Closes this cursor."""
230-
cursor_id, address = self._prepare_to_die()
231-
await self._collection.database.client._cleanup_cursor_lock(
232-
cursor_id,
233-
address,
234-
self._sock_mgr,
235-
self._session,
236-
)
237-
if self._session and self._session._implicit:
238-
self._session._attached_to_cursor = False
239-
self._session = None
240-
self._sock_mgr = None
241-
242156
def _end_session(self) -> None:
243157
if self._session and self._session._implicit:
244158
self._session._attached_to_cursor = False
245159
self._session._end_implicit_session()
246160
self._session = None
247161

248-
async def close(self) -> None:
249-
"""Explicitly close / kill this cursor."""
250-
await self._die_lock()
251-
252162
async def _send_message(self, operation: _GetMore) -> None:
253163
"""Send a getmore message and handle the response."""
254164
client = self._collection.database.client
@@ -330,6 +240,9 @@ async def _refresh(self) -> int:
330240
def __aiter__(self) -> AsyncIterator[_DocumentType]:
331241
return self
332242

243+
async def __aenter__(self) -> AsyncCommandCursor[_DocumentType]:
244+
return self
245+
333246
async def next(self) -> _DocumentType:
334247
"""Advance the cursor."""
335248
# Block until a document is returnable.
@@ -385,41 +298,6 @@ async def try_next(self) -> Optional[_DocumentType]:
385298
"""
386299
return await self._try_next(get_more_allowed=True)
387300

388-
async def __aenter__(self) -> AsyncCommandCursor[_DocumentType]:
389-
return self
390-
391-
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
392-
await self.close()
393-
394-
@_csot.apply
395-
async def to_list(self, length: Optional[int] = None) -> list[_DocumentType]:
396-
"""Converts the contents of this cursor to a list more efficiently than ``[doc async for doc in cursor]``.
397-
398-
To use::
399-
400-
>>> await cursor.to_list()
401-
402-
Or, so read at most n items from the cursor::
403-
404-
>>> await cursor.to_list(n)
405-
406-
If the cursor is empty or has no more results, an empty list will be returned.
407-
408-
.. versionadded:: 4.9
409-
"""
410-
res: list[_DocumentType] = []
411-
remaining = length
412-
if isinstance(length, int) and length < 1:
413-
raise ValueError("to_list() length must be greater than 0")
414-
while self.alive:
415-
if not await self._next_batch(res, remaining):
416-
break
417-
if length is not None:
418-
remaining = length - len(res)
419-
if remaining == 0:
420-
break
421-
return res
422-
423301

424302
class AsyncRawBatchCommandCursor(AsyncCommandCursor[_DocumentType]):
425303
_getmore_class = _RawBatchGetMore

0 commit comments

Comments
 (0)