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 1/3] 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 5db8ce9244b5ba6..9ae72743919a329 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_gzip.py b/Lib/test/test_gzip.py index 9a2e1dd248fe949..f14a882d3868666 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": + 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 e3d54f6315aade6..e53c4749f58cf22 100644 --- a/Lib/test/test_io/test_fileio.py +++ b/Lib/test/test_io/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/test_general.py b/Lib/test/test_io/test_general.py index 7cea0392d0d261e..ac9c5a425d7ea2a 100644 --- a/Lib/test/test_io/test_general.py +++ b/Lib/test/test_io/test_general.py @@ -960,8 +960,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+") + 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 52b13b98cbcce5c..7eec34f2f294ada 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 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 000000000000000..b18781e0dceb8c5 --- /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 9992d48a1d8fc7a..b84c1bd3e22c187 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"; From a8edca62fc6d44d16c7f86d49421be1a5ebea3e5 Mon Sep 17 00:00:00 2001 From: Emma Smith Date: Tue, 21 Oct 2025 14:48:29 -0700 Subject: [PATCH 2/3] gh-132835: Add defensive NULL checks to MRO resolution (GH-134763) Currently, there are a few places where tp_mro could theoretically become NULL, but do not in practice. This commit adds defensive checks for NULL values to ensure that any changes do not introduce a crash and that state invariants are upheld. The assertions added in this commit are all instances where a NULL value would get passed to something not expecting a NULL, so it is better to catch an assertion failure than crash later on. There are a few cases where it is OK for the return of lookup_tp_mro to be NULL, such as when passed to is_subtype_with_mro, which handles this explicitly. --- Objects/typeobject.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 721d48712446a0f..6cc6d366a3598c4 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -1780,7 +1780,7 @@ static int recurse_down_subclasses(PyTypeObject *type, PyObject *name, // Compute tp_mro for this type and all of its subclasses. This // is called after __bases__ is assigned to an existing type. static int -mro_hierarchy(PyTypeObject *type, PyObject *temp) +mro_hierarchy_for_complete_type(PyTypeObject *type, PyObject *temp) { ASSERT_TYPE_LOCK_HELD(); @@ -1791,6 +1791,7 @@ mro_hierarchy(PyTypeObject *type, PyObject *temp) return res; } PyObject *new_mro = lookup_tp_mro(type); + assert(new_mro != NULL); PyObject *tuple; if (old_mro != NULL) { @@ -1835,7 +1836,7 @@ mro_hierarchy(PyTypeObject *type, PyObject *temp) Py_ssize_t n = PyList_GET_SIZE(subclasses); for (Py_ssize_t i = 0; i < n; i++) { PyTypeObject *subclass = _PyType_CAST(PyList_GET_ITEM(subclasses, i)); - res = mro_hierarchy(subclass, temp); + res = mro_hierarchy_for_complete_type(subclass, temp); if (res < 0) { break; } @@ -1926,7 +1927,7 @@ type_set_bases_unlocked(PyTypeObject *type, PyObject *new_bases, PyTypeObject *b if (temp == NULL) { goto bail; } - if (mro_hierarchy(type, temp) < 0) { + if (mro_hierarchy_for_complete_type(type, temp) < 0) { goto undo; } Py_DECREF(temp); @@ -3432,6 +3433,7 @@ mro_implementation_unlocked(PyTypeObject *type) */ PyTypeObject *base = _PyType_CAST(PyTuple_GET_ITEM(bases, 0)); PyObject *base_mro = lookup_tp_mro(base); + assert(base_mro != NULL); Py_ssize_t k = PyTuple_GET_SIZE(base_mro); PyObject *result = PyTuple_New(k + 1); if (result == NULL) { @@ -3466,9 +3468,12 @@ mro_implementation_unlocked(PyTypeObject *type) return NULL; } + PyObject *mro_to_merge; for (Py_ssize_t i = 0; i < n; i++) { PyTypeObject *base = _PyType_CAST(PyTuple_GET_ITEM(bases, i)); - to_merge[i] = lookup_tp_mro(base); + mro_to_merge = lookup_tp_mro(base); + assert(mro_to_merge != NULL); + to_merge[i] = mro_to_merge; } to_merge[n] = bases; @@ -8998,6 +9003,7 @@ type_ready_inherit(PyTypeObject *type) // Inherit slots PyObject *mro = lookup_tp_mro(type); + assert(mro != NULL); Py_ssize_t n = PyTuple_GET_SIZE(mro); for (Py_ssize_t i = 1; i < n; i++) { PyObject *b = PyTuple_GET_ITEM(mro, i); From 29b38b7aae884c14085a918282ea7f0798ed7a2a Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 22 Oct 2025 07:12:26 +0800 Subject: [PATCH 3/3] gh-55258: Skip tests of stdout status on mobile platforms (#140401) Skip tests of stdout status on mobile platforms. --- Lib/test/test_support.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index 92909519636cf32..d69328a6a6ac90c 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -96,6 +96,8 @@ def test_get_attribute(self): self.test_get_attribute) self.assertRaises(unittest.SkipTest, support.get_attribute, self, "foo") + @unittest.skipIf(support.is_android or support.is_apple_mobile, + 'Mobile platforms redirect stdout to system log') def test_get_original_stdout(self): if isinstance(sys.stdout, io.StringIO): # gh-55258: When --junit-xml is used, stdout is a StringIO: