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/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] 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/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/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_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 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() diff --git a/extra_tests/snippets/stdlib_typing.py b/extra_tests/snippets/stdlib_typing.py index 8ba64844c52..4bebfae4540 100644 --- a/extra_tests/snippets/stdlib_typing.py +++ b/extra_tests/snippets/stdlib_typing.py @@ -65,6 +65,11 @@ def method(self, value: Union[int, float]) -> Union[str, bytes]: 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 + # Eager aliases without a caller module never inspect their aliased value. alias_module_lookups = []