Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions Lib/test/test_itertools.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,25 @@ def test_accumulate(self):
with self.assertRaises(TypeError):
list(accumulate([10, 20], 100))

def test_accumulate_reenter(self):
# A source iterable (or binop) that calls back into next() on the
# same accumulate object must not be allowed to silently corrupt
# the running total.
class I:
count = 0
def __iter__(self):
return self
def __next__(self):
self.count += 1
if self.count == 2:
return next(it)
return self.count

it = accumulate(I())
self.assertEqual(next(it), 1)
with self.assertRaisesRegex(RuntimeError, "accumulate"):
next(it)

def test_batched(self):
self.assertEqual(list(batched('ABCDEFG', 3)),
[('A', 'B', 'C'), ('D', 'E', 'F'), ('G',)])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Fix :func:`itertools.accumulate` silently corrupting its running total
when the source iterable (or the ``func`` callable) re-enters the same
:func:`!accumulate` iterator via a nested call to :func:`!next`. It now
raises :exc:`RuntimeError` on re-entry instead of producing wrong,
silently-dropped values, matching the existing behavior of
:func:`itertools.tee`.
14 changes: 13 additions & 1 deletion Modules/itertoolsmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -3079,6 +3079,7 @@ typedef struct {
PyObject *binop;
PyObject *initial;
itertools_state *state;
int running;
} accumulateobject;

#define accumulateobject_CAST(op) ((accumulateobject *)(op))
Expand Down Expand Up @@ -3120,6 +3121,7 @@ itertools_accumulate_impl(PyTypeObject *type, PyObject *iterable,
lz->it = it;
lz->initial = Py_XNewRef(initial);
lz->state = find_state_by_type(type);
lz->running = 0;
return (PyObject *)lz;
}

Expand Down Expand Up @@ -3160,12 +3162,21 @@ accumulate_next_lock_held(PyObject *op)
lz->initial = Py_NewRef(Py_None);
return Py_NewRef(lz->total);
}
if (lz->running) {
PyErr_SetString(PyExc_RuntimeError,
"cannot re-enter the accumulate iterator");
return NULL;
}
lz->running = 1;
val = (*Py_TYPE(lz->it)->tp_iternext)(lz->it);
if (val == NULL)
if (val == NULL) {
lz->running = 0;
return NULL;
}

if (lz->total == NULL) {
lz->total = Py_NewRef(val);
lz->running = 0;
return lz->total;
}

Expand All @@ -3174,6 +3185,7 @@ accumulate_next_lock_held(PyObject *op)
else
newtotal = PyObject_CallFunctionObjArgs(lz->binop, lz->total, val, NULL);
Py_DECREF(val);
lz->running = 0;
if (newtotal == NULL)
return NULL;

Expand Down
Loading