From e0c0f5ac4d38ae8347a27ff92c0e62cf13b5fec1 Mon Sep 17 00:00:00 2001 From: Luan Taraschi <130802253+luantaraschi@users.noreply.github.com> Date: Thu, 20 Aug 2026 22:20:32 -0300 Subject: [PATCH] Drop what a deque with maxlen of zero is handed append and appendleft trimmed before pushing, and the test they used, maxlen == len, holds for an empty deque whose bound is zero. The pop then had nothing to remove and the item stayed: >>> d = deque(maxlen=0) >>> d.append(1) >>> list(d) [1] Both now push first and trim after, which is the order CPython uses, so a bound of zero drops what just arrived. extend, extendleft, insert, rotate, the operators and the constructor were already right. Assisted-by: Claude Code:claude-opus-5 --- crates/vm/src/stdlib/_collections.rs | 14 ++++-- .../snippets/stdlib_collections_deque.py | 43 +++++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/crates/vm/src/stdlib/_collections.rs b/crates/vm/src/stdlib/_collections.rs index 4bd4aee25b3..3783d6fff9b 100644 --- a/crates/vm/src/stdlib/_collections.rs +++ b/crates/vm/src/stdlib/_collections.rs @@ -77,6 +77,10 @@ mod _collections { fn borrow_deque_mut(&self) -> PyRwLockWriteGuard<'_, VecDeque> { self.deque.write() } + + fn is_over_maxlen(&self, deque: &VecDeque) -> bool { + self.maxlen.is_some_and(|maxlen| deque.len() > maxlen) + } } #[pyclass( @@ -96,20 +100,22 @@ mod _collections { fn append(&self, obj: PyObjectRef) { self.state.fetch_add(1); let mut deque = self.borrow_deque_mut(); - if self.maxlen == Some(deque.len()) { + deque.push_back(obj); + // Trim after pushing, so that a `maxlen` of zero drops what just + // arrived instead of popping from an empty deque and keeping it. + if self.is_over_maxlen(&deque) { deque.pop_front(); } - deque.push_back(obj); } #[pymethod] fn appendleft(&self, obj: PyObjectRef) { self.state.fetch_add(1); let mut deque = self.borrow_deque_mut(); - if self.maxlen == Some(deque.len()) { + deque.push_front(obj); + if self.is_over_maxlen(&deque) { deque.pop_back(); } - deque.push_front(obj); } #[pymethod] diff --git a/extra_tests/snippets/stdlib_collections_deque.py b/extra_tests/snippets/stdlib_collections_deque.py index d4d3f25bc4f..0c280fd9b02 100644 --- a/extra_tests/snippets/stdlib_collections_deque.py +++ b/extra_tests/snippets/stdlib_collections_deque.py @@ -108,3 +108,46 @@ class D(deque): # until the allocator gives up, so it is left out of this check. with assert_raises(MemoryError): deque([0]) * sys.maxsize + + +# maxlen=0 keeps nothing, whichever end the item arrives at. +d = deque(maxlen=0) +d.append(1) +d.appendleft(2) +assert list(d) == [] +assert len(d) == 0 +assert d.maxlen == 0 + +d = deque(maxlen=0) +d.extend("abc") +d.extendleft("abc") +d += "abc" +assert list(d) == [] + +assert list(deque("abc", maxlen=0)) == [] +assert list(deque("ab", maxlen=0) * 3) == [] +assert list(deque("ab", maxlen=0) + deque("cd")) == [] + +d = deque("abc", maxlen=0) +d.rotate(1) +assert list(d) == [] + +assert_raises(IndexError, deque(maxlen=0).insert, 0, 1) + + +# A bounded deque still drops from the far end, and only once it is full. +d = deque(maxlen=1) +d.append(1) +assert list(d) == [1] +d.append(2) +assert list(d) == [2] +d.appendleft(3) +assert list(d) == [3] + +d = deque("ab", maxlen=3) +d.append("c") +assert list(d) == ["a", "b", "c"] +d.append("d") +assert list(d) == ["b", "c", "d"] +d.appendleft("z") +assert list(d) == ["z", "b", "c"]