Skip to content

Commit 7bc99dd

Browse files
authored
[3.13] Tee of tee was not producing n independent iterators (gh-123884) (gh-125081)
1 parent f5fea4d commit 7bc99dd

File tree

4 files changed

+92
-82
lines changed

4 files changed

+92
-82
lines changed

Doc/library/itertools.rst

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -691,25 +691,36 @@ loops that truncate the stream.
691691

692692
def tee(iterable, n=2):
693693
if n < 0:
694-
raise ValueError('n must be >= 0')
695-
iterator = iter(iterable)
696-
shared_link = [None, None]
697-
return tuple(_tee(iterator, shared_link) for _ in range(n))
698-
699-
def _tee(iterator, link):
700-
try:
701-
while True:
702-
if link[1] is None:
703-
link[0] = next(iterator)
704-
link[1] = [None, None]
705-
value, link = link
706-
yield value
707-
except StopIteration:
708-
return
709-
710-
Once a :func:`tee` has been created, the original *iterable* should not be
711-
used anywhere else; otherwise, the *iterable* could get advanced without
712-
the tee objects being informed.
694+
raise ValueError
695+
if n == 0:
696+
return ()
697+
iterator = _tee(iterable)
698+
result = [iterator]
699+
for _ in range(n - 1):
700+
result.append(_tee(iterator))
701+
return tuple(result)
702+
703+
class _tee:
704+
705+
def __init__(self, iterable):
706+
it = iter(iterable)
707+
if isinstance(it, _tee):
708+
self.iterator = it.iterator
709+
self.link = it.link
710+
else:
711+
self.iterator = it
712+
self.link = [None, None]
713+
714+
def __iter__(self):
715+
return self
716+
717+
def __next__(self):
718+
link = self.link
719+
if link[1] is None:
720+
link[0] = next(self.iterator)
721+
link[1] = [None, None]
722+
value, self.link = link
723+
return value
713724

714725
When the input *iterable* is already a tee iterator object, all
715726
members of the return tuple are constructed as if they had been

Lib/test/test_itertools.py

Lines changed: 48 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1642,10 +1642,11 @@ def test_tee(self):
16421642
self.assertEqual(len(result), n)
16431643
self.assertEqual([list(x) for x in result], [list('abc')]*n)
16441644

1645-
# tee pass-through to copyable iterator
1645+
# tee objects are independent (see bug gh-123884)
16461646
a, b = tee('abc')
16471647
c, d = tee(a)
1648-
self.assertTrue(a is c)
1648+
e, f = tee(c)
1649+
self.assertTrue(len({a, b, c, d, e, f}) == 6)
16491650

16501651
# test tee_new
16511652
t1, t2 = tee('abc')
@@ -2072,21 +2073,36 @@ def test_tee_recipe(self):
20722073

20732074
def tee(iterable, n=2):
20742075
if n < 0:
2075-
raise ValueError('n must be >= 0')
2076-
iterator = iter(iterable)
2077-
shared_link = [None, None]
2078-
return tuple(_tee(iterator, shared_link) for _ in range(n))
2076+
raise ValueError
2077+
if n == 0:
2078+
return ()
2079+
iterator = _tee(iterable)
2080+
result = [iterator]
2081+
for _ in range(n - 1):
2082+
result.append(_tee(iterator))
2083+
return tuple(result)
2084+
2085+
class _tee:
2086+
2087+
def __init__(self, iterable):
2088+
it = iter(iterable)
2089+
if isinstance(it, _tee):
2090+
self.iterator = it.iterator
2091+
self.link = it.link
2092+
else:
2093+
self.iterator = it
2094+
self.link = [None, None]
20792095

2080-
def _tee(iterator, link):
2081-
try:
2082-
while True:
2083-
if link[1] is None:
2084-
link[0] = next(iterator)
2085-
link[1] = [None, None]
2086-
value, link = link
2087-
yield value
2088-
except StopIteration:
2089-
return
2096+
def __iter__(self):
2097+
return self
2098+
2099+
def __next__(self):
2100+
link = self.link
2101+
if link[1] is None:
2102+
link[0] = next(self.iterator)
2103+
link[1] = [None, None]
2104+
value, self.link = link
2105+
return value
20902106

20912107
# End tee() recipe #############################################
20922108

@@ -2132,12 +2148,10 @@ def _tee(iterator, link):
21322148
self.assertRaises(TypeError, tee, [1,2], 'x')
21332149
self.assertRaises(TypeError, tee, [1,2], 3, 'x')
21342150

2135-
# Tests not applicable to the tee() recipe
2136-
if False:
2137-
# tee object should be instantiable
2138-
a, b = tee('abc')
2139-
c = type(a)('def')
2140-
self.assertEqual(list(c), list('def'))
2151+
# tee object should be instantiable
2152+
a, b = tee('abc')
2153+
c = type(a)('def')
2154+
self.assertEqual(list(c), list('def'))
21412155

