]> granicus.if.org Git - python/commitdiff
bpo-29942: Fix the use of recursion in itertools.chain.from_iterable. (#913)
authorT. Wouters <thomas@python.org>
Thu, 30 Mar 2017 19:49:22 +0000 (12:49 -0700)
committerGitHub <noreply@github.com>
Thu, 30 Mar 2017 19:49:22 +0000 (12:49 -0700)
Fix the use of recursion in itertools.chain.from_iterable. Using recursion
is unnecessary, and can easily cause stack overflows, especially when
building in low optimization modes or with Py_DEBUG enabled.
(cherry picked from commit 5466d4af5fe76ec0a5fbc8a05675287d9e8e9d14)

Lib/test/test_itertools.py
Misc/NEWS
Modules/itertoolsmodule.c

index 567210425756ee44be8f4d8028dfa1bb5c3260f2..7b2362d29e9bcfa672d27554b0488b79a8fe6436 100644 (file)
@@ -1465,6 +1465,14 @@ class RegressionTests(unittest.TestCase):
         self.assertRaises(AssertionError, list, cycle(gen1()))
         self.assertEqual(hist, [0,1])
 
+    def test_long_chain_of_empty_iterables(self):
+        # Make sure itertools.chain doesn't run into recursion limits when
+        # dealing with long chains of empty iterables. Even with a high
+        # number this would probably only fail in Py_DEBUG mode.
+        it = chain.from_iterable(() for unused in xrange(10000000))
+        with self.assertRaises(StopIteration):
+            next(it)
+
 class SubclassWithKwargsTest(unittest.TestCase):
     def test_keywords_in_subclass(self):
         # count is not subclassable...
index 0ca08d6befbfbb709c33cee920761bb181506884..559a8088a7e36c1cea1f0228047319e86bde8b41 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -42,6 +42,9 @@ Extension Modules
 Library
 -------
 
+- bpo-29942: Fix a crash in itertools.chain.from_iterable when encountering
+  long runs of empty iterables.
+
 - bpo-29861: Release references to tasks, their arguments and their results
   as soon as they are finished in multiprocessing.Pool.
 
index f91501bb24787d49dceb9921d0c00942fa5c05e0..cf3aadfd3e07128e2afecc9cdbe1b10e9752f2d0 100644 (file)
@@ -1708,33 +1708,37 @@ chain_next(chainobject *lz)
 {
     PyObject *item;
 
-    if (lz->source == NULL)
-        return NULL;                                    /* already stopped */
-
-    if (lz->active == NULL) {
-        PyObject *iterable = PyIter_Next(lz->source);
-        if (iterable == NULL) {
-            Py_CLEAR(lz->source);
-            return NULL;                                /* no more input sources */
-        }
-        lz->active = PyObject_GetIter(iterable);
-        Py_DECREF(iterable);
+    /* lz->source is the iterator of iterables. If it's NULL, we've already
+     * consumed them all. lz->active is the current iterator. If it's NULL,
+     * we should grab a new one from lz->source. */
+    while (lz->source != NULL) {
         if (lz->active == NULL) {
-            Py_CLEAR(lz->source);
-            return NULL;                                /* input not iterable */
+            PyObject *iterable = PyIter_Next(lz->source);
+            if (iterable == NULL) {
+                Py_CLEAR(lz->source);
+                return NULL;                  /* no more input sources */
+            }
+            lz->active = PyObject_GetIter(iterable);
+            Py_DECREF(iterable);
+            if (lz->active == NULL) {
+                Py_CLEAR(lz->source);
+                return NULL;                  /* input not iterable */
+            }
         }
+        item = PyIter_Next(lz->active);
+        if (item != NULL)
+            return item;
+        if (PyErr_Occurred()) {
+            if (PyErr_ExceptionMatches(PyExc_StopIteration))
+                PyErr_Clear();
+            else
+                return NULL;                  /* input raised an exception */
+        }
+        /* lz->active is consumed, try with the next iterable. */
+        Py_CLEAR(lz->active);
     }
-    item = PyIter_Next(lz->active);
-    if (item != NULL)
-        return item;
-    if (PyErr_Occurred()) {
-        if (PyErr_ExceptionMatches(PyExc_StopIteration))
-            PyErr_Clear();
-        else
-            return NULL;                                /* input raised an exception */
-    }
-    Py_CLEAR(lz->active);
-    return chain_next(lz);                      /* recurse and use next active */
+    /* Everything had been consumed already. */
+    return NULL;
 }
 
 PyDoc_STRVAR(chain_doc,