Skip to content

Commit 5fb019f

Browse files
authored
gh-129559: Add bytearray.resize() (GH-129560)
Add bytearray.resize() which wraps PyByteArray_Resize. Make negative size passed to resize exception/error rather than crash in optimized builds.
1 parent 7d9a22f commit 5fb019f

File tree

7 files changed

+158
-9
lines changed

7 files changed

+158
-9
lines changed

Diff for: Doc/c-api/bytearray.rst

+5
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ Direct API functions
7474
.. c:function:: int PyByteArray_Resize(PyObject *bytearray, Py_ssize_t len)
7575
7676
Resize the internal buffer of *bytearray* to *len*.
77+
Failure is a ``-1`` return with an exception set.
78+
79+
.. versionchanged:: next
80+
A negative *len* will now result in an exception being set and -1 returned.
81+
7782
7883
Macros
7984
^^^^^^

Diff for: Doc/library/stdtypes.rst

+32
Original file line numberDiff line numberDiff line change
@@ -2841,6 +2841,38 @@ objects.
28412841
optional *sep* and *bytes_per_sep* parameters to insert separators
28422842
between bytes in the hex output.
28432843

2844+
.. method:: resize(size)
2845+
2846+
Resize the :class:`bytearray` to contain *size* bytes. *size* must be
2847+
greater than or equal to 0.
2848+
2849+
If the :class:`bytearray` needs to shrink, bytes beyond *size* are truncated.
2850+
2851+
If the :class:`bytearray` needs to grow, all new bytes, those beyond *size*,
2852+
will be set to null bytes.
2853+
2854+
2855+
This is equivalent to:
2856+
2857+
>>> def resize(ba, size):
2858+
... if len(ba) > size:
2859+
... del ba[size:]
2860+
... else:
2861+
... ba += b'\0' * (size - len(ba))
2862+
2863+
Examples:
2864+
2865+
>>> shrink = bytearray(b'abc')
2866+
>>> shrink.resize(1)
2867+
>>> (shrink, len(shrink))
2868+
(bytearray(b'a'), 1)
2869+
>>> grow = bytearray(b'abc')
2870+
>>> grow.resize(5)
2871+
>>> (grow, len(grow))
2872+
(bytearray(b'abc\x00\x00'), 5)
2873+
2874+
.. versionadded:: next
2875+
28442876
Since bytearray objects are sequences of integers (akin to a list), for a
28452877
bytearray object *b*, ``b[0]`` will be an integer, while ``b[0:1]`` will be
28462878
a bytearray object of length 1. (This contrasts with text strings, where

Diff for: Lib/test/test_bytes.py

+45-6
Original file line numberDiff line numberDiff line change
@@ -1359,6 +1359,44 @@ def by(s):
13591359
b = by("Hello, world")
13601360
self.assertEqual(re.findall(br"\w+", b), [by("Hello"), by("world")])
13611361

1362+
def test_resize(self):
1363+
ba = bytearray(b'abcdef')
1364+
self.assertIsNone(ba.resize(3))
1365+
self.assertEqual(ba, bytearray(b'abc'))
1366+
1367+
self.assertIsNone(ba.resize(10))
1368+
self.assertEqual(len(ba), 10)
1369+
# Bytes beyond set values must be cleared.
1370+
self.assertEqual(ba, bytearray(b'abc\0\0\0\0\0\0\0'))
1371+
1372+
ba[3:10] = b'defghij'
1373+
self.assertEqual(ba, bytearray(b'abcdefghij'))
1374+
1375+
self.assertIsNone(ba.resize(2 ** 20))
1376+
self.assertEqual(len(ba), 2**20)
1377+
self.assertEqual(ba, bytearray(b'abcdefghij' + b'\0' * (2 ** 20 - 10)))
1378+
1379+
self.assertIsNone(ba.resize(0))
1380+
self.assertEqual(ba, bytearray())
1381+
1382+
self.assertIsNone(ba.resize(10))
1383+
self.assertEqual(ba, bytearray(b'\0' * 10))
1384+
1385+
# Subclass
1386+
ba = ByteArraySubclass(b'abcdef')
1387+
self.assertIsNone(ba.resize(3))
1388+
self.assertEqual(ba, bytearray(b'abc'))
1389+
1390+
# Check arguments
1391+
self.assertRaises(TypeError, bytearray().resize)
1392+
self.assertRaises(TypeError, bytearray().resize, (10, 10))
1393+
1394+
self.assertRaises(ValueError, bytearray().resize, -1)
1395+
self.assertRaises(ValueError, bytearray().resize, -200)
1396+
self.assertRaises(MemoryError, bytearray().resize, sys.maxsize)
1397+
self.assertRaises(MemoryError, bytearray(1000).resize, sys.maxsize)
1398+
1399+
13621400
def test_setitem(self):
13631401
def setitem_as_mapping(b, i, val):
13641402
b[i] = val
@@ -1715,17 +1753,18 @@ def test_resize_forbidden(self):
17151753
# if it wouldn't reallocate the underlying buffer.
17161754
# Furthermore, no destructive changes to the buffer may be applied
17171755
# before raising the error.
1718-
b = bytearray(range(10))
1756+
b = bytearray(10)
17191757
v = memoryview(b)
1720-
def resize(n):
1758+
def manual_resize(n):
17211759
b[1:-1] = range(n + 1, 2*n - 1)
1722-
resize(10)
1760+
b.resize(10)
17231761
orig = b[:]
1724-
self.assertRaises(BufferError, resize, 11)
1762+
self.assertRaises(BufferError, b.resize, 11)
1763+
self.assertRaises(BufferError, manual_resize, 11)
17251764
self.assertEqual(b, orig)
1726-
self.assertRaises(BufferError, resize, 9)
1765+
self.assertRaises(BufferError, b.resize, 9)
17271766
self.assertEqual(b, orig)
1728-
self.assertRaises(BufferError, resize, 0)
1767+
self.assertRaises(BufferError, b.resize, 0)
17291768
self.assertEqual(b, orig)
17301769
# Other operations implying resize
17311770
self.assertRaises(BufferError, b.pop, 0)

Diff for: Lib/test/test_capi/test_bytearray.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,11 @@ def test_resize(self):
151151
self.assertEqual(resize(ba, 3), 0)
152152
self.assertEqual(ba, bytearray(b'abc'))
153153

154+
self.assertRaises(ValueError, resize, bytearray(), -1)
155+
self.assertRaises(ValueError, resize, bytearray(), -200)
154156
self.assertRaises(MemoryError, resize, bytearray(), PY_SSIZE_T_MAX)
155157
self.assertRaises(MemoryError, resize, bytearray(1000), PY_SSIZE_T_MAX)
156158

157-
# CRASHES resize(bytearray(b'abc'), -1)
158159
# CRASHES resize(b'abc', 0)
159160
# CRASHES resize(object(), 0)
160161
# CRASHES resize(NULL, 0)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :meth:`bytearray.resize` method so :class:`bytearray` can be efficiently
2+
resized in place.

Diff for: Objects/bytearrayobject.c

+32-1
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,12 @@ PyByteArray_Resize(PyObject *self, Py_ssize_t requested_size)
184184
assert(self != NULL);
185185
assert(PyByteArray_Check(self));
186186
assert(logical_offset <= alloc);
187-
assert(requested_size >= 0);
187+
188+
if (requested_size < 0) {
189+
PyErr_Format(PyExc_ValueError,
190+
"Can only resize to positive sizes, got %zd", requested_size);
191+
return -1;
192+
}
188193

189194
if (requested_size == Py_SIZE(self)) {
190195
return 0;
@@ -1388,6 +1393,31 @@ bytearray_removesuffix_impl(PyByteArrayObject *self, Py_buffer *suffix)
13881393
}
13891394

13901395

1396+
/*[clinic input]
1397+
bytearray.resize
1398+
size: Py_ssize_t
1399+
New size to resize to..
1400+
/
1401+
Resize the internal buffer of bytearray to len.
1402+
[clinic start generated code]*/
1403+
1404+
static PyObject *
1405+
bytearray_resize_impl(PyByteArrayObject *self, Py_ssize_t size)
1406+
/*[clinic end generated code: output=f73524922990b2d9 input=75fd4d17c4aa47d3]*/
1407+
{
1408+
Py_ssize_t start_size = PyByteArray_GET_SIZE(self);
1409+
int result = PyByteArray_Resize((PyObject *)self, size);
1410+
if (result < 0) {
1411+
return NULL;
1412+
}
1413+
// Set new bytes to null bytes
1414+
if (size > start_size) {
1415+
memset(PyByteArray_AS_STRING(self) + start_size, 0, size - start_size);
1416+
}
1417+
Py_RETURN_NONE;
1418+
}
1419+
1420+
13911421
/*[clinic input]
13921422
bytearray.translate
13931423
@@ -2361,6 +2391,7 @@ static PyMethodDef bytearray_methods[] = {
23612391
BYTEARRAY_REPLACE_METHODDEF
23622392
BYTEARRAY_REMOVEPREFIX_METHODDEF
23632393
BYTEARRAY_REMOVESUFFIX_METHODDEF
2394+
BYTEARRAY_RESIZE_METHODDEF
23642395
BYTEARRAY_REVERSE_METHODDEF
23652396
BYTEARRAY_RFIND_METHODDEF
23662397
BYTEARRAY_RINDEX_METHODDEF

Diff for: Objects/clinic/bytearrayobject.c.h

+40-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)