diff --git a/Lib/test/list_tests.py b/Lib/test/list_tests.py index e76f79c274e744..79c5bdcfffecc0 100644 --- a/Lib/test/list_tests.py +++ b/Lib/test/list_tests.py @@ -202,6 +202,88 @@ def test_slice_assign_iterator(self): x[:] = reversed(range(3)) self.assertEqual(x, self.type2test([2, 1, 0])) + def test_extended_slice_assign_infinite_iterator(self): + # gh-146268: assigning an infinite iterator to an extended slice + # (step != 1) should raise ValueError, not hang indefinitely. + def infinite_gen(): + while True: + yield "foo" + + a = self.type2test(range(4)) + with self.assertRaises(ValueError) as cm: + a[::2] = infinite_gen() + self.assertIn("yielded more items than extended slice of size 2", + str(cm.exception)) + # list should be unchanged after the failed assignment + self.assertEqual(a, self.type2test(range(4))) + + a = self.type2test(range(10)) + with self.assertRaises(ValueError): + a[::3] = infinite_gen() + self.assertEqual(a, self.type2test(range(10))) + + # Negative step with infinite iterator + a = self.type2test(range(6)) + with self.assertRaises(ValueError): + a[::-2] = infinite_gen() + self.assertEqual(a, self.type2test(range(6))) + + # Explicit start and stop with infinite iterator + a = self.type2test(range(10)) + with self.assertRaises(ValueError): + a[1:8:2] = infinite_gen() + self.assertEqual(a, self.type2test(range(10))) + + # Explicit start only + a = self.type2test(range(10)) + with self.assertRaises(ValueError): + a[2::3] = infinite_gen() + self.assertEqual(a, self.type2test(range(10))) + + # Explicit stop only + a = self.type2test(range(10)) + with self.assertRaises(ValueError): + a[:7:2] = infinite_gen() + self.assertEqual(a, self.type2test(range(10))) + + # Negative step with explicit start and stop + a = self.type2test(range(10)) + with self.assertRaises(ValueError): + a[8:1:-2] = infinite_gen() + self.assertEqual(a, self.type2test(range(10))) + + def test_extended_slice_assign_iterator(self): + # Assigning a finite iterator with the correct length to an + # extended slice should work. + a = self.type2test(range(10)) + a[::2] = iter(range(5)) + self.assertEqual(a, self.type2test([0, 1, 1, 3, 2, 5, 3, 7, 4, 9])) + + # Too few items from an iterator + a = self.type2test(range(10)) + with self.assertRaises(ValueError) as cm: + a[::2] = iter(range(3)) + self.assertIn("sequence of size 3 to extended slice of size 5", + str(cm.exception)) + self.assertEqual(a, self.type2test(range(10))) + + # Too many items from an iterator + a = self.type2test(range(10)) + with self.assertRaises(ValueError) as cm: + a[::2] = iter(range(10)) + self.assertIn("yielded more items than extended slice of size 5", + str(cm.exception)) + self.assertEqual(a, self.type2test(range(10))) + + def test_extended_slice_assign_non_iterable(self): + # Assigning a non-iterable to an extended slice should raise TypeError. + a = self.type2test(range(4)) + with self.assertRaises(TypeError) as cm: + a[::2] = 42 + self.assertIn("must assign iterable to extended slice", + str(cm.exception)) + self.assertEqual(a, self.type2test(range(4))) + def test_delslice(self): a = self.type2test([0, 1]) del a[1:2] diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-03-22-07-24-41.gh-issue-146268.-f860z.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-22-07-24-41.gh-issue-146268.-f860z.rst new file mode 100644 index 00000000000000..6fe9a3db153672 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-22-07-24-41.gh-issue-146268.-f860z.rst @@ -0,0 +1,3 @@ +Fixed extended slice assignment (e.g. ``l[::2] = x``) hanging when the +right-hand side is an infinite iterator. A bounded iteration is now used so +that a :exc:`ValueError` is raised promptly. Patch by Charles Machalow. diff --git a/Objects/listobject.c b/Objects/listobject.c index 654b8130e70840..2c2e99bac2e696 100644 --- a/Objects/listobject.c +++ b/Objects/listobject.c @@ -3821,12 +3821,72 @@ list_ass_subscript_lock_held(PyObject *_self, PyObject *item, PyObject *value) PyObject **garbage, **seqitems, **selfitems; Py_ssize_t i; size_t cur; + bool bounded_iter = false; /* protect against a[::-1] = a */ if (self == (PyListObject*)value) { seq = list_slice_lock_held((PyListObject *)value, 0, Py_SIZE(value)); } + else if (step != 1 && + !PyList_CheckExact(value) && + !PyTuple_CheckExact(value)) + { + /* For extended slices (step != 1) with arbitrary iterables, + use bounded iteration to avoid hanging on infinite + iterators (gh-146268). We compute a preliminary slice + length to cap the number of items we collect. The real + slice length is recomputed afterwards because the + iterable's __next__ may mutate the list. */ + Py_ssize_t tmp_start = start, tmp_stop = stop; + Py_ssize_t slicelength_bound = adjust_slice_indexes( + self, &tmp_start, &tmp_stop, step); + + PyObject *it = PyObject_GetIter(value); + if (it == NULL) { + if (PyErr_ExceptionMatches(PyExc_TypeError)) { + PyObject *exc = PyErr_GetRaisedException(); + PyErr_SetString(PyExc_TypeError, + "must assign iterable " + "to extended slice"); + _PyErr_ChainExceptions1(exc); + } + return -1; + } + Py_ssize_t alloc = slicelength_bound + 1; + if (alloc <= 0) { + /* Overflow or zero-length slice; still collect at + least 1 item so the size check can detect a + non-empty iterable. */ + alloc = 1; + } + seq = PyList_New(alloc); + if (seq == NULL) { + Py_DECREF(it); + return -1; + } + Py_ssize_t j; + for (j = 0; j < alloc; j++) { + PyObject *v = PyIter_Next(it); + if (v == NULL) { + if (PyErr_Occurred()) { + /* Discard unfilled slots before decref */ + Py_SET_SIZE(seq, j); + Py_DECREF(seq); + Py_DECREF(it); + return -1; + } + break; + } + PyList_SET_ITEM(seq, j, v); + } + Py_DECREF(it); + /* Shrink to the number of items actually collected */ + if (j < alloc) { + Py_SET_SIZE(seq, j); + } + bounded_iter = true; + } else { seq = PySequence_Fast(value, "must assign iterable " @@ -3845,12 +3905,22 @@ list_ass_subscript_lock_held(PyObject *_self, PyObject *item, PyObject *value) } if (PySequence_Fast_GET_SIZE(seq) != slicelength) { - PyErr_Format(PyExc_ValueError, - "attempt to assign sequence of " - "size %zd to extended slice of " - "size %zd", - PySequence_Fast_GET_SIZE(seq), - slicelength); + if (bounded_iter && + PySequence_Fast_GET_SIZE(seq) > slicelength) { + PyErr_Format(PyExc_ValueError, + "attempt to assign iterable that yielded " + "more items than extended slice of " + "size %zd", + slicelength); + } + else { + PyErr_Format(PyExc_ValueError, + "attempt to assign sequence of " + "size %zd to extended slice of " + "size %zd", + PySequence_Fast_GET_SIZE(seq), + slicelength); + } Py_DECREF(seq); return -1; }