Skip to content

Commit 2fb6921

Browse files
[2.7] bpo-34410: Fix a crash in the tee iterator when re-enter it. (GH-15625) (GH-15740)
RuntimeError is now raised in this case. (cherry picked from commit 526a014)
1 parent 0229b56 commit 2fb6921

File tree

4 files changed

+56
-0
lines changed

4 files changed

+56
-0
lines changed

Doc/library/itertools.rst

+4
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,10 @@ loops that truncate the stream.
659659
used anywhere else; otherwise, the *iterable* could get advanced without
660660
the tee objects being informed.
661661

662+
``tee`` iterators are not threadsafe. A :exc:`RuntimeError` may be
663+
raised when using simultaneously iterators returned by the same :func:`tee`
664+
call, even if the original *iterable* is threadsafe.
665+
662666
This itertool may require significant auxiliary storage (depending on how
663667
much temporary data needs to be stored). In general, if one iterator uses
664668
most or all of the data before another iterator starts, it is faster to use

Lib/test/test_itertools.py

+41
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
import copy
1111
import pickle
1212
from functools import reduce
13+
try:
14+
import threading
15+
except ImportError:
16+
threading = None
1317
maxsize = test_support.MAX_Py_ssize_t
1418
minsize = -maxsize-1
1519

@@ -984,6 +988,43 @@ def test_tee_del_backward(self):
984988
del forward, backward
985989
raise
986990

991+
def test_tee_reenter(self):
992+
class I:
993+
first = True
994+
def __iter__(self):
995+
return self
996+
def next(self):
997+
first = self.first
998+
self.first = False
999+
if first:
1000+
return next(b)
1001+
1002+
a, b = tee(I())
1003+
with self.assertRaisesRegexp(RuntimeError, "tee"):
1004+
next(a)
1005+
1006+
@unittest.skipUnless(threading, 'Threading required for this test.')
1007+
def test_tee_concurrent(self):
1008+
start = threading.Event()
1009+
finish = threading.Event()
1010+
class I:
1011+
def __iter__(self):
1012+
return self
1013+
def next(self):
1014+
start.set()
1015+
finish.wait()
1016+
1017+
a, b = tee(I())
1018+
thread = threading.Thread(target=next, args=[a])
1019+
thread.start()
1020+
try:
1021+
start.wait()
1022+
with self.assertRaisesRegexp(RuntimeError, "tee"):
1023+
next(b)
1024+
finally:
1025+
finish.set()
1026+
thread.join()
1027+
9871028
def test_StopIteration(self):
9881029
self.assertRaises(StopIteration, izip().next)
9891030

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fixed a crash in the :func:`tee` iterator when re-enter it. RuntimeError is
2+
now raised in this case.

Modules/itertoolsmodule.c

+9
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ typedef struct {
314314
PyObject_HEAD
315315
PyObject *it;
316316
int numread;
317+
int running;
317318
PyObject *nextlink;
318319
PyObject *(values[LINKCELLS]);
319320
} teedataobject;
@@ -336,6 +337,7 @@ teedataobject_new(PyObject *it)
336337
if (tdo == NULL)
337338
return NULL;
338339

340+
tdo->running = 0;
339341
tdo->numread = 0;
340342
tdo->nextlink = NULL;
341343
Py_INCREF(it);
@@ -364,7 +366,14 @@ teedataobject_getitem(teedataobject *tdo, int i)
364366
else {
365367
/* this is the lead iterator, so fetch more data */
366368
assert(i == tdo->numread);
369+
if (tdo->running) {
370+
PyErr_SetString(PyExc_RuntimeError,
371+
"cannot re-enter the tee iterator");
372+
return NULL;
373+
}
374+
tdo->running = 1;
367375
value = PyIter_Next(tdo->it);
376+
tdo->running = 0;
368377
if (value == NULL)
369378
return NULL;
370379
tdo->numread++;

0 commit comments

Comments
 (0)