21422156
# test long-lagged and multi-way split
21432157
a, b, c = tee(range(2000), 3)
@@ -2158,21 +2172,19 @@ def _tee(iterator, link):
21582172
self.assertEqual(len(result), n)
21592173
self.assertEqual([list(x) for x in result], [list('abc')]*n)
21602174

2175+
# tee objects are independent (see bug gh-123884)
2176+
a, b = tee('abc')
2177+
c, d = tee(a)
2178+
e, f = tee(c)
2179+
self.assertTrue(len({a, b, c, d, e, f}) == 6)
21612180

2162-
# Tests not applicable to the tee() recipe
2163-
if False:
2164-
# tee pass-through to copyable iterator
2165-
a, b = tee('abc')
2166-
c, d = tee(a)
2167-
self.assertTrue(a is c)
2168-
2169-
# test tee_new
2170-
t1, t2 = tee('abc')
2171-
tnew = type(t1)
2172-
self.assertRaises(TypeError, tnew)
2173-
self.assertRaises(TypeError, tnew, 10)
2174-
t3 = tnew(t1)
2175-
self.assertTrue(list(t1) == list(t2) == list(t3) == list('abc'))
2181+
# test tee_new
2182+
t1, t2 = tee('abc')
2183+
tnew = type(t1)
2184+
self.assertRaises(TypeError, tnew)
2185+
self.assertRaises(TypeError, tnew, 10)
2186+
t3 = tnew(t1)
2187+
self.assertTrue(list(t1) == list(t2) == list(t3) == list('abc'))
21762188

21772189
# test that tee objects are weak referencable
21782190
a, b = tee(range(10))
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fixed bug in itertools.tee() handling of other tee inputs (a tee in a tee).
2+
The output now has the promised *n* independent new iterators. Formerly,
3+
the first iterator was identical (not independent) to the input iterator.
4+
This would sometimes give surprising results.

Modules/itertoolsmodule.c

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,7 +1173,7 @@ itertools_tee_impl(PyObject *module, PyObject *iterable, Py_ssize_t n)
11731173
/*[clinic end generated code: output=1c64519cd859c2f0 input=c99a1472c425d66d]*/
11741174
{
11751175
Py_ssize_t i;
1176-
PyObject *it, *copyable, *copyfunc, *result;
1176+
PyObject *it, *to, *result;
11771177

11781178
if (n < 0) {
11791179
PyErr_SetString(PyExc_ValueError, "n must be >= 0");
@@ -1190,41 +1190,24 @@ itertools_tee_impl(PyObject *module, PyObject *iterable, Py_ssize_t n)
11901190
return NULL;
11911191
}
11921192

1193-
if (PyObject_GetOptionalAttr(it, &_Py_ID(__copy__), &copyfunc) < 0) {
1194-
Py_DECREF(it);
1193+
(void)&_Py_ID(__copy__); // Retain a reference to __copy__
1194+
itertools_state *state = get_module_state(module);
1195+
to = tee_fromiterable(state, it);
1196+
Py_DECREF(it);
1197+
if (to == NULL) {
11951198
Py_DECREF(result);
11961199
return NULL;
11971200
}
1198-
if (copyfunc != NULL) {
1199-
copyable = it;
1200-
}
1201-
else {
1202-
itertools_state *state = get_module_state(module);
1203-
copyable = tee_fromiterable(state, it);
1204-
Py_DECREF(it);
1205-
if (copyable == NULL) {
1206-
Py_DECREF(result);
1207-
return NULL;
1208-
}
1209-
copyfunc = PyObject_GetAttr(copyable, &_Py_ID(__copy__));
1210-
if (copyfunc == NULL) {
1211-
Py_DECREF(copyable);
1212-
Py_DECREF(result);
1213-
return NULL;
1214-
}
1215-
}
12161201

1217-
PyTuple_SET_ITEM(result, 0, copyable);
1202+
PyTuple_SET_ITEM(result, 0, to);
12181203
for (i = 1; i < n; i++) {
1219-
copyable = _PyObject_CallNoArgs(copyfunc);
1220-
if (copyable == NULL) {
1221-
Py_DECREF(copyfunc);
1204+
to = tee_copy((teeobject *)to, NULL);
1205+
if (to == NULL) {
12221206
Py_DECREF(result);
12231207
return NULL;
12241208
}
1225-
PyTuple_SET_ITEM(result, i, copyable);
1209+
PyTuple_SET_ITEM(result, i, to);
12261210
}
1227-
Py_DECREF(copyfunc);
12281211
return result;
12291212
}
12301213

0 commit comments

Comments
 (0)