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
58 changes: 58 additions & 0 deletions Lib/test/list_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,64 @@ 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)))

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]
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
80 changes: 74 additions & 6 deletions Objects/listobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -3821,12 +3821,70 @@ 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)) {
PyErr_SetString(PyExc_TypeError,
"must assign iterable "
"to extended slice");
}
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 "
Expand All @@ -3845,12 +3903,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;
}
Expand Down
Loading