Skip to content

Commit e2a0713

Browse files
committed
gh-161055: Fix TextIOWrapper.tell() after next() and flush()
flush() re-enabled tell() while the decoder snapshot was stale from next(), so tell() returned a wrong position. seek() now re-enables tell() itself.
1 parent 410fdda commit e2a0713

4 files changed

Lines changed: 42 additions & 2 deletions

File tree

‎Lib/_pyio.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2271,7 +2271,10 @@ def writable(self):
22712271

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

22762279
def close(self):
22772280
if self.buffer is not None and not self.closed:
@@ -2568,6 +2571,7 @@ def _reset_encoder(position):
25682571
position = self.buffer.seek(0, whence)
25692572
self._set_decoded_chars('')
25702573
self._snapshot = None
2574+
self._telling = self._seekable
25712575
if self._decoder:
25722576
self._decoder.reset()
25732577
_reset_encoder(position)
@@ -2587,6 +2591,7 @@ def _reset_encoder(position):
25872591
self.buffer.seek(start_pos)
25882592
self._set_decoded_chars('')
25892593
self._snapshot = None
2594+
self._telling = self._seekable
25902595

25912596
# Restore the decoder to its state from the safe start point.
25922597
if cookie == 0 and self._decoder:

‎Lib/test/test_io/test_textio.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,29 @@ def test_telling(self):
597597
self.assertEqual(f.tell(), p2)
598598
f.close()
599599

600+
def test_tell_after_next_and_flush(self):
601+
# next() stops updating the decoder snapshot, and flush() must not
602+
# re-enable tell() with that stale snapshot.
603+
data = b"a\n" + b"b" * 10000 + b"\nc\n"
604+
for prepare in ["none", "readline", "seek"]:
605+
with self.subTest(prepare=prepare):
606+
f = self.TextIOWrapper(self.BytesIO(data), encoding="ascii")
607+
if prepare != "none":
608+
self.assertEqual(f.readline(), "a\n")
609+
if prepare == "seek":
610+
f.seek(f.tell())
611+
next(f)
612+
f.flush()
613+
self.assertRaises(OSError, f.tell)
614+
self.assertEqual(list(f)[-1], "c\n")
615+
self.assertEqual(f.tell(), len(data))
616+
f.seek(0)
617+
next(f)
618+
self.assertRaises(OSError, f.tell)
619+
f.seek(2)
620+
self.assertEqual(f.tell(), 2)
621+
self.assertEqual(f.read(3), "bbb")
622+
600623
def test_seeking(self):
601624
chunk_size = _default_chunk_size()
602625
prefix_size = chunk_size - 2
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix :meth:`io.TextIOWrapper.tell` returning a wrong position after
2+
:func:`next` and :meth:`~io.IOBase.flush`. It now raises :exc:`OSError`, as it
3+
does without the :meth:`!flush` call.

‎Modules/_io/textio.c‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2712,6 +2712,7 @@ _io_TextIOWrapper_seek_impl(textio *self, PyObject *cookieObj, int whence)
27122712

27132713
textiowrapper_set_decoded_chars(self, NULL);
27142714
Py_CLEAR(self->snapshot);
2715+
self->telling = self->seekable;
27152716
if (self->decoder) {
27162717
res = PyObject_CallMethodNoArgs(self->decoder, &_Py_ID(reset));
27172718
if (res == NULL)
@@ -2780,6 +2781,7 @@ _io_TextIOWrapper_seek_impl(textio *self, PyObject *cookieObj, int whence)
27802781

27812782
textiowrapper_set_decoded_chars(self, NULL);
27822783
Py_CLEAR(self->snapshot);
2784+
self->telling = self->seekable;
27832785

27842786
/* Restore the decoder to its state from the safe start point. */
27852787
if (self->decoder) {
@@ -3256,7 +3258,14 @@ _io_TextIOWrapper_flush_impl(textio *self)
32563258
{
32573259
CHECK_ATTACHED(self);
32583260
CHECK_CLOSED(self);
3259-
self->telling = self->seekable;
3261+
/* The snapshot is stale after next(), so only re-enable tell() when
3262+
it needs neither the snapshot nor pending decoded text. */
3263+
if (self->snapshot == NULL &&
3264+
(self->decoded_chars == NULL ||
3265+
PyUnicode_GET_LENGTH(self->decoded_chars) == 0))
3266+
{
3267+
self->telling = self->seekable;
3268+
}
32603269
if (_textiowrapper_writeflush(self) < 0)
32613270
return NULL;
32623271
return buffer_callmethod_noargs(self, &_Py_ID(flush));

0 commit comments

Comments
 (0)