diff --git a/Lib/_pyio.py b/Lib/_pyio.py index cf4ef04f37d26cc..9cbc75a4cf93535 100644 --- a/Lib/_pyio.py +++ b/Lib/_pyio.py @@ -727,6 +727,8 @@ def readinto1(self, b): def _readinto(self, b, read1): if not isinstance(b, memoryview): b = memoryview(b) + if b.readonly: + raise TypeError("readinto() argument must be read-write bytes-like object") b = b.cast('B') if read1: diff --git a/Lib/test/test_io/test_bufferedio.py b/Lib/test/test_io/test_bufferedio.py index e83dd0d4e28d006..da7917d3a72ee6c 100644 --- a/Lib/test/test_io/test_bufferedio.py +++ b/Lib/test/test_io/test_bufferedio.py @@ -12,6 +12,7 @@ from test import support from test.support import check_sanitizer, os_helper, threading_helper from .utils import byteslike, CTestCase, PyTestCase +from .utils import check_readinto_readonly_buffer import io # C implementation. @@ -313,6 +314,10 @@ def test_readinto(self): self.assertEqual(bufio.readinto(b), 1) self.assertEqual(b, b"cb") + def test_readinto_readonly_buffer(self): + check_readinto_readonly_buffer( + self, lambda data: self.tp(self.BytesIO(data)), prefill=True) + def test_readinto1(self): buffer_size = 10 rawio = self.MockRawIO((b"abc", b"de", b"fgh", b"jkl")) diff --git a/Lib/test/test_io/test_memoryio.py b/Lib/test/test_io/test_memoryio.py index b6f3aa93e7aa43f..8bb5ad33bf2a763 100644 --- a/Lib/test/test_io/test_memoryio.py +++ b/Lib/test/test_io/test_memoryio.py @@ -5,6 +5,7 @@ import unittest from test import support +from .utils import check_readinto_readonly_buffer import gc import io @@ -568,6 +569,9 @@ def test_readinto(self): memio.readinto(b) self.assertEqual(b, b"") + def test_readinto_readonly_buffer(self): + check_readinto_readonly_buffer(self, self.ioclass, empty_and_eof=True) + def test_relative_seek(self): buf = self.buftype("1234567890") memio = self.ioclass(buf) diff --git a/Lib/test/test_io/utils.py b/Lib/test/test_io/utils.py index dde49337a24f0b9..f0414a44a7ce9fe 100644 --- a/Lib/test/test_io/utils.py +++ b/Lib/test/test_io/utils.py @@ -2,6 +2,7 @@ import threading import time import unittest +from itertools import product import io # C implementation of io import _pyio as pyio # Python implementation of io @@ -26,6 +27,30 @@ def byteslike(*pos, **kw): return obj +def check_readinto_readonly_buffer(testcase, stream_factory, *, + empty_and_eof=False, prefill=False): + data = b"abcdef" + buffers = [b"xxx", memoryview(b"xxx")] + positions = [0, 4] + if empty_and_eof: + buffers.extend([b"", memoryview(b"")]) + positions.append(10) + methods = ("readinto", "readinto1") + prefilled = (False, True) if prefill else (False,) + for method, buffer, position, buffered in product( + methods, buffers, positions, prefilled): + with testcase.subTest(method=method, buffer=buffer, position=position, + buffered=buffered): + with stream_factory(data) as stream: + stream.seek(position) + if buffered: + stream.peek(1) + op = getattr(stream, method) + testcase.assertRaises(TypeError, op, buffer) + testcase.assertEqual(stream.tell(), position) + testcase.assertEqual(stream.read(), data[position:]) + + class MockRawIOWithoutRead: """A RawIO implementation without read(), so as to exercise the default RawIO.read() which calls readinto().""" diff --git a/Misc/NEWS.d/next/Library/2026-10-07-06-28-00.gh-issue-158952.tR7kLp.rst b/Misc/NEWS.d/next/Library/2026-10-07-06-28-00.gh-issue-158952.tR7kLp.rst new file mode 100644 index 000000000000000..06383eab7766ff9 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-07-06-28-00.gh-issue-158952.tR7kLp.rst @@ -0,0 +1,3 @@ +The pure Python implementation of :meth:`io.BufferedIOBase.readinto` and +:meth:`~io.BufferedIOBase.readinto1` now rejects read-only buffers before +reading input, without changing the stream position.