Skip to content

Commit 548c214

Browse files
committed
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.
1 parent 285d96d commit 548c214

5 files changed

Lines changed: 44 additions & 3 deletions

File tree

Doc/library/io.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,13 @@ than raw I/O does.
785785

786786
.. versionadded:: 3.5
787787

788+
.. method:: truncate(size=None, /)
789+
790+
In :class:`BytesIO`, this is the same as :meth:`IOBase.truncate`.
791+
792+
.. versionchanged:: next
793+
Now extends the underlying buffer as :meth:`IOBase.truncate` documents.
794+
788795
.. class:: BufferedReader(raw, buffer_size=DEFAULT_BUFFER_SIZE)
789796

790797
A buffered binary stream providing higher-level access to a readable, non

Lib/_pyio.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1016,7 +1016,7 @@ def truncate(self, pos=None):
10161016
pos = pos_index()
10171017
if pos < 0:
10181018
raise ValueError("negative truncate position %r" % (pos,))
1019-
del self._buffer[pos:]
1019+
self._buffer.resize(pos)
10201020
return pos
10211021

10221022
def readable(self):

Lib/test/test_io/test_memoryio.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,31 @@ def test_relative_seek(self):
558558
memio.seek(1, 1)
559559
self.assertEqual(memio.read(), buf[1:])
560560

561+
def test_truncate_extend(self):
562+
# gh-71448: Extending with truncate should allocate space.
563+
buf = self.buftype("123")
564+
memio = self.ioclass(buf)
565+
566+
self.assertEqual(memio.tell(), 0)
567+
self.assertEqual(memio.truncate(4), 4)
568+
self.assertEqual(len(memio.getbuffer()), 4)
569+
self.assertEqual(memio.getvalue(), b"123\x00")
570+
self.assertEqual(memio.tell(), 0) # Truncate keeps pos.
571+
# Truncate to position 0 should work.
572+
self.assertEqual(memio.truncate(), 0)
573+
self.assertEqual(memio.getvalue(), b"")
574+
575+
self.assertEqual(memio.seek(12), 12)
576+
self.assertEqual(memio.truncate(1), 1)
577+
self.assertEqual(len(memio.getbuffer()), 1)
578+
self.assertEqual(memio.getvalue(), b"\x00")
579+
self.assertEqual(memio.tell(), 12)
580+
581+
self.assertEqual(memio.truncate(), 12)
582+
self.assertEqual(len(memio.getbuffer()), 12)
583+
self.assertEqual(memio.getvalue(), b"\x00" * 12)
584+
self.assertEqual(memio.tell(), 12)
585+
561586
def test_issue141311(self):
562587
memio = self.ioclass()
563588
# Seek allows PY_SSIZE_T_MAX, read should handle that.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Update :meth:`io.BytesIO.truncate` to match :meth:`io.IOBase.truncate`
2+
resize extension behavior. All resize behavior now matches.

Modules/_io/bytesio.c

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -667,10 +667,17 @@ _io_BytesIO_truncate_impl(bytesio *self, PyObject *size)
667667
}
668668
}
669669

670-
if (new_size < self->string_size) {
670+
if (new_size != self->string_size) {
671+
Py_ssize_t orig_size = self->string_size;
671672
self->string_size = new_size;
672-
if (resize_buffer_lock_held(self, new_size) < 0)
673+
if (resize_buffer_lock_held(self, new_size) < 0) {
673674
return NULL;
675+
}
676+
/* Fill new space with zeros */
677+
if (new_size > orig_size) {
678+
memset(PyBytes_AS_STRING(self->buf) + orig_size, '\0',
679+
(new_size - orig_size) * sizeof(char));
680+
}
674681
}
675682

676683
return PyLong_FromSsize_t(new_size);

0 commit comments

Comments
 (0)