Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Doc/library/io.rst
Original file line number Diff line number Diff line change
Expand Up @@ -763,10 +763,13 @@ than raw I/O does.

.. note::
As long as the view exists, the :class:`BytesIO` object cannot be
resized or closed.
resized. Closing it does not invalidate the view.

.. versionadded:: 3.2

.. versionchanged:: next
The :class:`BytesIO` object can now be closed while the view exists.

.. method:: getvalue()

Return :class:`bytes` containing the entire contents of the buffer.
Expand Down
35 changes: 20 additions & 15 deletions Lib/test/test_io/test_memoryio.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,9 +457,6 @@ def test_getbuffer(self):
# raises a BufferError.
self.assertRaises(BufferError, memio.write, b'x' * 100)
self.assertRaises(BufferError, memio.truncate)
# gh-111049: _io.BytesIO detach on close would lead to corruption.
if self.ioclass is io.BytesIO:
self.assertRaises(BufferError, memio.close)
self.assertFalse(memio.closed)
# Mutating the buffer updates the BytesIO
buf[3:6] = b"abc"
Expand All @@ -474,12 +471,7 @@ def test_getbuffer(self):
self.assertRaises(ValueError, memio.getbuffer)

def test_getbuffer_delete(self):
# gh-111330: _pyio .close() works and the buffer stays working
if self.ioclass is io.BytesIO:
# gh-111049: _io.BytesIO detach on close would lead to corruption.
# gh-111331: It would be nice to support this.
self.skipTest("io.BytesIO does not support, gh-111049")

# gh-111330, gh-111331: .close() works and the buffer stays working
memio = self.ioclass(b"1234567890")
buf = memio.getbuffer()
self.assertEqual(bytes(buf), b"1234567890")
Expand All @@ -489,6 +481,21 @@ def test_getbuffer_delete(self):
buf[3:6] = b"abc"
self.assertEqual(bytes(buf), b"123abc7890")
self.assertRaises(ValueError, memio.getbuffer)
self.assertRaises(ValueError, memio.getvalue)
del buf
support.gc_collect()
memio.close()

def test_getbuffer_del(self):
# gh-111330, gh-111331: deleting the BytesIO which has an exported
# buffer does not emit an unraisable exception.
memio = self.ioclass(b"1234567890")
buf = memio.getbuffer()
with support.catch_unraisable_exception() as cm:
del memio
support.gc_collect()
self.assertIsNone(cm.unraisable)
self.assertEqual(bytes(buf), b"1234567890")

def test_getbuffer_empty(self):
memio = self.ioclass()
Expand All @@ -513,15 +520,13 @@ def test_getbuffer_gc_collect(self):
a = [buf]
a.append(a)

# gh-111330: _pyio GC with exports should pass.
# gh-111330, gh-111331: no unraisable exception is emitted.
with support.catch_unraisable_exception() as cm:
del memio
self.assertIsNone(cm.unraisable)
del buf
del a
# The C implementation emits an unraisable exception.
with support.catch_unraisable_exception():
del buf
del a
gc.collect()
self.assertIsNone(cm.unraisable)
self.assertIsNone(memiowr())
self.assertIsNone(bufwr())

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Closing a :class:`io.BytesIO` object which has exported buffers no longer
fails with :exc:`BufferError`. The exported buffers keep the data alive and
stay usable. As a result, destroying or garbage collecting such object no
longer emits an unraisable exception.
14 changes: 11 additions & 3 deletions Modules/_io/bytesio.c
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ typedef struct {
* Py_REFCNT(buf) == 1, exports == 0.
* Py_REFCNT(buf) > 1. exports == 0,
first modification or export causes the internal buffer copying.
* exports > 0. Py_REFCNT(buf) == 1, any modifications are forbidden.
* exports > 0. Any modifications are forbidden. Every exported buffer
keeps a reference to buf, so it outlives closing of the bytesio object.
*/

static int
Expand Down Expand Up @@ -925,7 +926,7 @@ static PyObject *
_io_BytesIO_close_impl(bytesio *self)
/*[clinic end generated code: output=1471bb9411af84a0 input=34ce76d8bd17a23b]*/
{
CHECK_EXPORTS(self);
/* The exported buffers keep the internal buffer alive. */
Py_CLEAR(self->buf);
Py_RETURN_NONE;
}
Expand Down Expand Up @@ -1281,6 +1282,9 @@ bytesiobuf_getbuffer_lock_held(PyObject *op, Py_buffer *view, int flags)

_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(b);

if (check_closed(b)) {
return -1;
}
if (FT_ATOMIC_LOAD_SSIZE_RELAXED(b->exports) == 0 && SHARED_BUF(b)) {
if (unshare_buffer_lock_held(b, b->string_size) < 0)
return -1;
Expand All @@ -1290,6 +1294,9 @@ bytesiobuf_getbuffer_lock_held(PyObject *op, Py_buffer *view, int flags)
(void)PyBuffer_FillInfo(view, op,
PyBytes_AS_STRING(b->buf), b->string_size,
0, flags);
/* Keep the internal buffer alive: the bytesio object can be closed
while the buffer is exported. */
view->internal = Py_NewRef(b->buf);
FT_ATOMIC_ADD_SSIZE(b->exports, 1);
return 0;
}
Expand All @@ -1311,11 +1318,12 @@ bytesiobuf_getbuffer(PyObject *op, Py_buffer *view, int flags)
}

static void
bytesiobuf_releasebuffer(PyObject *op, Py_buffer *Py_UNUSED(view))
bytesiobuf_releasebuffer(PyObject *op, Py_buffer *view)
{
bytesiobuf *obj = bytesiobuf_CAST(op);
bytesio *b = bytesio_CAST(obj->source);
FT_ATOMIC_ADD_SSIZE(b->exports, -1);
Py_CLEAR(view->internal);
}

static int
Expand Down
Loading