diff --git a/Doc/c-api/bytearray.rst b/Doc/c-api/bytearray.rst index 9045689a6be567..15295096a710c8 100644 --- a/Doc/c-api/bytearray.rst +++ b/Doc/c-api/bytearray.rst @@ -74,6 +74,11 @@ Direct API functions .. c:function:: int PyByteArray_Resize(PyObject *bytearray, Py_ssize_t len) Resize the internal buffer of *bytearray* to *len*. + Failure is a ``-1`` return with an exception set. + + .. versionchanged:: next + A negative *len* will now result in an exception being set and -1 returned. + Macros ^^^^^^ diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index 6050784264707b..4a15e27f82a160 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -2841,6 +2841,38 @@ objects. optional *sep* and *bytes_per_sep* parameters to insert separators between bytes in the hex output. + .. method:: resize(size) + + Resize the :class:`bytearray` to contain *size* bytes. *size* must be + greater than or equal to 0. + + If the :class:`bytearray` needs to shrink, bytes beyond *size* are truncated. + + If the :class:`bytearray` needs to grow, all new bytes, those beyond *size*, + will be set to null bytes. + + + This is equivalent to: + + >>> def resize(ba, size): + ... if len(ba) > size: + ... del ba[size:] + ... else: + ... ba += b'\0' * (size - len(ba)) + + Examples: + + >>> shrink = bytearray(b'abc') + >>> shrink.resize(1) + >>> (shrink, len(shrink)) + (bytearray(b'a'), 1) + >>> grow = bytearray(b'abc') + >>> grow.resize(5) + >>> (grow, len(grow)) + (bytearray(b'abc\x00\x00'), 5) + + .. versionadded:: next + Since bytearray objects are sequences of integers (akin to a list), for a bytearray object *b*, ``b[0]`` will be an integer, while ``b[0:1]`` will be a bytearray object of length 1. (This contrasts with text strings, where diff --git a/Lib/test/test_bytes.py b/Lib/test/test_bytes.py index 7bb1ab38aa4fdf..18d619eb6239a1 100644 --- a/Lib/test/test_bytes.py +++ b/Lib/test/test_bytes.py @@ -1359,6 +1359,44 @@ def by(s): b = by("Hello, world") self.assertEqual(re.findall(br"\w+", b), [by("Hello"), by("world")]) + def test_resize(self): + ba = bytearray(b'abcdef') + self.assertIsNone(ba.resize(3)) + self.assertEqual(ba, bytearray(b'abc')) + + self.assertIsNone(ba.resize(10)) + self.assertEqual(len(ba), 10) + # Bytes beyond set values must be cleared. + self.assertEqual(ba, bytearray(b'abc\0\0\0\0\0\0\0')) + + ba[3:10] = b'defghij' + self.assertEqual(ba, bytearray(b'abcdefghij')) + + self.assertIsNone(ba.resize(2 ** 20)) + self.assertEqual(len(ba), 2**20) + self.assertEqual(ba, bytearray(b'abcdefghij' + b'\0' * (2 ** 20 - 10))) + + self.assertIsNone(ba.resize(0)) + self.assertEqual(ba, bytearray()) + + self.assertIsNone(ba.resize(10)) + self.assertEqual(ba, bytearray(b'\0' * 10)) + + # Subclass + ba = ByteArraySubclass(b'abcdef') + self.assertIsNone(ba.resize(3)) + self.assertEqual(ba, bytearray(b'abc')) + + # Check arguments + self.assertRaises(TypeError, bytearray().resize) + self.assertRaises(TypeError, bytearray().resize, (10, 10)) + + self.assertRaises(ValueError, bytearray().resize, -1) + self.assertRaises(ValueError, bytearray().resize, -200) + self.assertRaises(MemoryError, bytearray().resize, sys.maxsize) + self.assertRaises(MemoryError, bytearray(1000).resize, sys.maxsize) + + def test_setitem(self): def setitem_as_mapping(b, i, val): b[i] = val @@ -1715,17 +1753,18 @@ def test_resize_forbidden(self): # if it wouldn't reallocate the underlying buffer. # Furthermore, no destructive changes to the buffer may be applied # before raising the error. - b = bytearray(range(10)) + b = bytearray(10) v = memoryview(b) - def resize(n): + def manual_resize(n): b[1:-1] = range(n + 1, 2*n - 1) - resize(10) + b.resize(10) orig = b[:] - self.assertRaises(BufferError, resize, 11) + self.assertRaises(BufferError, b.resize, 11) + self.assertRaises(BufferError, manual_resize, 11) self.assertEqual(b, orig) - self.assertRaises(BufferError, resize, 9) + self.assertRaises(BufferError, b.resize, 9) self.assertEqual(b, orig) - self.assertRaises(BufferError, resize, 0) + self.assertRaises(BufferError, b.resize, 0) self.assertEqual(b, orig) # Other operations implying resize self.assertRaises(BufferError, b.pop, 0) diff --git a/Lib/test/test_capi/test_bytearray.py b/Lib/test/test_capi/test_bytearray.py index 39099f6b82240f..323e0d2a5acdcb 100644 --- a/Lib/test/test_capi/test_bytearray.py +++ b/Lib/test/test_capi/test_bytearray.py @@ -151,10 +151,11 @@ def test_resize(self): self.assertEqual(resize(ba, 3), 0) self.assertEqual(ba, bytearray(b'abc')) + self.assertRaises(ValueError, resize, bytearray(), -1) + self.assertRaises(ValueError, resize, bytearray(), -200) self.assertRaises(MemoryError, resize, bytearray(), PY_SSIZE_T_MAX) self.assertRaises(MemoryError, resize, bytearray(1000), PY_SSIZE_T_MAX) - # CRASHES resize(bytearray(b'abc'), -1) # CRASHES resize(b'abc', 0) # CRASHES resize(object(), 0) # CRASHES resize(NULL, 0) diff --git a/Misc/NEWS.d/next/Library/2025-02-01-14-55-33.gh-issue-129559.hQCeAz.rst b/Misc/NEWS.d/next/Library/2025-02-01-14-55-33.gh-issue-129559.hQCeAz.rst new file mode 100644 index 00000000000000..f08d47b63a84b7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-02-01-14-55-33.gh-issue-129559.hQCeAz.rst @@ -0,0 +1,2 @@ +Add :meth:`bytearray.resize` method so :class:`bytearray` can be efficiently +resized in place. diff --git a/Objects/bytearrayobject.c b/Objects/bytearrayobject.c index 21584332e0e443..6133d30f49930a 100644 --- a/Objects/bytearrayobject.c +++ b/Objects/bytearrayobject.c @@ -184,7 +184,12 @@ PyByteArray_Resize(PyObject *self, Py_ssize_t requested_size) assert(self != NULL); assert(PyByteArray_Check(self)); assert(logical_offset <= alloc); - assert(requested_size >= 0); + + if (requested_size < 0) { + PyErr_Format(PyExc_ValueError, + "Can only resize to positive sizes, got %zd", requested_size); + return -1; + } if (requested_size == Py_SIZE(self)) { return 0; @@ -1388,6 +1393,31 @@ bytearray_removesuffix_impl(PyByteArrayObject *self, Py_buffer *suffix) } +/*[clinic input] +bytearray.resize + size: Py_ssize_t + New size to resize to.. + / +Resize the internal buffer of bytearray to len. +[clinic start generated code]*/ + +static PyObject * +bytearray_resize_impl(PyByteArrayObject *self, Py_ssize_t size) +/*[clinic end generated code: output=f73524922990b2d9 input=75fd4d17c4aa47d3]*/ +{ + Py_ssize_t start_size = PyByteArray_GET_SIZE(self); + int result = PyByteArray_Resize((PyObject *)self, size); + if (result < 0) { + return NULL; + } + // Set new bytes to null bytes + if (size > start_size) { + memset(PyByteArray_AS_STRING(self) + start_size, 0, size - start_size); + } + Py_RETURN_NONE; +} + + /*[clinic input] bytearray.translate @@ -2361,6 +2391,7 @@ static PyMethodDef bytearray_methods[] = { BYTEARRAY_REPLACE_METHODDEF BYTEARRAY_REMOVEPREFIX_METHODDEF BYTEARRAY_REMOVESUFFIX_METHODDEF + BYTEARRAY_RESIZE_METHODDEF BYTEARRAY_REVERSE_METHODDEF BYTEARRAY_RFIND_METHODDEF BYTEARRAY_RINDEX_METHODDEF diff --git a/Objects/clinic/bytearrayobject.c.h b/Objects/clinic/bytearrayobject.c.h index 91cf5363e639d1..03b5a8a516cc09 100644 --- a/Objects/clinic/bytearrayobject.c.h +++ b/Objects/clinic/bytearrayobject.c.h @@ -565,6 +565,45 @@ bytearray_removesuffix(PyObject *self, PyObject *arg) return return_value; } +PyDoc_STRVAR(bytearray_resize__doc__, +"resize($self, size, /)\n" +"--\n" +"\n" +"Resize the internal buffer of bytearray to len.\n" +"\n" +" size\n" +" New size to resize to.."); + +#define BYTEARRAY_RESIZE_METHODDEF \ + {"resize", (PyCFunction)bytearray_resize, METH_O, bytearray_resize__doc__}, + +static PyObject * +bytearray_resize_impl(PyByteArrayObject *self, Py_ssize_t size); + +static PyObject * +bytearray_resize(PyObject *self, PyObject *arg) +{ + PyObject *return_value = NULL; + Py_ssize_t size; + + { + Py_ssize_t ival = -1; + PyObject *iobj = _PyNumber_Index(arg); + if (iobj != NULL) { + ival = PyLong_AsSsize_t(iobj); + Py_DECREF(iobj); + } + if (ival == -1 && PyErr_Occurred()) { + goto exit; + } + size = ival; + } + return_value = bytearray_resize_impl((PyByteArrayObject *)self, size); + +exit: + return return_value; +} + PyDoc_STRVAR(bytearray_translate__doc__, "translate($self, table, /, delete=b\'\')\n" "--\n" @@ -1623,4 +1662,4 @@ bytearray_sizeof(PyObject *self, PyObject *Py_UNUSED(ignored)) { return bytearray_sizeof_impl((PyByteArrayObject *)self); } -/*[clinic end generated code: output=bc8bec8514102bf3 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=41bb67a8a181e733 input=a9049054013a1b77]*/