From 71956aaf99a5c1443baa86849fbd8865a78bd522 Mon Sep 17 00:00:00 2001 From: dot Date: Sat, 10 Oct 2026 20:54:40 +0900 Subject: [PATCH 1/4] Preserve ParamSpec bounds as in Python 3.14.8 Extract the backported bound-validation removal and align the CPython reference pin to 3.14.8 so both snippet interpreters exercise the same contract. Assisted-by: OpenAI Codex:model identifier unavailable --- .python-version | 2 +- crates/vm/src/stdlib/typevar.rs | 5 +---- extra_tests/snippets/stdlib_typing.py | 6 ++++++ 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.python-version b/.python-version index a128d5c0d97..9b8fe2fbb85 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.14.7 +3.14.8 diff --git a/crates/vm/src/stdlib/typevar.rs b/crates/vm/src/stdlib/typevar.rs index 19ad3562540..3cc4b9b4b66 100644 --- a/crates/vm/src/stdlib/typevar.rs +++ b/crates/vm/src/stdlib/typevar.rs @@ -583,10 +583,7 @@ pub(crate) mod typevar { return Err(vm.new_type_error("ParamSpec() takes at most 1 positional argument")); }; - let bound = kwargs - .swap_remove("bound") - .map(|b| type_check(b, "Bound must be a type.", vm)) - .transpose()?; + let bound = kwargs.swap_remove("bound"); let covariant = kwargs .swap_remove("covariant") .map(|v| v.try_to_bool(vm)) diff --git a/extra_tests/snippets/stdlib_typing.py b/extra_tests/snippets/stdlib_typing.py index 4082d683f8d..c07690c54c4 100644 --- a/extra_tests/snippets/stdlib_typing.py +++ b/extra_tests/snippets/stdlib_typing.py @@ -63,3 +63,9 @@ def method(self, value: Union[int, float]) -> Union[str, bytes]: repr(nested) except RecursionError: pass + + +# Python 3.14.8 retains ParamSpec bounds without converting or validating them. +assert ParamSpec("Unbounded").__bound__ is None +for bound in (None, "int", "not valid Python!", (int, str), object()): + assert ParamSpec("BoundSpec", bound=bound).__bound__ is bound From 399e5b06d141d6bf25bbdc36083da29035318c73 Mon Sep 17 00:00:00 2001 From: dot Date: Sat, 10 Oct 2026 23:51:46 +0900 Subject: [PATCH 2/4] Align TextIOWrapper chunk-size deletion with Python 3.14.8 The 3.14.8 reference pin exposes the Argument Clinic deletion guard moved before initialization and attachment checks by CPython 90a1f02b7da0c356d10a23f1faee7dc063f887ed. Preserve assignment checks and align the exact deletion error, including subclass type names. Assisted-by: OpenAI Codex:model identifier unavailable --- crates/vm/src/stdlib/_io.rs | 42 +++++++++++++++++-------------- extra_tests/snippets/stdlib_io.py | 22 +++++++++++++--- 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/crates/vm/src/stdlib/_io.rs b/crates/vm/src/stdlib/_io.rs index d4cd66e1491..14ca013a831 100644 --- a/crates/vm/src/stdlib/_io.rs +++ b/crates/vm/src/stdlib/_io.rs @@ -3568,6 +3568,17 @@ mod _io { #[pygetset(setter, name = "_CHUNK_SIZE")] fn set_chunksize(&self, value: PySetterValue, vm: &VirtualMachine) -> PyResult<()> { + let PySetterValue::Assign(object_value) = value else { + let type_name = self.class().slot_name(); + let mut end = type_name.len().min(100); + while !type_name.is_char_boundary(end) { + end -= 1; + } + return Err(vm.new_attribute_error(format!( + "attribute '_CHUNK_SIZE' of '{}' objects cannot be deleted", + &type_name[..end] + ))); + }; { let textio = self.lock(vm)?; if self.buffer.deref().is_none() { @@ -3576,27 +3587,20 @@ mod _io { drop(textio); } - let chunk_size: isize = match value { - PySetterValue::Assign(object_value) => { - let integer = object_value.try_index(vm)?; + let integer = object_value.try_index(vm)?; - integer.try_to_primitive::(vm).map_err(|_| { - let class = object_value.class(); - let type_name = class.name(); - let mut end = type_name.len().min(200); - while !type_name.is_char_boundary(end) { - end -= 1; - } - vm.new_value_error(format!( - "cannot fit '{}' into an index-sized integer", - &type_name[..end] - )) - })? + let chunk_size = integer.try_to_primitive::(vm).map_err(|_| { + let class = object_value.class(); + let type_name = class.name(); + let mut end = type_name.len().min(200); + while !type_name.is_char_boundary(end) { + end -= 1; } - PySetterValue::Delete => { - return Err(vm.new_attribute_error("cannot delete attribute")); - } - }; + vm.new_value_error(format!( + "cannot fit '{}' into an index-sized integer", + &type_name[..end] + )) + })?; if chunk_size <= 0 { return Err(vm.new_value_error("a strictly positive integer is required")); diff --git a/extra_tests/snippets/stdlib_io.py b/extra_tests/snippets/stdlib_io.py index 8346ddbb62d..07b127999ea 100644 --- a/extra_tests/snippets/stdlib_io.py +++ b/extra_tests/snippets/stdlib_io.py @@ -166,10 +166,24 @@ def __index__(self): "underlying buffer has been detached", lambda: setattr(detached_textio, "_CHUNK_SIZE", 16), ) -expect_value_error( - "underlying buffer has been detached", - lambda: delattr(detached_textio, "_CHUNK_SIZE"), -) +for wrapper in (textio, uninitialized_textio, detached_textio): + with assert_raises(AttributeError) as error: + del wrapper._CHUNK_SIZE + assert str(error.exception) == ( + "attribute '_CHUNK_SIZE' of '_io.TextIOWrapper' objects cannot be deleted" + ) + + +for wrapper_name in ("ChunkWrapper", "€" * 34): + wrapper_type = type(wrapper_name, (TextIOWrapper,), {}) + wrapper = wrapper_type(BytesIO()) + with assert_raises(AttributeError) as error: + del wrapper._CHUNK_SIZE + displayed_name = wrapper_name.encode("utf-8")[:100].decode("utf-8", "ignore") + assert str(error.exception) == ( + f"attribute '_CHUNK_SIZE' of '{displayed_name}' objects cannot be deleted" + ) + wrapper.close() long_type_name = "X" * 250 From f165f99a9c16accb878334ea195883169e90cfd7 Mon Sep 17 00:00:00 2001 From: dot Date: Sat, 10 Oct 2026 22:29:41 +0900 Subject: [PATCH 3/4] Validate mmap assignments against current storage Use the existing mapping guard for bounds and writes after argument conversion, including empty assignments. Match the Python 3.14.8 ordering while preserving export guards and ownership APIs. Assisted-by: OpenAI Codex:model identifier unavailable --- crates/stdlib/src/mmap.rs | 58 +++++++++++++---------------- extra_tests/snippets/stdlib_mmap.py | 38 +++++++++++++++++++ 2 files changed, 63 insertions(+), 33 deletions(-) diff --git a/crates/stdlib/src/mmap.rs b/crates/stdlib/src/mmap.rs index a242f8f1d29..371a800c477 100644 --- a/crates/stdlib/src/mmap.rs +++ b/crates/stdlib/src/mmap.rs @@ -1455,17 +1455,15 @@ mod mmap { value: &PyObject, vm: &VirtualMachine, ) -> PyResult<()> { - let i: usize = i - .wrapped_at(self.__len__()) - .ok_or_else(|| vm.new_index_error("mmap index out of range"))?; - let b = value_from_object(vm, value)?; self.try_writable(vm, |mmap| { + let i = i + .wrapped_at(mmap.len()) + .ok_or_else(|| vm.new_index_error("mmap index out of range"))?; mmap[i] = b; - })?; - - Ok(()) + Ok(()) + })? } fn setitem_by_slice( @@ -1474,43 +1472,37 @@ mod mmap { value: &PyObject, vm: &VirtualMachine, ) -> PyResult<()> { - let (range, step, slice_len) = slice.adjust_indices(self.__len__()); - let bytes = bytes_from_object(vm, value)?; - if bytes.len() != slice_len { - return Err(vm.new_index_error("mmap slice assignment is wrong size")); - } + self.try_writable(vm, |mmap| { + let (range, step, slice_len) = slice.adjust_indices(mmap.len()); - if slice_len == 0 { - // do nothing - Ok(()) - } else if step == 1 { - self.try_writable(vm, |mmap| { + if bytes.len() != slice_len { + return Err(vm.new_index_error("mmap slice assignment is wrong size")); + } + + if slice_len == 0 { + // do nothing + } else if step == 1 { (&mut mmap[range]) .write(&bytes) .map_err(|err| err.to_pyexception(vm))?; - Ok(()) - })? - } else { - let mut bi = 0; // bytes index - if step.is_negative() { - for i in range.rev().step_by(step.unsigned_abs()) { - self.try_writable(vm, |mmap| { - mmap[i] = bytes[bi]; - })?; - bi += 1; - } } else { - for i in range.step_by(step.unsigned_abs()) { - self.try_writable(vm, |mmap| { + let mut bi = 0; // bytes index + if step.is_negative() { + for i in range.rev().step_by(step.unsigned_abs()) { + mmap[i] = bytes[bi]; + bi += 1; + } + } else { + for i in range.step_by(step.unsigned_abs()) { mmap[i] = bytes[bi]; - })?; - bi += 1; + bi += 1; + } } } Ok(()) - } + })? } } diff --git a/extra_tests/snippets/stdlib_mmap.py b/extra_tests/snippets/stdlib_mmap.py index 2c4efdf58bf..1dc89e29d80 100644 --- a/extra_tests/snippets/stdlib_mmap.py +++ b/extra_tests/snippets/stdlib_mmap.py @@ -77,3 +77,41 @@ with assert_raises(ValueError): mapped.resize(4) assert mapped.closed + + +# Assignment validates current storage after argument conversion, even if empty. +with mmap.mmap(-1, 4) as mapped: + with assert_raises(ValueError): + mapped[4] = 256 + mapped[0:0] = b"" +with assert_raises(ValueError): + mapped[0:0] = b"" + +with mmap.mmap(-1, 4, access=mmap.ACCESS_READ) as mapped: + with assert_raises(TypeError): + mapped[0:0] = b"" + + +class CloseOnIndex: + def __init__(self, mapped): + self.mapped = mapped + + def __index__(self): + self.mapped.close() + return 0 + + +for close_from in ("key", "value", "slice"): + mapped = mmap.mmap(-1, 4) + closer = CloseOnIndex(mapped) + try: + with assert_raises(ValueError): + if close_from == "key": + mapped[closer] = 0 + elif close_from == "value": + mapped[0] = closer + else: + mapped[closer:0] = b"" + assert mapped.closed + finally: + mapped.close() From 6acb8d618a9bb70a8984931c3eefe2763b9171be Mon Sep 17 00:00:00 2001 From: dot Date: Sun, 11 Oct 2026 05:58:02 +0900 Subject: [PATCH 4/4] Align mmap boundary regression with Python 3.14.8 Include the matching upstream GH-158027 test-data correction: use an integer RHS so the unchanged IndexError assertion checks index bounds after conversion. Preserve the runtime assignment and storage guards. Assisted-by: OpenAI Codex:model identifier unavailable --- Lib/test/test_mmap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_mmap.py b/Lib/test/test_mmap.py index bd8744a0ca8..309c0d8c933 100644 --- a/Lib/test/test_mmap.py +++ b/Lib/test/test_mmap.py @@ -73,7 +73,7 @@ def test_basic(self): # Shouldn't crash on boundary (Issue #5292) self.assertRaises(IndexError, m.__getitem__, len(m)) - self.assertRaises(IndexError, m.__setitem__, len(m), b'\0') + self.assertRaises(IndexError, m.__setitem__, len(m), 0) # Modify the file's content m[0] = b'3'[0]