Skip to content
Merged
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
18 changes: 18 additions & 0 deletions Doc/library/io.rst
Original file line number Diff line number Diff line change
Expand Up @@ -908,6 +908,24 @@ Text I/O

Whether line buffering is enabled.

.. attribute:: write_through

Whether writes are passed immediately to the underlying binary
buffer.

.. versionadded:: 3.7

.. method:: reconfigure(*, line_buffering=None, write_through=None)

Reconfigure this text stream using new settings for *line_buffering*
and *write_through*. Passing ``None`` as an argument will retain
the current setting for that parameter.

This method does an implicit stream flush before setting the
new parameters.

.. versionadded:: 3.7


.. class:: StringIO(initial_value='', newline='\\n')

Expand Down
23 changes: 22 additions & 1 deletion Lib/_pyio.py
Original file line number Diff line number Diff line change
Expand Up @@ -1943,7 +1943,6 @@ def __init__(self, buffer, encoding=None, errors=None, newline=None,
raise ValueError("invalid errors: %r" % errors)

self._buffer = buffer
self._line_buffering = line_buffering
self._encoding = encoding
self._errors = errors
self._readuniversal = not newline
Expand All @@ -1969,6 +1968,12 @@ def __init__(self, buffer, encoding=None, errors=None, newline=None,
# Sometimes the encoder doesn't exist
pass

self._configure(line_buffering, write_through)

def _configure(self, line_buffering=False, write_through=False):
self._line_buffering = line_buffering
self._write_through = write_through

# self._snapshot is either None, or a tuple (dec_flags, next_input)
# where dec_flags is the second (integer) item of the decoder state
# and next_input is the chunk of input bytes that comes next after the
Expand Down Expand Up @@ -2007,10 +2012,26 @@ def errors(self):
def line_buffering(self):
return self._line_buffering

@property
def write_through(self):
return self._write_through

@property
def buffer(self):
return self._buffer

def reconfigure(self, *, line_buffering=None, write_through=None):
"""Reconfigure the text stream with new parameters.

This also flushes the stream.
"""
if line_buffering is None:
line_buffering = self.line_buffering
if write_through is None:
write_through = self.write_through
self.flush()
self._configure(line_buffering, write_through)

def seekable(self):
if self.closed:
raise ValueError("I/O operation on closed file.")
Expand Down
51 changes: 51 additions & 0 deletions Lib/test/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -2440,6 +2440,7 @@ def test_detach(self):
self.assertEqual(t.encoding, "ascii")
self.assertEqual(t.errors, "strict")
self.assertFalse(t.line_buffering)
self.assertFalse(t.write_through)

def test_repr(self):
raw = self.BytesIO("hello".encode("utf-8"))
Expand Down Expand Up @@ -2482,6 +2483,33 @@ def test_line_buffering(self):
t.write("A\rB")
self.assertEqual(r.getvalue(), b"XY\nZA\rB")

def test_reconfigure_line_buffering(self):
r = self.BytesIO()
b = self.BufferedWriter(r, 1000)
t = self.TextIOWrapper(b, newline="\n", line_buffering=False)
t.write("AB\nC")
self.assertEqual(r.getvalue(), b"")

t.reconfigure(line_buffering=True) # implicit flush
self.assertEqual(r.getvalue(), b"AB\nC")
t.write("DEF\nG")
self.assertEqual(r.getvalue(), b"AB\nCDEF\nG")
t.write("H")
self.assertEqual(r.getvalue(), b"AB\nCDEF\nG")
t.reconfigure(line_buffering=False) # implicit flush
self.assertEqual(r.getvalue(), b"AB\nCDEF\nGH")
t.write("IJ")
self.assertEqual(r.getvalue(), b"AB\nCDEF\nGH")

# Keeping default value
t.reconfigure()
t.reconfigure(line_buffering=None)
self.assertEqual(t.line_buffering, False)
t.reconfigure(line_buffering=True)
t.reconfigure()
t.reconfigure(line_buffering=None)
self.assertEqual(t.line_buffering, True)

def test_default_encoding(self):
old_environ = dict(os.environ)
try:
Expand Down Expand Up @@ -3164,6 +3192,29 @@ def write(self, *args, **kwargs):
self.assertTrue(write_called)
self.assertEqual(rawio.getvalue(), data * 11) # all flushed

def test_reconfigure_write_through(self):
raw = self.MockRawIO([])
t = self.TextIOWrapper(raw, encoding='ascii', newline='\n')
t.write('1')
t.reconfigure(write_through=True) # implied flush
self.assertEqual(t.write_through, True)
self.assertEqual(b''.join(raw._write_stack), b'1')
t.write('23')
self.assertEqual(b''.join(raw._write_stack), b'123')
t.reconfigure(write_through=False)
self.assertEqual(t.write_through, False)
t.write('45')
t.flush()
self.assertEqual(b''.join(raw._write_stack), b'12345')
# Keeping default value
t.reconfigure()
t.reconfigure(write_through=None)
self.assertEqual(t.write_through, False)
t.reconfigure(write_through=True)
t.reconfigure()
t.reconfigure(write_through=None)
self.assertEqual(t.write_through, True)

def test_read_nonbytes(self):
# Issue #17106
# Crash when underlying read() returns non-bytes
Expand Down
3 changes: 3 additions & 0 deletions Misc/NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,9 @@ Extension Modules
Library
-------

- bpo-30526: Add TextIOWrapper.reconfigure() and a TextIOWrapper.write_through
attribute.

- bpo-30245: Fix possible overflow when organize struct.pack_into
error message. Patch by Yuan Liu.

Expand Down
37 changes: 36 additions & 1 deletion Modules/_io/clinic/textio.c.h
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,41 @@ _io_TextIOWrapper___init__(PyObject *self, PyObject *args, PyObject *kwargs)
return return_value;
}

PyDoc_STRVAR(_io_TextIOWrapper_reconfigure__doc__,
"reconfigure($self, /, *, line_buffering=None, write_through=None)\n"
"--\n"
"\n"
"Reconfigure the text stream with new parameters.\n"
"\n"
"This also does an implicit stream flush.");

#define _IO_TEXTIOWRAPPER_RECONFIGURE_METHODDEF \
{"reconfigure", (PyCFunction)_io_TextIOWrapper_reconfigure, METH_FASTCALL, _io_TextIOWrapper_reconfigure__doc__},

static PyObject *
_io_TextIOWrapper_reconfigure_impl(textio *self,
PyObject *line_buffering_obj,
PyObject *write_through_obj);

static PyObject *
_io_TextIOWrapper_reconfigure(textio *self, PyObject **args, Py_ssize_t nargs, PyObject *kwnames)
{
PyObject *return_value = NULL;
static const char * const _keywords[] = {"line_buffering", "write_through", NULL};
static _PyArg_Parser _parser = {"|$OO:reconfigure", _keywords, 0};
PyObject *line_buffering_obj = Py_None;
PyObject *write_through_obj = Py_None;

if (!_PyArg_ParseStackAndKeywords(args, nargs, kwnames, &_parser,
&line_buffering_obj, &write_through_obj)) {
goto exit;
}
return_value = _io_TextIOWrapper_reconfigure_impl(self, line_buffering_obj, write_through_obj);

exit:
return return_value;
}

PyDoc_STRVAR(_io_TextIOWrapper_detach__doc__,
"detach($self, /)\n"
"--\n"
Expand Down Expand Up @@ -480,4 +515,4 @@ _io_TextIOWrapper_close(textio *self, PyObject *Py_UNUSED(ignored))
{
return _io_TextIOWrapper_close_impl(self);
}
/*[clinic end generated code: output=8e5c21c88c7c70bc input=a9049054013a1b77]*/
/*[clinic end generated code: output=7d0dc8eae4b725a1 input=a9049054013a1b77]*/
60 changes: 60 additions & 0 deletions Modules/_io/textio.c
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,64 @@ _io_TextIOWrapper___init___impl(textio *self, PyObject *buffer,
return -1;
}

/* Return *default_value* if ob is None, 0 if ob is false, 1 if ob is true,
* -1 on error.
*/
static int
convert_optional_bool(PyObject *obj, int default_value)
{
long v;
if (obj == Py_None) {
v = default_value;
}
else {
v = PyLong_AsLong(obj);
if (v == -1 && PyErr_Occurred())
return -1;
}
return v != 0;
}


/*[clinic input]
_io.TextIOWrapper.reconfigure
*
line_buffering as line_buffering_obj: object = None
write_through as write_through_obj: object = None

Reconfigure the text stream with new parameters.

This also does an implicit stream flush.

[clinic start generated code]*/

static PyObject *
_io_TextIOWrapper_reconfigure_impl(textio *self,
PyObject *line_buffering_obj,
PyObject *write_through_obj)
/*[clinic end generated code: output=7cdf79e7001e2856 input=baade27ecb9db7bc]*/
{
int line_buffering;
int write_through;
PyObject *res;

line_buffering = convert_optional_bool(line_buffering_obj,
self->line_buffering);
write_through = convert_optional_bool(write_through_obj,
self->write_through);
if (line_buffering < 0 || write_through < 0) {
return NULL;
}
res = PyObject_CallMethodObjArgs((PyObject *) self, _PyIO_str_flush, NULL);
Py_XDECREF(res);
if (res == NULL) {
return NULL;
}
self->line_buffering = line_buffering;
self->write_through = write_through;
Py_RETURN_NONE;
}

static int
textiowrapper_clear(textio *self)
{
Expand Down Expand Up @@ -2839,6 +2897,7 @@ PyTypeObject PyIncrementalNewlineDecoder_Type = {

static PyMethodDef textiowrapper_methods[] = {
_IO_TEXTIOWRAPPER_DETACH_METHODDEF
_IO_TEXTIOWRAPPER_RECONFIGURE_METHODDEF
_IO_TEXTIOWRAPPER_WRITE_METHODDEF
_IO_TEXTIOWRAPPER_READ_METHODDEF
_IO_TEXTIOWRAPPER_READLINE_METHODDEF
Expand All @@ -2862,6 +2921,7 @@ static PyMemberDef textiowrapper_members[] = {
{"encoding", T_OBJECT, offsetof(textio, encoding), READONLY},
{"buffer", T_OBJECT, offsetof(textio, buffer), READONLY},
{"line_buffering", T_BOOL, offsetof(textio, line_buffering), READONLY},
{"write_through", T_BOOL, offsetof(textio, write_through), READONLY},
{"_finalizing", T_BOOL, offsetof(textio, finalizing), 0},
{NULL}
};
Expand Down