]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-129559: Add `bytearray.resize()` (GH-129560)
authorCody Maloney <cmaloney@users.noreply.github.com>
Wed, 5 Feb 2025 19:33:17 +0000 (11:33 -0800)
committerGitHub <noreply@github.com>
Wed, 5 Feb 2025 19:33:17 +0000 (11:33 -0800)
Add bytearray.resize() which wraps PyByteArray_Resize.

Make negative size passed to resize exception/error rather than crash in optimized builds.

Doc/c-api/bytearray.rst
Doc/library/stdtypes.rst
Lib/test/test_bytes.py
Lib/test/test_capi/test_bytearray.py
Misc/NEWS.d/next/Library/2025-02-01-14-55-33.gh-issue-129559.hQCeAz.rst [new file with mode: 0644]
Objects/bytearrayobject.c
Objects/clinic/bytearrayobject.c.h

index 9045689a6be56756c61f11b307058b469fcf5b1e..15295096a710c87d4c89c3a5719a7f72e7e4fa86 100644 (file)
@@ -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
 ^^^^^^
index 6050784264707bea90972771aacee7d7abe0ee2b..4a15e27f82a160a2bb1138eb2a60d1d68b3ee543 100644 (file)
@@ -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
index 7bb1ab38aa4fdf2230d370a84c199bffdced8b82..18d619eb6239a1c2c7cfc6d2980a54795a31e252 100644 (file)
@@ -1359,6 +1359,44 @@ class ByteArrayTest(BaseBytesTest, unittest.TestCase):
         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 @@ class ByteArrayTest(BaseBytesTest, unittest.TestCase):
         # 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)
index 39099f6b82240f96c5c6e4ae47807a43c1843d7a..323e0d2a5acdcbae3e4c34222f1326e9739cebc7 100644 (file)
@@ -151,10 +151,11 @@ class CAPITest(unittest.TestCase):
         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 (file)
index 0000000..f08d47b
--- /dev/null
@@ -0,0 +1,2 @@
+Add :meth:`bytearray.resize` method so :class:`bytearray` can be efficiently
+resized in place.
index 21584332e0e443dffbf83eb3d9e1db03d8eff7aa..6133d30f49930a360fb1293c15af330a87188c78 100644 (file)
@@ -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
index 91cf5363e639d1638fcf3ddd5058af4e333be9b6..03b5a8a516cc0960a2303a58a6e4470d68532b46 100644 (file)
@@ -565,6 +565,45 @@ exit:
     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]*/