diff --git a/Lib/_pyio.py b/Lib/_pyio.py index cf4ef04f37d26c..370b518f1e5e5a 100644 --- a/Lib/_pyio.py +++ b/Lib/_pyio.py @@ -528,7 +528,7 @@ def nreadahead(): return 1 n = (readahead.find(b"\n") + 1) or len(readahead) if size >= 0: - n = min(n, size) + n = min(n, size - len(res)) return n else: def nreadahead(): diff --git a/Lib/test/test_io/test_general.py b/Lib/test/test_io/test_general.py index 085ed3ea6a95ee..d12c5535b263ef 100644 --- a/Lib/test/test_io/test_general.py +++ b/Lib/test/test_io/test_general.py @@ -319,6 +319,22 @@ def test_readline(self): with self.open(os_helper.TESTFN, "r", encoding="utf-8") as f: self.assertRaises(TypeError, f.readline, 5.3) + def test_readline_limit_with_short_read(self): + class Reader(self.IOBase): + data = b"abcdef" + + def peek(self, size): + return self.data + + def read(self, size): + data = self.data[:min(size, 2)] + self.data = self.data[len(data):] + return data + + reader = Reader() + self.assertEqual(reader.readline(3), b"abc") + self.assertEqual(reader.data, b"def") + def test_readline_nonsizeable(self): # Issue #30061 # Crash when readline() returns an object without __len__ diff --git a/Misc/NEWS.d/next/Library/2026-10-20-00-00-00.gh-issue-159739.4c9783.rst b/Misc/NEWS.d/next/Library/2026-10-20-00-00-00.gh-issue-159739.4c9783.rst new file mode 100644 index 00000000000000..b65ac4cede106b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-20-00-00-00.gh-issue-159739.4c9783.rst @@ -0,0 +1,2 @@ +Fix :meth:`io.IOBase.readline` returning more bytes than requested when a +stream supports :meth:`!peek` and returns short reads. diff --git a/Modules/_io/iobase.c b/Modules/_io/iobase.c index 5b9b9c856431b0..2b482687e85c5b 100644 --- a/Modules/_io/iobase.c +++ b/Modules/_io/iobase.c @@ -653,8 +653,9 @@ _io__IOBase_readline_impl(PyObject *self, Py_ssize_t limit) Py_ssize_t n = 0; const char *buf = PyBytes_AS_STRING(readahead); if (limit >= 0) { + Py_ssize_t remaining = limit - PyBytesWriter_GetSize(writer); do { - if (n >= PyBytes_GET_SIZE(readahead) || n >= limit) + if (n >= PyBytes_GET_SIZE(readahead) || n >= remaining) break; if (buf[n++] == '\n') break;