Skip to content

Commit f18b364

Browse files
committed
PYTHON-894 - Set cursor.alive False after final batch.
Even with this change, "next" can raise StopIteration even though "alive" is True. For example if batch size is 2 and there are 4 documents in the result set, then after the 4th document "alive" is True but "next" raises StopIteration.
1 parent 773a8df commit f18b364

File tree

4 files changed

+50
-2
lines changed

4 files changed

+50
-2
lines changed

pymongo/command_cursor.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def __init__(self, collection, cursor_info,
4040
)
4141
self.__retrieved = retrieved
4242
self.__batch_size = 0
43-
self.__killed = False
43+
self.__killed = (self.__id == 0)
4444

4545
if "ns" in cursor_info:
4646
self.__ns = cursor_info["ns"]
@@ -123,6 +123,8 @@ def __send_message(self, msg):
123123
client.disconnect()
124124
raise
125125
self.__id = response["cursor_id"]
126+
if self.__id == 0:
127+
self.__killed = True
126128

127129
assert response["starting_from"] == self.__retrieved, (
128130
"Result batch started from %s, expected %s" % (
@@ -153,7 +155,14 @@ def _refresh(self):
153155

154156
@property
155157
def alive(self):
156-
"""Does this cursor have the potential to return more data?"""
158+
"""Does this cursor have the potential to return more data?
159+
160+
Even if :attr:`alive` is ``True``, :meth:`.next` can raise
161+
:exc:`StopIteration`. Best to use a for loop::
162+
163+
for doc in collection.aggregate(pipeline):
164+
print(doc)
165+
"""
157166
return bool(len(self.__data) or (not self.__killed))
158167

159168
@property

pymongo/cursor.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -977,6 +977,8 @@ def __send_message(self, message):
977977
raise
978978

979979
self.__id = response["cursor_id"]
980+
if self.__id == 0:
981+
self.__killed = True
980982

981983
# starting from doesn't get set on getmore's for tailable cursors
982984
if not (self.__query_flags & _QUERY_OPTIONS["tailable_cursor"]):
@@ -1050,6 +1052,11 @@ def alive(self):
10501052
since they will stop iterating even though they *may* return more
10511053
results in the future.
10521054
1055+
With regular cursors, simply use a for loop instead of :attr:`alive`::
1056+
1057+
for doc in collection.find():
1058+
print(doc)
1059+
10531060
.. versionadded:: 1.5
10541061
"""
10551062
return bool(len(self.__data) or (not self.__killed))

test/test_collection.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1430,6 +1430,23 @@ def test_aggregation_cursor(self):
14301430
expected_sum,
14311431
sum(doc['_id'] for doc in cursor))
14321432

1433+
def test_aggregation_cursor_alive(self):
1434+
if not version.at_least(self.db.connection, (2, 5, 1)):
1435+
raise SkipTest("Aggregation cursor requires MongoDB >= 2.5.1")
1436+
self.db.test.remove()
1437+
self.db.test.insert([{} for _ in range(3)])
1438+
self.addCleanup(self.db.test.remove)
1439+
cursor = self.db.test.aggregate(pipeline=[], cursor={'batchSize': 2})
1440+
n = 0
1441+
while True:
1442+
cursor.next()
1443+
n += 1
1444+
if 3 == n:
1445+
self.assertFalse(cursor.alive)
1446+
break
1447+
1448+
self.assertTrue(cursor.alive)
1449+
14331450
def test_parallel_scan(self):
14341451
if is_mongos(self.db.connection):
14351452
raise SkipTest("mongos does not support parallel_scan")

test/test_cursor.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1165,5 +1165,20 @@ def close(self, dummy):
11651165
client.set_cursor_manager(CursorManager)
11661166
ctx.exit()
11671167

1168+
def test_alive(self):
1169+
self.db.test.remove()
1170+
self.db.test.insert([{} for _ in range(3)])
1171+
self.addCleanup(self.db.test.remove)
1172+
cursor = self.db.test.find().batch_size(2)
1173+
n = 0
1174+
while True:
1175+
cursor.next()
1176+
n += 1
1177+
if 3 == n:
1178+
self.assertFalse(cursor.alive)
1179+
break
1180+
1181+
self.assertTrue(cursor.alive)
1182+
11681183
if __name__ == "__main__":
11691184
unittest.main()

0 commit comments

Comments
 (0)