From 800bd2687d2e348bc74ce7f26f879403e21383ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hilmi=20Tuna=20Say=C4=B1n?= Date: Wed, 4 Nov 2026 01:09:42 +0100 Subject: [PATCH] gh-161275: Fix TextIOWrapper.truncate() with no argument truncate() delegated to the buffer without an explicit position, so the stream was resized to wherever the buffer had read ahead to rather than to the current position of the text stream. _pyio passes tell() through and already behaves correctly, so this only changes the C implementation. --- Lib/test/test_io/test_textio.py | 22 +++++++++++++++++++ ...-11-04-09-40-00.gh-issue-161275.Rt4Xw8.rst | 3 +++ Modules/_io/textio.c | 14 ++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-11-04-09-40-00.gh-issue-161275.Rt4Xw8.rst diff --git a/Lib/test/test_io/test_textio.py b/Lib/test/test_io/test_textio.py index a18386d68569ae..ff4a6742401069 100644 --- a/Lib/test/test_io/test_textio.py +++ b/Lib/test/test_io/test_textio.py @@ -577,6 +577,28 @@ def multi_line_test(self, f, enc): rlines.append((pos, line)) self.assertEqual(rlines, wlines) + def test_truncate_after_read(self): + # gh-161275: truncate() without an argument resizes the stream to the + # current position. The buffer may have read ahead of the text + # stream, so the buffer's own position must not be used. + with self.open(os_helper.TESTFN, "w", encoding="utf-8") as f: + f.write("abcde") + with self.open(os_helper.TESTFN, "r+", encoding="utf-8") as f: + self.assertEqual(f.read(2), "ab") + self.assertEqual(f.tell(), 2) + self.assertEqual(f.truncate(), 2) + self.assertEqual(f.tell(), 2) + with self.open(os_helper.TESTFN, "r", encoding="utf-8") as f: + self.assertEqual(f.read(), "ab") + + # Also with a file larger than the read-ahead chunk. + with self.open(os_helper.TESTFN, "w", encoding="utf-8") as f: + f.write("0123456789" * 2000) + with self.open(os_helper.TESTFN, "r+", encoding="utf-8") as f: + self.assertEqual(f.read(10), "0123456789") + self.assertEqual(f.truncate(), 10) + self.assertEqual(os.path.getsize(os_helper.TESTFN), 10) + def test_telling(self): f = self.open(os_helper.TESTFN, "w+", encoding="utf-8") p0 = f.tell() diff --git a/Misc/NEWS.d/next/Library/2026-11-04-09-40-00.gh-issue-161275.Rt4Xw8.rst b/Misc/NEWS.d/next/Library/2026-11-04-09-40-00.gh-issue-161275.Rt4Xw8.rst new file mode 100644 index 00000000000000..c8f61417443e36 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-11-04-09-40-00.gh-issue-161275.Rt4Xw8.rst @@ -0,0 +1,3 @@ +:meth:`~io.IOBase.truncate` on an :class:`io.TextIOWrapper` with no argument +resized the stream to the position of the underlying buffer, which may have +read ahead, instead of to the current position of the text stream. diff --git a/Modules/_io/textio.c b/Modules/_io/textio.c index 4ef5251d451a4c..b0231819046875 100644 --- a/Modules/_io/textio.c +++ b/Modules/_io/textio.c @@ -3112,6 +3112,20 @@ _io_TextIOWrapper_truncate_impl(textio *self, PyObject *pos) return NULL; } + if (pos == Py_None) { + /* The buffer may have read ahead of this stream's position, so the + buffer's own position must not be used here. Resize to the + position reported by tell(), as _pyio already does. */ + PyObject *cookie = _io_TextIOWrapper_tell_impl(self); + if (cookie == NULL) { + return NULL; + } + PyObject *res = buffer_callmethod_onearg(self, &_Py_ID(truncate), + cookie); + Py_DECREF(cookie); + return res; + } + return buffer_callmethod_onearg(self, &_Py_ID(truncate), pos); }