diff --git a/Lib/test/test_io/test_bufferedio.py b/Lib/test/test_io/test_bufferedio.py index e83dd0d4e28d00..d6c8ac4d62a0da 100644 --- a/Lib/test/test_io/test_bufferedio.py +++ b/Lib/test/test_io/test_bufferedio.py @@ -1473,6 +1473,30 @@ def test_interleaved_readline_write(self): f.flush() self.assertEqual(raw.getvalue(), b'1b\n2def\n3\n') + def test_append_write(self): + # Uses a real FileIO so that append behavior is reproduced accurately + with self.FileIO(os_helper.TESTFN, 'wb') as f: + f.write(b'test test') + + with self.FileIO(os_helper.TESTFN, 'ab+') as raw: + with self.tp(raw) as f: + self.assertEqual(f.tell(), 9) + f.write(b'A') + f.seek(0) + self.assertEqual(f.read(), b'test testA') + f.seek(0) + self.assertEqual(f.read(1), b't') + self.assertEqual(f.write(b'B'), 1) + f.seek(0) + + # This read previously returned b'tBst testA' but that is + # incorect if the underlying raw file is in append mode; + # see bpo-20082 + self.assertEqual(f.read(), b'test testAB') + f.flush() + f.seek(0) + self.assertEqual(f.read(), b'test testAB') + # You can't construct a BufferedRandom over a non-seekable stream. test_unseekable = None diff --git a/Modules/_io/bufferedio.c b/Modules/_io/bufferedio.c index 0fdae7b2d21004..24bbd75a3bd65e 100644 --- a/Modules/_io/bufferedio.c +++ b/Modules/_io/bufferedio.c @@ -228,6 +228,7 @@ typedef struct { int detached; int readable; int writable; + int appending; char finalizing; /* True if this is a vanilla Buffered object (rather than a user derived @@ -1350,6 +1351,37 @@ _io__Buffered_tell_impl(buffered *self) return PyLong_FromOff_t(pos); } +static PyObject * +_buffered_seek_unlocked(buffered *self, Py_off_t target, int whence) +{ + PyObject *res = NULL; + + if (self->writable) { + res = _bufferedwriter_flush_unlocked(self); + if (res == NULL) { + return res; + } + Py_CLEAR(res); + } + + /* TODO: align on block boundary and read buffer if needed? */ + if (whence == 1) { + target -= RAW_OFFSET(self); + } + + Py_off_t n = _buffered_raw_seek(self, target, whence); + if (n == -1) { + return res; + } + self->raw_pos = -1; + res = PyLong_FromOff_t(n); + if (res != NULL && self->readable) { + _bufferedreader_reset_buf(self); + } + + return res; +} + /*[clinic input] @critical_section _io._Buffered.seek @@ -1362,7 +1394,7 @@ static PyObject * _io__Buffered_seek_impl(buffered *self, PyObject *targetobj, int whence) /*[clinic end generated code: output=7ae0e8dc46efdefb input=b5a12be70e0ad07b]*/ { - Py_off_t target, n; + Py_off_t target; PyObject *res = NULL; CHECK_INITIALIZED(self) @@ -1430,25 +1462,8 @@ _io__Buffered_seek_impl(buffered *self, PyObject *targetobj, int whence) return NULL; /* Fallback: invoke raw seek() method and clear buffer */ - if (self->writable) { - res = _bufferedwriter_flush_unlocked(self); - if (res == NULL) - goto end; - Py_CLEAR(res); - } + res = _buffered_seek_unlocked(self, target, whence); - /* TODO: align on block boundary and read buffer if needed? */ - if (whence == 1) - target -= RAW_OFFSET(self); - n = _buffered_raw_seek(self, target, whence); - if (n == -1) - goto end; - self->raw_pos = -1; - res = PyLong_FromOff_t(n); - if (res != NULL && self->readable) - _bufferedreader_reset_buf(self); - -end: LEAVE_BUFFERED(self) return res; } @@ -1926,6 +1941,26 @@ _bufferedwriter_reset_buf(buffered *self) self->write_end = -1; } +static void +_bufferedwriter_set_append(buffered *self) +{ + PyObject *mode = NULL; + if (PyObject_GetOptionalAttr(self->raw, &_Py_ID(mode), &mode) < 0) { + /* Raw fileobj has no mode string so as far as we can know it has + normal write behavior */ + self->appending = 0; + } else if (mode != NULL && PyUnicode_Check(mode)) { + if (PyUnicode_FindChar(mode, 'a', 0, + PyUnicode_GET_LENGTH(mode), 1) >= 0) { + self->appending = 1; + } + else { + self->appending = 0; + } + Py_DECREF(mode); + } +} + /*[clinic input] _io.BufferedWriter.__init__ raw: object @@ -1956,6 +1991,8 @@ _io_BufferedWriter___init___impl(buffered *self, PyObject *raw, self->readable = 0; self->writable = 1; + _bufferedwriter_set_append(self); + self->buffer_size = buffer_size; if (_buffered_init(self) < 0) return -1; @@ -2025,14 +2062,16 @@ _bufferedwriter_flush_unlocked(buffered *self) if (!VALID_WRITE_BUFFER(self) || self->write_pos == self->write_end) goto end; - /* First, rewind */ - rewind = RAW_OFFSET(self) + (self->pos - self->write_pos); - if (rewind != 0) { - n = _buffered_raw_seek(self, -rewind, 1); - if (n < 0) { - goto error; + /* First, rewind unless raw file is in append mode */ + if (!self->appending) { + rewind = RAW_OFFSET(self) + (self->pos - self->write_pos); + if (rewind != 0) { + n = _buffered_raw_seek(self, -rewind, 1); + if (n < 0) { + goto error; + } + self->raw_pos -= rewind; } - self->raw_pos -= rewind; } while (self->write_pos < self->write_end) { n = _bufferedwriter_raw_write(self, @@ -2111,6 +2150,15 @@ _io_BufferedWriter_write_impl(buffered *self, Py_buffer *buffer) self->pos = 0; self->raw_pos = 0; } + + if (self->appending) { + res = _buffered_seek_unlocked(self, 0, SEEK_END); + if (res == NULL) { + goto error; + } + Py_DECREF(res); + } + avail = Py_SAFE_DOWNCAST(self->buffer_size - self->pos, Py_off_t, Py_ssize_t); if (buffer->len <= avail && buffer->len < self->buffer_size) { memcpy(self->buffer + self->pos, buffer->buf, buffer->len); @@ -2504,6 +2552,8 @@ _io_BufferedRandom___init___impl(buffered *self, PyObject *raw, self->readable = 1; self->writable = 1; + _bufferedwriter_set_append(self); + if (_buffered_init(self) < 0) return -1; _bufferedreader_reset_buf(self);