From 998b89020456db591be41e6529b04f4bc8c8181f Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Sat, 8 Aug 2026 11:11:23 -0400 Subject: [PATCH] gh-111330: Fix GC of _pyio.BytesIO with exports (GH-152335) Swap out the buffer rather than using `.clear()` which will raise an exception if there are exports. --- Lib/_pyio.py | 2 +- Lib/test/test_io/test_memoryio.py | 27 ++++++++++++++++--- ...-06-26-13-00-46.gh-issue-111330.DBcCXA.rst | 2 ++ 3 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-06-26-13-00-46.gh-issue-111330.DBcCXA.rst diff --git a/Lib/_pyio.py b/Lib/_pyio.py index 1118b54633b7cc..ac301180d284fa 100644 --- a/Lib/_pyio.py +++ b/Lib/_pyio.py @@ -921,7 +921,7 @@ def getbuffer(self): def close(self): if self._buffer is not None: - self._buffer.clear() + self._buffer = bytearray() super().close() def read(self, size=-1): diff --git a/Lib/test/test_io/test_memoryio.py b/Lib/test/test_io/test_memoryio.py index f53525d77b7dba..026dff23fe7ce2 100644 --- a/Lib/test/test_io/test_memoryio.py +++ b/Lib/test/test_io/test_memoryio.py @@ -457,7 +457,9 @@ def test_getbuffer(self): # raises a BufferError. self.assertRaises(BufferError, memio.write, b'x' * 100) self.assertRaises(BufferError, memio.truncate) - self.assertRaises(BufferError, memio.close) + # 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" @@ -471,6 +473,23 @@ def test_getbuffer(self): memio.close() 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") + + memio = self.ioclass(b"1234567890") + buf = memio.getbuffer() + self.assertEqual(bytes(buf), b"1234567890") + memio.close() + self.assertTrue(memio.closed) + self.assertEqual(bytes(buf), b"1234567890") + buf[3:6] = b"abc" + self.assertEqual(bytes(buf), b"123abc7890") + self.assertRaises(ValueError, memio.getbuffer) + def test_getbuffer_empty(self): memio = self.ioclass() buf = memio.getbuffer() @@ -493,9 +512,11 @@ def test_getbuffer_gc_collect(self): # Create a reference loop. a = [buf] a.append(a) - # The Python implementation emits an unraisable exception. - with support.catch_unraisable_exception(): + + # gh-111330: _pyio GC with exports should pass. + with support.catch_unraisable_exception() as cm: del memio + self.assertIsNone(cm.unraisable) del buf del a # The C implementation emits an unraisable exception. diff --git a/Misc/NEWS.d/next/Library/2026-06-26-13-00-46.gh-issue-111330.DBcCXA.rst b/Misc/NEWS.d/next/Library/2026-06-26-13-00-46.gh-issue-111330.DBcCXA.rst new file mode 100644 index 00000000000000..597cd8d526da58 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-26-13-00-46.gh-issue-111330.DBcCXA.rst @@ -0,0 +1,2 @@ +Update pure-Python :class:`io.BytesIO` to close cleanly when the data has an +export such as a :class:`memoryview`.