From 889201f84ddffbbe0e5f101feffffffc3aa3e962 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Fri, 15 Aug 2025 20:37:06 +0200 Subject: [PATCH 1/7] Commit ____ Co-authored-by: Xiang Zhang --- Lib/_pyio.py | 7 ++++++- Lib/test/test_fileio.py | 4 ++-- Lib/test/test_io.py | 4 ++-- ...2025-08-15-20-35-30.gh-issue-69528.qc-Eh_.rst | 1 + Modules/_io/fileio.c | 16 +++++++++++++--- 5 files changed, 24 insertions(+), 8 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-08-15-20-35-30.gh-issue-69528.qc-Eh_.rst diff --git a/Lib/_pyio.py b/Lib/_pyio.py index 5db8ce9244b5ba..9ae72743919a32 100644 --- a/Lib/_pyio.py +++ b/Lib/_pyio.py @@ -1498,6 +1498,7 @@ class FileIO(RawIOBase): _writable = False _appending = False _seekable = None + _truncate = False _closefd = True def __init__(self, file, mode='r', closefd=True, opener=None): @@ -1553,6 +1554,7 @@ def __init__(self, file, mode='r', closefd=True, opener=None): flags = 0 elif 'w' in mode: self._writable = True + self._truncate = True flags = os.O_CREAT | os.O_TRUNC elif 'a' in mode: self._writable = True @@ -1877,7 +1879,10 @@ def mode(self): return 'ab' elif self._readable: if self._writable: - return 'rb+' + if self._truncate: + return 'wb+' + else: + return 'rb+' else: return 'rb' else: diff --git a/Lib/test/test_fileio.py b/Lib/test/test_fileio.py index e3d54f6315aade..e53c4749f58cf2 100644 --- a/Lib/test/test_fileio.py +++ b/Lib/test/test_fileio.py @@ -567,8 +567,8 @@ def testModeStrings(self): # test that the mode attribute is correct for various mode strings # given as init args try: - for modes in [('w', 'wb'), ('wb', 'wb'), ('wb+', 'rb+'), - ('w+b', 'rb+'), ('a', 'ab'), ('ab', 'ab'), + for modes in [('w', 'wb'), ('wb', 'wb'), ('wb+', 'wb+'), + ('w+b', 'wb+'), ('a', 'ab'), ('ab', 'ab'), ('ab+', 'ab+'), ('a+b', 'ab+'), ('r', 'rb'), ('rb', 'rb'), ('rb+', 'rb+'), ('r+b', 'rb+')]: # read modes are last so that TESTFN will exist first diff --git a/Lib/test/test_io.py b/Lib/test/test_io.py index 92be2763e5ed1e..57b95a7667fdb2 100644 --- a/Lib/test/test_io.py +++ b/Lib/test/test_io.py @@ -4337,8 +4337,8 @@ def test_attributes(self): f = self.open(os_helper.TESTFN, "w+", encoding="utf-8") self.assertEqual(f.mode, "w+") - self.assertEqual(f.buffer.mode, "rb+") # Does it really matter? - self.assertEqual(f.buffer.raw.mode, "rb+") + self.assertEqual(f.buffer.mode, "wb+") # Does it really matter? + self.assertEqual(f.buffer.raw.mode, "wb+") g = self.open(f.fileno(), "wb", closefd=False) self.assertEqual(g.mode, "wb") diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-08-15-20-35-30.gh-issue-69528.qc-Eh_.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-15-20-35-30.gh-issue-69528.qc-Eh_.rst new file mode 100644 index 00000000000000..11f1f9cb3723c4 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-15-20-35-30.gh-issue-69528.qc-Eh_.rst @@ -0,0 +1 @@ +:mod:`io`: Fix a bug where the ``'wb+'`` mode was treated as ``'rb+'``. diff --git a/Modules/_io/fileio.c b/Modules/_io/fileio.c index 26537fc6395e9f..909dc89536f790 100644 --- a/Modules/_io/fileio.c +++ b/Modules/_io/fileio.c @@ -70,6 +70,7 @@ typedef struct { unsigned int writable : 1; unsigned int appending : 1; signed int seekable : 2; /* -1 means unknown */ + unsigned int truncate : 1; unsigned int closefd : 1; char finalizing; /* Stat result which was grabbed at file open, useful for optimizing common @@ -209,6 +210,7 @@ fileio_new(PyTypeObject *type, PyObject *args, PyObject *kwds) self->writable = 0; self->appending = 0; self->seekable = -1; + self->truncate = 0; self->stat_atopen = NULL; self->closefd = 1; self->weakreflist = NULL; @@ -341,6 +343,7 @@ _io_FileIO___init___impl(fileio *self, PyObject *nameobj, const char *mode, goto bad_mode; rwa = 1; self->writable = 1; + self->truncate = 1; flags |= O_CREAT | O_TRUNC; break; case 'a': @@ -1156,10 +1159,17 @@ mode_string(fileio *self) return "ab"; } else if (self->readable) { - if (self->writable) - return "rb+"; - else + if (self->writable) { + if (self->truncate) { + return "wb+"; + } + else { + return "rb+"; + } + } + else { return "rb"; + } } else return "wb"; From c355e4fcc46a56f7c8113c024623038cbafd97e6 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Fri, 15 Aug 2025 21:03:20 +0200 Subject: [PATCH 2/7] Fix `test_tempfile` --- Lib/test/test_tempfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_tempfile.py b/Lib/test/test_tempfile.py index 52b13b98cbcce5..7eec34f2f294ad 100644 --- a/Lib/test/test_tempfile.py +++ b/Lib/test/test_tempfile.py @@ -1386,7 +1386,7 @@ def test_properties(self): f.write(b'x') self.assertTrue(f._rolled) - self.assertEqual(f.mode, 'rb+') + self.assertEqual(f.mode, 'wb+') self.assertIsNotNone(f.name) with self.assertRaises(AttributeError): f.newlines From bfab82aef9f0481483422727b0d95164e5440fe8 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sat, 16 Aug 2025 17:36:13 +0200 Subject: [PATCH 3/7] Serhiy's request --- Lib/test/test_gzip.py | 2 ++ Lib/test/test_io.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_gzip.py b/Lib/test/test_gzip.py index 9a2e1dd248fe94..6265f593e642dc 100644 --- a/Lib/test/test_gzip.py +++ b/Lib/test/test_gzip.py @@ -326,6 +326,8 @@ def test_mode(self): os_helper.unlink(self.filename) with gzip.GzipFile(self.filename, 'x') as f: self.assertEqual(f.myfileobj.mode, 'xb') + with gzip.GzipFile(self.filename, 'wb+') as f: + self.assertEqual(f.myfileobj.mode, 'wb+') def test_1647484(self): for mode in ('wb', 'rb'): diff --git a/Lib/test/test_io.py b/Lib/test/test_io.py index 57b95a7667fdb2..03a65d02d28cc5 100644 --- a/Lib/test/test_io.py +++ b/Lib/test/test_io.py @@ -4337,7 +4337,7 @@ def test_attributes(self): f = self.open(os_helper.TESTFN, "w+", encoding="utf-8") self.assertEqual(f.mode, "w+") - self.assertEqual(f.buffer.mode, "wb+") # Does it really matter? + self.assertEqual(f.buffer.mode, "wb+") self.assertEqual(f.buffer.raw.mode, "wb+") g = self.open(f.fileno(), "wb", closefd=False) From 92cf720046218a86ba9608f8f2fa8f167ac46c2d Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sun, 17 Aug 2025 08:36:40 +0200 Subject: [PATCH 4/7] Serhiy's request --- Lib/test/test_gzip.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_gzip.py b/Lib/test/test_gzip.py index 6265f593e642dc..e083cf114d2555 100644 --- a/Lib/test/test_gzip.py +++ b/Lib/test/test_gzip.py @@ -326,8 +326,6 @@ def test_mode(self): os_helper.unlink(self.filename) with gzip.GzipFile(self.filename, 'x') as f: self.assertEqual(f.myfileobj.mode, 'xb') - with gzip.GzipFile(self.filename, 'wb+') as f: - self.assertEqual(f.myfileobj.mode, 'wb+') def test_1647484(self): for mode in ('wb', 'rb'): @@ -582,6 +580,24 @@ def test_fileobj_with_name(self): self.assertIs(f.writable(), False) self.assertIs(f.seekable(), True) + with open(self.filename, "wb+") as raw: + with gzip.GzipFile(fileobj=raw) as f: + f.write(b'something') + self.assertEqual(f.name, raw.name) + self.assertEqual(f.fileno(), raw.fileno()) + self.assertEqual(f.mode, gzip.WRITE) + self.assertIs(f.readable(), False) + self.assertIs(f.writable(), True) + self.assertIs(f.seekable(), True) + self.assertIs(f.closed, False) + self.assertIs(f.closed, True) + self.assertEqual(f.name, raw.name) + self.assertRaises(AttributeError, f.fileno) + self.assertEqual(f.mode, gzip.WRITE) + self.assertIs(f.readable(), False) + self.assertIs(f.writable(), True) + self.assertIs(f.seekable(), True) + def test_fileobj_from_fdopen(self): # Issue #13781: Opening a GzipFile for writing fails when using a # fileobj created with os.fdopen(). From c2e20650eb5d8c174a45bf72fa663ae07ca3caff Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sun, 17 Aug 2025 09:04:47 +0200 Subject: [PATCH 5/7] Move tests --- Lib/test/test_gzip.py | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/Lib/test/test_gzip.py b/Lib/test/test_gzip.py index e083cf114d2555..35eaca6a130baf 100644 --- a/Lib/test/test_gzip.py +++ b/Lib/test/test_gzip.py @@ -580,24 +580,6 @@ def test_fileobj_with_name(self): self.assertIs(f.writable(), False) self.assertIs(f.seekable(), True) - with open(self.filename, "wb+") as raw: - with gzip.GzipFile(fileobj=raw) as f: - f.write(b'something') - self.assertEqual(f.name, raw.name) - self.assertEqual(f.fileno(), raw.fileno()) - self.assertEqual(f.mode, gzip.WRITE) - self.assertIs(f.readable(), False) - self.assertIs(f.writable(), True) - self.assertIs(f.seekable(), True) - self.assertIs(f.closed, False) - self.assertIs(f.closed, True) - self.assertEqual(f.name, raw.name) - self.assertRaises(AttributeError, f.fileno) - self.assertEqual(f.mode, gzip.WRITE) - self.assertIs(f.readable(), False) - self.assertIs(f.writable(), True) - self.assertIs(f.seekable(), True) - def test_fileobj_from_fdopen(self): # Issue #13781: Opening a GzipFile for writing fails when using a # fileobj created with os.fdopen(). @@ -657,7 +639,7 @@ def test_fileobj_mode(self): with open(self.filename, mode) as f: with gzip.GzipFile(fileobj=f) as g: self.assertEqual(g.mode, gzip.READ) - for mode in "wb", "ab", "xb": + for mode in "wb", "ab", "xb", "wb+", "ab+": if "x" in mode: os_helper.unlink(self.filename) with open(self.filename, mode) as f: From bc40ecd3205fc635b2a4aaa564864d6c0fce2f64 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sun, 17 Aug 2025 09:32:18 +0200 Subject: [PATCH 6/7] Add xb+ test --- Lib/test/test_gzip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_gzip.py b/Lib/test/test_gzip.py index 35eaca6a130baf..f14a882d386866 100644 --- a/Lib/test/test_gzip.py +++ b/Lib/test/test_gzip.py @@ -639,7 +639,7 @@ def test_fileobj_mode(self): with open(self.filename, mode) as f: with gzip.GzipFile(fileobj=f) as g: self.assertEqual(g.mode, gzip.READ) - for mode in "wb", "ab", "xb", "wb+", "ab+": + for mode in "wb", "ab", "xb", "wb+", "ab+", "xb+": if "x" in mode: os_helper.unlink(self.filename) with open(self.filename, mode) as f: From f21f44288155290ca13d95375b4612d3c0c21c4c Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sun, 17 Aug 2025 15:41:46 +0200 Subject: [PATCH 7/7] NEWS nits --- .../2025-08-15-20-35-30.gh-issue-69528.qc-Eh_.rst | 1 - .../next/Library/2025-08-15-20-35-30.gh-issue-69528.qc-Eh_.rst | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) delete mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-08-15-20-35-30.gh-issue-69528.qc-Eh_.rst create mode 100644 Misc/NEWS.d/next/Library/2025-08-15-20-35-30.gh-issue-69528.qc-Eh_.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-08-15-20-35-30.gh-issue-69528.qc-Eh_.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-15-20-35-30.gh-issue-69528.qc-Eh_.rst deleted file mode 100644 index 11f1f9cb3723c4..00000000000000 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-08-15-20-35-30.gh-issue-69528.qc-Eh_.rst +++ /dev/null @@ -1 +0,0 @@ -:mod:`io`: Fix a bug where the ``'wb+'`` mode was treated as ``'rb+'``. diff --git a/Misc/NEWS.d/next/Library/2025-08-15-20-35-30.gh-issue-69528.qc-Eh_.rst b/Misc/NEWS.d/next/Library/2025-08-15-20-35-30.gh-issue-69528.qc-Eh_.rst new file mode 100644 index 00000000000000..b18781e0dceb8c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-08-15-20-35-30.gh-issue-69528.qc-Eh_.rst @@ -0,0 +1,2 @@ +The :attr:`~io.FileIO.mode` attribute of files opened in the ``'wb+'`` mode is +now ``'wb+'`` instead of ``'rb+'``.