diff --git a/crates/vm/src/builtins/bytearray.rs b/crates/vm/src/builtins/bytearray.rs index c463489df..d6d424799 100644 --- a/crates/vm/src/builtins/bytearray.rs +++ b/crates/vm/src/builtins/bytearray.rs @@ -23,7 +23,7 @@ use crate::{ }, }, convert::{ToPyObject, ToPyResult}, - function::{ArgBytesLike, FuncArgs, PyComparisonValue, PySsize}, + function::{ArgBytesLike, FuncArgs, OptionalArg, PyComparisonValue, PySsize}, protocol::{ BufferDescriptor, BufferFlags, BufferMethods, BufferResizeGuard, PyBuffer, PyIterReturn, PyMappingMethods, PyNumberMethods, PySequenceMethods, @@ -924,15 +924,17 @@ impl Initializer for PyByteArray { type Args = ByteInnerNewOptions; fn init(zelf: &Py, options: Self::Args, vm: &VirtualMachine) -> PyResult<()> { + let source_is_missing = matches!(&options.source, OptionalArg::Missing); // Reject exports and clear existing contents before converting source. zelf.try_resizable(vm)?.elements = Vec::new(); // Conversion can call Python, so do not hold the lock across it and // check again for exports acquired by a callback or another thread. let (mut inner, zero_count) = options.get_inner_with_zero_count(bytearray_from_object, vm)?; - // A zero integer count performs no further writes, including when its - // __index__ callback changed this bytearray or acquired an export. - if zero_count { + // Missing sources and zero integer counts perform no further writes. + // Argument validation above still rejects encoding/errors without a + // source; a zero __index__ callback may have changed or exported self. + if source_is_missing || zero_count { return Ok(()); } core::mem::swap(&mut *zelf.try_resizable(vm)?, &mut inner); diff --git a/extra_tests/snippets/builtin_bytearray.py b/extra_tests/snippets/builtin_bytearray.py index 2f2793a76..6b397b12f 100644 --- a/extra_tests/snippets/builtin_bytearray.py +++ b/extra_tests/snippets/builtin_bytearray.py @@ -1504,3 +1504,52 @@ def test_bytearray_callback_exports(): test_bytearray_callback_exports() + + +def test_bytearray_missing_source_init(): + from testutils import assert_raises + + for initial in (b"", b"previous contents"): + value = bytearray(initial) + assert value.__init__() is None + assert value == b"" + assert value.__alloc__() == 0 + + for kwargs, message in ( + ({"encoding": "utf-8"}, "encoding without a string argument"), + ({"errors": "strict"}, "errors without a string argument"), + ( + {"encoding": "utf-8", "errors": "strict"}, + "encoding without a string argument", + ), + ): + value = bytearray(initial) + with assert_raises(TypeError) as caught: + value.__init__(**kwargs) + assert str(caught.exception) == message + assert value == b"" + assert value.__alloc__() == 0 + + for kwargs in ( + {}, + {"encoding": "utf-8"}, + {"errors": "strict"}, + {"encoding": "utf-8", "errors": "strict"}, + ): + value = bytearray(initial) + with memoryview(value) as view: + with assert_raises(BufferError): + value.__init__(**kwargs) + assert value == initial + assert view.tobytes() == initial + assert value.__init__() is None + assert value == b"" + + # Explicit None is a source and must still go through conversion. + value = bytearray(initial) + with assert_raises(TypeError): + value.__init__(None) + assert value == b"" + + +test_bytearray_missing_source_init()