Skip to content

Commit

Permalink
[3.12] gh-119506: fix _io.TextIOWrapper.write() write during flush (G…
Browse files Browse the repository at this point in the history
…H-119507) (#119965)

gh-119506: fix `_io.TextIOWrapper.write()` write during flush (GH-119507)
(cherry picked from commit 52586f9)

Co-authored-by: Radislav Chugunov <[email protected]>
Co-authored-by: Inada Naoki <[email protected]>
  • Loading branch information
3 people authored Jun 19, 2024
1 parent 57f955f commit cde976d
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 9 deletions.
22 changes: 22 additions & 0 deletions Lib/test/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -4066,6 +4066,28 @@ def write(self, data):
t.write("x"*chunk_size)
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):
io = pyio
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix :meth:`!io.TextIOWrapper.write` method breaks internal buffer when the method is called again during flushing internal buffer.
31 changes: 22 additions & 9 deletions Modules/_io/textio.c
Original file line number Diff line number Diff line change
Expand Up @@ -1723,16 +1723,26 @@ _io_TextIOWrapper_write_impl(textio *self, PyObject *text)
bytes_len = PyBytes_GET_SIZE(b);
}

if (self->pending_bytes == NULL) {
self->pending_bytes_count = 0;
self->pending_bytes = b;
}
else if (self->pending_bytes_count + bytes_len > self->chunk_size) {
// Prevent to concatenate more than chunk_size data.
if (_textiowrapper_writeflush(self) < 0) {
Py_DECREF(b);
return NULL;
// We should avoid concatinating huge data.
// Flush the buffer before adding b to the buffer if b is not small.
// https://github.com/python/cpython/issues/87426
if (bytes_len >= self->chunk_size) {
// _textiowrapper_writeflush() calls buffer.write().
// self->pending_bytes can be appended during buffer->write()
// or other thread.
// We need to loop until buffer becomes empty.
// 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;
}
else if (!PyList_CheckExact(self->pending_bytes)) {
Expand All @@ -1741,6 +1751,9 @@ _io_TextIOWrapper_write_impl(textio *self, PyObject *text)
Py_DECREF(b);
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, 1, b);
self->pending_bytes = list;
Expand Down

0 comments on commit cde976d

Please sign in to comment.