From 02c1abfc54954ec89a0c728823efde5ce7918a0f Mon Sep 17 00:00:00 2001 From: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com> Date: Tue, 21 Oct 2025 18:33:30 +0100 Subject: [PATCH] gh-69528: Distinguish between file modes "wb+" and "rb+" (GH-137834) Co-authored-by: Xiang Zhang --- Lib/_pyio.py | 7 ++++++- Lib/test/test_gzip.py | 2 +- Lib/test/test_io/test_fileio.py | 4 ++-- Lib/test/test_io/test_general.py | 4 ++-- Lib/test/test_tempfile.py | 2 +- ...2025-08-15-20-35-30.gh-issue-69528.qc-Eh_.rst | 2 ++ Modules/_io/fileio.c | 16 +++++++++++++--- 7 files changed, 27 insertions(+), 10 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-08-15-20-35-30.gh-issue-69528.qc-Eh_.rst diff --git a/Lib/_pyio.py b/Lib/_pyio.py index 5db8ce9244b5..9ae72743919a 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 @@ class FileIO(RawIOBase): 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 @@ class FileIO(RawIOBase): 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_gzip.py b/Lib/test/test_gzip.py index 9a2e1dd248fe..f14a882d3868 100644 --- a/Lib/test/test_gzip.py +++ b/Lib/test/test_gzip.py @@ -639,7 +639,7 @@ class TestGzip(BaseTest): 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+", "xb+": if "x" in mode: os_helper.unlink(self.filename) with open(self.filename, mode) as f: diff --git a/Lib/test/test_io/test_fileio.py b/Lib/test/test_io/test_fileio.py index e3d54f6315aa..e53c4749f58c 100644 --- a/Lib/test/test_io/test_fileio.py +++ b/Lib/test/test_io/test_fileio.py @@ -567,8 +567,8 @@ class OtherFileTests: # 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/test_general.py b/Lib/test/test_io/test_general.py index 7cea0392d0d2..ac9c5a425d7e 100644 --- a/Lib/test/test_io/test_general.py +++ b/Lib/test/test_io/test_general.py @@ -960,8 +960,8 @@ class MiscIOTest: 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+") + self.assertEqual(f.buffer.raw.mode, "wb+") g = self.open(f.fileno(), "wb", closefd=False) self.assertEqual(g.mode, "wb") diff --git a/Lib/test/test_tempfile.py b/Lib/test/test_tempfile.py index 52b13b98cbcc..7eec34f2f294 100644 --- a/Lib/test/test_tempfile.py +++ b/Lib/test/test_tempfile.py @@ -1386,7 +1386,7 @@ class TestSpooledTemporaryFile(BaseTestCase): 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 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 000000000000..b18781e0dceb --- /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+'``. diff --git a/Modules/_io/fileio.c b/Modules/_io/fileio.c index 9992d48a1d8f..b84c1bd3e22c 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': @@ -1145,10 +1148,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"; -- 2.47.3