Skip to content

Commit 47a52f6

Browse files
authored
Improve first iteration logic (#1101)
Improving the first iteration logic in `PageIterator` and `ResultIterator`. Both iterators had a `_first_iteration` state attribute that could be replaced with better logic: - In `PageIterator`, it can be replaced with `_is_last_page` which - is exception-safe (only mutate after we do I/O), and - (IMO) more directly relates to the state we're handling - In `ResultsIterator`, we can do without `_first_iteration` (but we should initialize `_index` and `_count`) Inspired by #1059.
1 parent 4421acb commit 47a52f6

File tree

2 files changed

+34
-11
lines changed

2 files changed

+34
-11
lines changed

pynamodb/pagination.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import time
2-
from typing import Any, Callable, Dict, Iterable, Iterator, TypeVar, Optional
2+
from typing import Any, Callable, Dict, Iterable, Iterator, Optional, TypeVar
33

44
from pynamodb.constants import (CAMEL_COUNT, ITEMS, LAST_EVALUATED_KEY, SCANNED_COUNT,
55
CONSUMED_CAPACITY, TOTAL, CAPACITY_UNITS)
@@ -90,8 +90,8 @@ def __init__(
9090
self._operation = operation
9191
self._args = args
9292
self._kwargs = kwargs
93-
self._first_iteration = True
9493
self._last_evaluated_key = kwargs.get('exclusive_start_key')
94+
self._is_last_page = False
9595
self._total_scanned_count = 0
9696
self._rate_limiter = None
9797
if rate_limit:
@@ -102,18 +102,17 @@ def __iter__(self) -> Iterator[_T]:
102102
return self
103103

104104
def __next__(self) -> _T:
105-
if self._last_evaluated_key is None and not self._first_iteration:
105+
if self._is_last_page:
106106
raise StopIteration()
107107

108-
self._first_iteration = False
109-
110108
self._kwargs['exclusive_start_key'] = self._last_evaluated_key
111109

112110
if self._rate_limiter:
113111
self._rate_limiter.acquire()
114112
self._kwargs['return_consumed_capacity'] = TOTAL
115113
page = self._operation(*self._args, settings=self._settings, **self._kwargs)
116114
self._last_evaluated_key = page.get(LAST_EVALUATED_KEY)
115+
self._is_last_page = self._last_evaluated_key is None
117116
self._total_scanned_count += page[SCANNED_COUNT]
118117

119118
if self._rate_limiter:
@@ -170,10 +169,11 @@ def __init__(
170169
settings: OperationSettings = OperationSettings.default,
171170
) -> None:
172171
self.page_iter: PageIterator = PageIterator(operation, args, kwargs, rate_limit, settings)
173-
self._first_iteration = True
174172
self._map_fn = map_fn
175173
self._limit = limit
176174
self._total_count = 0
175+
self._index = 0
176+
self._count = 0
177177

178178
def _get_next_page(self) -> None:
179179
page = next(self.page_iter)
@@ -189,10 +189,6 @@ def __next__(self) -> _T:
189189
if self._limit == 0:
190190
raise StopIteration
191191

192-
if self._first_iteration:
193-
self._first_iteration = False
194-
self._get_next_page()
195-
196192
while self._index == self._count:
197193
self._get_next_page()
198194

@@ -209,7 +205,7 @@ def next(self) -> _T:
209205

210206
@property
211207
def last_evaluated_key(self) -> Optional[Dict[str, Dict[str, Any]]]:
212-
if self._first_iteration or self._index == self._count:
208+
if self._index == self._count:
213209
# Not started iterating yet: return `exclusive_start_key` if set, otherwise expect None; or,
214210
# Entire page has been consumed: last_evaluated_key is whatever DynamoDB returned
215211
# It may correspond to the current item, or it may correspond to an item evaluated but not returned.

tests/test_model.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1347,6 +1347,33 @@ def test_query_with_exclusive_start_key(self):
13471347
self.assertEqual(results_iter.total_count, 10)
13481348
self.assertEqual(results_iter.page_iter.total_scanned_count, 10)
13491349

1350+
def test_query_with_failure(self):
1351+
items = [
1352+
{
1353+
**GET_MODEL_ITEM_DATA[ITEM],
1354+
'user_id': {
1355+
STRING: f'id-{idx}'
1356+
},
1357+
}
1358+
for idx in range(30)
1359+
]
1360+
1361+
with patch(PATCH_METHOD) as req:
1362+
req.side_effect = [
1363+
Exception('bleep-bloop'),
1364+
{'Count': 10, 'ScannedCount': 10, 'Items': items[0:10], 'LastEvaluatedKey': {'user_id': items[10]['user_id']}},
1365+
]
1366+
results_iter = UserModel.query('foo', limit=10, page_size=10)
1367+
1368+
with pytest.raises(Exception, match='bleep-bloop'):
1369+
next(results_iter)
1370+
1371+
first_item = next(results_iter)
1372+
assert first_item.user_id == 'id-0'
1373+
1374+
second_item = next(results_iter)
1375+
assert second_item.user_id == 'id-1'
1376+
13501377
def test_query(self):
13511378
"""
13521379
Model.query

0 commit comments

Comments
 (0)