Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion Lib/_pyio.py
Original file line number Diff line number Diff line change
Expand Up @@ -2271,7 +2271,10 @@ def writable(self):

def flush(self):
self.buffer.flush()
self._telling = self._seekable
# The snapshot is stale after next(), so only re-enable tell() when
# it needs neither the snapshot nor pending decoded text.
if self._snapshot is None and not self._decoded_chars:
self._telling = self._seekable

def close(self):
if self.buffer is not None and not self.closed:
Expand Down Expand Up @@ -2568,6 +2571,7 @@ def _reset_encoder(position):
position = self.buffer.seek(0, whence)
self._set_decoded_chars('')
self._snapshot = None
self._telling = self._seekable
if self._decoder:
self._decoder.reset()
_reset_encoder(position)
Expand All @@ -2587,6 +2591,7 @@ def _reset_encoder(position):
self.buffer.seek(start_pos)
self._set_decoded_chars('')
self._snapshot = None
self._telling = self._seekable

# Restore the decoder to its state from the safe start point.
if cookie == 0 and self._decoder:
Expand Down
23 changes: 23 additions & 0 deletions Lib/test/test_io/test_textio.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,29 @@ def test_telling(self):
self.assertEqual(f.tell(), p2)
f.close()

def test_tell_after_next_and_flush(self):
# next() stops updating the decoder snapshot, and flush() must not
# re-enable tell() with that stale snapshot.
data = b"a\n" + b"b" * 10000 + b"\nc\n"
for prepare in ["none", "readline", "seek"]:
with self.subTest(prepare=prepare):
f = self.TextIOWrapper(self.BytesIO(data), encoding="ascii")
if prepare != "none":
self.assertEqual(f.readline(), "a\n")
if prepare == "seek":
f.seek(f.tell())
next(f)
f.flush()
self.assertRaises(OSError, f.tell)
self.assertEqual(list(f)[-1], "c\n")
self.assertEqual(f.tell(), len(data))
f.seek(0)
next(f)
self.assertRaises(OSError, f.tell)
f.seek(2)
self.assertEqual(f.tell(), 2)
self.assertEqual(f.read(3), "bbb")

def test_seeking(self):
chunk_size = _default_chunk_size()
prefix_size = chunk_size - 2
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix :meth:`io.TextIOWrapper.tell` returning a wrong position after
:func:`next` and :meth:`~io.IOBase.flush`. It now raises :exc:`OSError`, as it
does without the :meth:`!flush` call.
11 changes: 10 additions & 1 deletion Modules/_io/textio.c
Original file line number Diff line number Diff line change
Expand Up @@ -2712,6 +2712,7 @@ _io_TextIOWrapper_seek_impl(textio *self, PyObject *cookieObj, int whence)

textiowrapper_set_decoded_chars(self, NULL);
Py_CLEAR(self->snapshot);
self->telling = self->seekable;
if (self->decoder) {
res = PyObject_CallMethodNoArgs(self->decoder, &_Py_ID(reset));
if (res == NULL)
Expand Down Expand Up @@ -2780,6 +2781,7 @@ _io_TextIOWrapper_seek_impl(textio *self, PyObject *cookieObj, int whence)

textiowrapper_set_decoded_chars(self, NULL);
Py_CLEAR(self->snapshot);
self->telling = self->seekable;

/* Restore the decoder to its state from the safe start point. */
if (self->decoder) {
Expand Down Expand Up @@ -3256,7 +3258,14 @@ _io_TextIOWrapper_flush_impl(textio *self)
{
CHECK_ATTACHED(self);
CHECK_CLOSED(self);
self->telling = self->seekable;
/* The snapshot is stale after next(), so only re-enable tell() when
it needs neither the snapshot nor pending decoded text. */
if (self->snapshot == NULL &&
(self->decoded_chars == NULL ||
PyUnicode_GET_LENGTH(self->decoded_chars) == 0))
{
self->telling = self->seekable;
}
if (_textiowrapper_writeflush(self) < 0)
return NULL;
return buffer_callmethod_noargs(self, &_Py_ID(flush));
Expand Down
Loading