From 9e3dd1a83ea63f54a22d04b89454863d03b0f463 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Fri, 26 Jun 2026 13:11:15 -0700 Subject: [PATCH 1/2] gh-111330: Fix Gc of _pyio.BytesIO with exports Swap out the buffer rather than using `.clear()` which will raise an exception if there are epxorts. --- Lib/_pyio.py | 2 +- Lib/test/test_io/test_memoryio.py | 6 ++++-- .../Library/2026-06-26-13-00-46.gh-issue-111330.DBcCXA.rst | 2 ++ 3 files changed, 7 insertions(+), 3 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 1118b54633b7cc1..ac301180d284fa9 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 5a93d2634580461..2a682fca7ceb97d 100644 --- a/Lib/test/test_io/test_memoryio.py +++ b/Lib/test/test_io/test_memoryio.py @@ -493,9 +493,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 000000000000000..597cd8d526da588 --- /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`. From af73f2000f20316ba3b219d08561c4d0f65974b2 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Fri, 26 Jun 2026 14:27:20 -0700 Subject: [PATCH 2/2] Split BytesIO.close test for _pyio and _io --- Lib/test/test_io/test_memoryio.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_io/test_memoryio.py b/Lib/test/test_io/test_memoryio.py index 2a682fca7ceb97d..e44e9a6633e05dc 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()