Skip to content

Commit eed8635

Browse files
committed
gh-159739: Recheck text stream state after newline replacement
1 parent 5721af0 commit eed8635

3 files changed

Lines changed: 49 additions & 6 deletions

File tree

‎Lib/test/test_io/test_textio.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1457,6 +1457,32 @@ def replace(self, *args):
14571457
with self.assertRaisesRegex(TypeError, 'replace.*str'):
14581458
stream.write(Text('a\n'))
14591459

1460+
def test_write_newline_replace_changes_stream(self):
1461+
def fail_init(stream, buffer):
1462+
with self.assertRaises(LookupError):
1463+
stream.__init__(buffer, encoding='nonexistent-encoding')
1464+
1465+
actions = (
1466+
(lambda stream, buffer: stream.close(), ValueError, 'closed'),
1467+
(lambda stream, buffer: stream.detach(), ValueError, 'detached'),
1468+
(lambda stream, buffer: stream.__init__(buffer),
1469+
self.UnsupportedOperation, 'not writable'),
1470+
(fail_init, ValueError, 'uninitialized'),
1471+
)
1472+
for action, error, message in actions:
1473+
with self.subTest(message=message):
1474+
with (self.BytesIO() as raw,
1475+
self.BufferedReader(self.BytesIO()) as readonly):
1476+
stream = self.TextIOWrapper(raw, newline='\r\n')
1477+
1478+
class Text(str):
1479+
def replace(self, *args):
1480+
action(stream, readonly)
1481+
return 'a\r\n'
1482+
1483+
with self.assertRaisesRegex(error, message):
1484+
stream.write(Text('a\n'))
1485+
14601486
def test_chunk_size(self):
14611487
t = self.TextIOWrapper(self.BytesIO(), encoding="utf-8")
14621488
self.assertGreater(t._CHUNK_SIZE, 0)
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
Raise :exc:`TypeError` if a string subclass returns a non-string during newline translation in :class:`io.TextIOWrapper`.
1+
Raise :exc:`TypeError` if a string subclass returns a non-string during newline
2+
translation in :class:`io.TextIOWrapper`. Recheck the stream state if the
3+
replacement callback closes, detaches or reinitializes the stream.

‎Modules/_io/textio.c‎

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1730,6 +1730,19 @@ _textiowrapper_writeflush(textio *self)
17301730
return 0;
17311731
}
17321732

1733+
/* Return borrowed Py_None on success, or NULL on error. */
1734+
static PyObject *
1735+
textiowrapper_check_write(textio *self)
1736+
{
1737+
CHECK_ATTACHED(self);
1738+
CHECK_CLOSED(self);
1739+
1740+
if (self->encoder == NULL) {
1741+
return _unsupported(self->state, "not writable");
1742+
}
1743+
return Py_None;
1744+
}
1745+
17331746
/*[clinic input]
17341747
@critical_section
17351748
_io.TextIOWrapper.write
@@ -1747,11 +1760,8 @@ _io_TextIOWrapper_write_impl(textio *self, PyObject *text)
17471760
int haslf = 0;
17481761
int needflush = 0, text_needflush = 0;
17491762

1750-
CHECK_ATTACHED(self);
1751-
CHECK_CLOSED(self);
1752-
1753-
if (self->encoder == NULL) {
1754-
return _unsupported(self->state, "not writable");
1763+
if (textiowrapper_check_write(self) == NULL) {
1764+
return NULL;
17551765
}
17561766

17571767
Py_INCREF(text);
@@ -1774,6 +1784,11 @@ _io_TextIOWrapper_write_impl(textio *self, PyObject *text)
17741784
Py_DECREF(newtext);
17751785
return NULL;
17761786
}
1787+
/* replace() may have closed, detached or reinitialized the stream. */
1788+
if (textiowrapper_check_write(self) == NULL) {
1789+
Py_DECREF(newtext);
1790+
return NULL;
1791+
}
17771792
text = newtext;
17781793
}
17791794

0 commit comments

Comments
 (0)