gh-119506: fix `_io.TextIOWrapper.write()` write during flush (#119507)

Co-authored-by: Inada Naoki <songofacandy@gmail.com>
This commit is contained in:
Radislav Chugunov 2024-06-03 10:47:36 +03:00 committed by GitHub
parent 3ea9b92086
commit 52586f930f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 45 additions and 9 deletions

View File

@ -4016,6 +4016,28 @@ class CTextIOWrapperTest(TextIOWrapperTest):
t.write("x"*chunk_size) t.write("x"*chunk_size)
self.assertEqual([b"abcdef", b"ghi", b"x"*chunk_size], buf._write_stack) self.assertEqual([b"abcdef", b"ghi", b"x"*chunk_size], buf._write_stack)
def test_issue119506(self):
chunk_size = 8192
class MockIO(self.MockRawIO):
written = False
def write(self, data):
if not self.written:
self.written = True
t.write("middle")
return super().write(data)
buf = MockIO()
t = self.TextIOWrapper(buf)
t.write("abc")
t.write("def")
# writing data which size >= chunk_size cause flushing buffer before write.
t.write("g" * chunk_size)
t.flush()
self.assertEqual([b"abcdef", b"middle", b"g"*chunk_size],
buf._write_stack)
class PyTextIOWrapperTest(TextIOWrapperTest): class PyTextIOWrapperTest(TextIOWrapperTest):
io = pyio io = pyio

View File

@ -0,0 +1 @@
Fix :meth:`!io.TextIOWrapper.write` method breaks internal buffer when the method is called again during flushing internal buffer.

View File

@ -1719,16 +1719,26 @@ _io_TextIOWrapper_write_impl(textio *self, PyObject *text)
bytes_len = PyBytes_GET_SIZE(b); bytes_len = PyBytes_GET_SIZE(b);
} }
if (self->pending_bytes == NULL) { // We should avoid concatinating huge data.
self->pending_bytes_count = 0; // Flush the buffer before adding b to the buffer if b is not small.
self->pending_bytes = b; // https://github.com/python/cpython/issues/87426
} if (bytes_len >= self->chunk_size) {
else if (self->pending_bytes_count + bytes_len > self->chunk_size) { // _textiowrapper_writeflush() calls buffer.write().
// Prevent to concatenate more than chunk_size data. // self->pending_bytes can be appended during buffer->write()
if (_textiowrapper_writeflush(self) < 0) { // or other thread.
Py_DECREF(b); // We need to loop until buffer becomes empty.
return NULL; // https://github.com/python/cpython/issues/118138
// https://github.com/python/cpython/issues/119506
while (self->pending_bytes != NULL) {
if (_textiowrapper_writeflush(self) < 0) {
Py_DECREF(b);
return NULL;
}
} }
}
if (self->pending_bytes == NULL) {
assert(self->pending_bytes_count == 0);
self->pending_bytes = b; self->pending_bytes = b;
} }
else if (!PyList_CheckExact(self->pending_bytes)) { else if (!PyList_CheckExact(self->pending_bytes)) {
@ -1737,6 +1747,9 @@ _io_TextIOWrapper_write_impl(textio *self, PyObject *text)
Py_DECREF(b); Py_DECREF(b);
return NULL; return NULL;
} }
// Since Python 3.12, allocating GC object won't trigger GC and release
// GIL. See https://github.com/python/cpython/issues/97922
assert(!PyList_CheckExact(self->pending_bytes));
PyList_SET_ITEM(list, 0, self->pending_bytes); PyList_SET_ITEM(list, 0, self->pending_bytes);
PyList_SET_ITEM(list, 1, b); PyList_SET_ITEM(list, 1, b);
self->pending_bytes = list; self->pending_bytes = list;