From 548c2148d9be01ea3439c2d27aac49cd816ef084 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Wed, 17 Jun 2026 11:30:29 -0700 Subject: [PATCH] gh-71448: Support extension in BytesIO.truncate Truncate beyond current size of I/O objects is documented to change the amount of allocated space. BytesIO supported truncate to shrink but not truncate to extend. Update BytesIO to support extension and fully implement the `IOBase.truncate()` API. --- Doc/library/io.rst | 7 ++++++ Lib/_pyio.py | 2 +- Lib/test/test_io/test_memoryio.py | 25 +++++++++++++++++++ ...6-06-25-22-43-34.gh-issue-71448.ajvMxg.rst | 2 ++ Modules/_io/bytesio.c | 11 ++++++-- 5 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-06-25-22-43-34.gh-issue-71448.ajvMxg.rst diff --git a/Doc/library/io.rst b/Doc/library/io.rst index d47b74efe22de9d..ca35b97c754080b 100644 --- a/Doc/library/io.rst +++ b/Doc/library/io.rst @@ -785,6 +785,13 @@ than raw I/O does. .. versionadded:: 3.5 + .. method:: truncate(size=None, /) + + In :class:`BytesIO`, this is the same as :meth:`IOBase.truncate`. + + .. versionchanged:: next + Now extends the underlying buffer as :meth:`IOBase.truncate` documents. + .. class:: BufferedReader(raw, buffer_size=DEFAULT_BUFFER_SIZE) A buffered binary stream providing higher-level access to a readable, non diff --git a/Lib/_pyio.py b/Lib/_pyio.py index 4ba9b4070dff93e..4693993d9a13d8d 100644 --- a/Lib/_pyio.py +++ b/Lib/_pyio.py @@ -1016,7 +1016,7 @@ def truncate(self, pos=None): pos = pos_index() if pos < 0: raise ValueError("negative truncate position %r" % (pos,)) - del self._buffer[pos:] + self._buffer.resize(pos) return pos def readable(self): diff --git a/Lib/test/test_io/test_memoryio.py b/Lib/test/test_io/test_memoryio.py index 3669ac0b038b71b..e8b159fcc0d73ec 100644 --- a/Lib/test/test_io/test_memoryio.py +++ b/Lib/test/test_io/test_memoryio.py @@ -558,6 +558,31 @@ def test_relative_seek(self): memio.seek(1, 1) self.assertEqual(memio.read(), buf[1:]) + def test_truncate_extend(self): + # gh-71448: Extending with truncate should allocate space. + buf = self.buftype("123") + memio = self.ioclass(buf) + + self.assertEqual(memio.tell(), 0) + self.assertEqual(memio.truncate(4), 4) + self.assertEqual(len(memio.getbuffer()), 4) + self.assertEqual(memio.getvalue(), b"123\x00") + self.assertEqual(memio.tell(), 0) # Truncate keeps pos. + # Truncate to position 0 should work. + self.assertEqual(memio.truncate(), 0) + self.assertEqual(memio.getvalue(), b"") + + self.assertEqual(memio.seek(12), 12) + self.assertEqual(memio.truncate(1), 1) + self.assertEqual(len(memio.getbuffer()), 1) + self.assertEqual(memio.getvalue(), b"\x00") + self.assertEqual(memio.tell(), 12) + + self.assertEqual(memio.truncate(), 12) + self.assertEqual(len(memio.getbuffer()), 12) + self.assertEqual(memio.getvalue(), b"\x00" * 12) + self.assertEqual(memio.tell(), 12) + def test_issue141311(self): memio = self.ioclass() # Seek allows PY_SSIZE_T_MAX, read should handle that. diff --git a/Misc/NEWS.d/next/Library/2026-06-25-22-43-34.gh-issue-71448.ajvMxg.rst b/Misc/NEWS.d/next/Library/2026-06-25-22-43-34.gh-issue-71448.ajvMxg.rst new file mode 100644 index 000000000000000..05336eb85239471 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-25-22-43-34.gh-issue-71448.ajvMxg.rst @@ -0,0 +1,2 @@ +Update :meth:`io.BytesIO.truncate` to match :meth:`io.IOBase.truncate` +resize extension behavior. All resize behavior now matches. diff --git a/Modules/_io/bytesio.c b/Modules/_io/bytesio.c index 8cdcbd0d89c718e..07c5fbfb6029868 100644 --- a/Modules/_io/bytesio.c +++ b/Modules/_io/bytesio.c @@ -667,10 +667,17 @@ _io_BytesIO_truncate_impl(bytesio *self, PyObject *size) } } - if (new_size < self->string_size) { + if (new_size != self->string_size) { + Py_ssize_t orig_size = self->string_size; self->string_size = new_size; - if (resize_buffer_lock_held(self, new_size) < 0) + if (resize_buffer_lock_held(self, new_size) < 0) { return NULL; + } + /* Fill new space with zeros */ + if (new_size > orig_size) { + memset(PyBytes_AS_STRING(self->buf) + orig_size, '\0', + (new_size - orig_size) * sizeof(char)); + } } return PyLong_FromSsize_t(new_size);