diff --git a/Doc/library/io.rst b/Doc/library/io.rst index c8396915723..4f6ee5a4686 100644 --- a/Doc/library/io.rst +++ b/Doc/library/io.rst @@ -361,6 +361,17 @@ I/O Base Classes :class:`BufferedIOBase` provides or overrides these methods in addition to those from :class:`IOBase`: + .. method:: detach() + + Separate the underlying raw stream from the buffer and return it. + + After the raw stream has been detached, the buffer is in an unusable + state. + + Some buffers, like :class:`BytesIO`, do not have the concept of a single + raw stream to return from this method. They raise + :exc:`UnsupportedOperation`. + .. method:: read([n]) Read and return up to *n* bytes. If the argument is omitted, ``None``, or @@ -547,7 +558,9 @@ Buffered Streams *max_buffer_size* is unused and deprecated. - :class:`BufferedRWPair` implements all of :class:`BufferedIOBase`\'s methods. + :class:`BufferedRWPair` implements all of :class:`BufferedIOBase`\'s methods + except for :meth:`~BufferedIOBase.detach`, which raises + :exc:`UnsupportedOperation`. .. class:: BufferedRandom(raw[, buffer_size[, max_buffer_size]]) @@ -588,6 +601,17 @@ Text I/O A string, a tuple of strings, or ``None``, indicating the newlines translated so far. + .. method:: detach() + + Separate the underlying buffer from the :class:`TextIOBase` and return it. + + After the underlying buffer has been detached, the :class:`TextIOBase` is + in an unusable state. + + Some :class:`TextIOBase` implementations, like :class:`StringIO`, may not + have the concept of an underlying buffer and calling this method will + raise :exc:`UnsupportedOperation`. + .. method:: read(n) Read and return at most *n* characters from the stream as a single diff --git a/Lib/_pyio.py b/Lib/_pyio.py index e580366df7c..e3e7c3da00c 100644 --- a/Lib/_pyio.py +++ b/Lib/_pyio.py @@ -642,6 +642,15 @@ class BufferedIOBase(IOBase): """ self._unsupported("write") + def detach(self) -> None: + """ + Separate the underlying raw stream from the buffer and return it. + + After the raw stream has been detached, the buffer is in an unusable + state. + """ + self._unsupported("detach") + io.BufferedIOBase.register(BufferedIOBase) @@ -689,13 +698,21 @@ class _BufferedIOMixin(BufferedIOBase): self.raw.flush() def close(self): - if not self.closed: + if not self.closed and self.raw is not None: try: self.flush() except IOError: pass # If flush() fails, just give up self.raw.close() + def detach(self): + if self.raw is None: + raise ValueError("raw stream already detached") + self.flush() + raw = self.raw + self.raw = None + return raw + ### Inquiries ### def seekable(self): @@ -1236,6 +1253,15 @@ class TextIOBase(IOBase): """ self._unsupported("readline") + def detach(self) -> None: + """ + Separate the underlying buffer from the TextIOBase and return it. + + After the underlying buffer has been detached, the TextIO is in an + unusable state. + """ + self._unsupported("detach") + @property def encoding(self): """Subclasses should override.""" @@ -1448,11 +1474,12 @@ class TextIOWrapper(TextIOBase): self._telling = self._seekable def close(self): - try: - self.flush() - except IOError: - pass # If flush() fails, just give up - self.buffer.close() + if self.buffer is not None: + try: + self.flush() + except IOError: + pass # If flush() fails, just give up + self.buffer.close() @property def closed(self): @@ -1647,6 +1674,14 @@ class TextIOWrapper(TextIOBase): self.seek(pos) return self.buffer.truncate() + def detach(self): + if self.buffer is None: + raise ValueError("buffer is already detached") + self.flush() + buffer = self.buffer + self.buffer = None + return buffer + def seek(self, cookie, whence=0): if self.closed: raise ValueError("tell on closed file") @@ -1865,3 +1900,7 @@ class StringIO(TextIOWrapper): @property def encoding(self): return None + + def detach(self): + # This doesn't make sense on StringIO. + self._unsupported("detach") diff --git a/Lib/test/test_io.py b/Lib/test/test_io.py index a8c878eadfb..1a525dc9135 100644 --- a/Lib/test/test_io.py +++ b/Lib/test/test_io.py @@ -526,6 +526,12 @@ class PyIOTest(IOTest): class CommonBufferedTests: # Tests common to BufferedReader, BufferedWriter and BufferedRandom + def test_detach(self): + raw = self.MockRawIO() + buf = self.tp(raw) + self.assertIs(buf.detach(), raw) + self.assertRaises(ValueError, buf.detach) + def test_fileno(self): rawio = self.MockRawIO() bufio = self.tp(rawio) @@ -811,6 +817,14 @@ class BufferedWriterTest(unittest.TestCase, CommonBufferedTests): bufio.flush() self.assertEquals(b"".join(rawio._write_stack), b"abcghi") + def test_detach_flush(self): + raw = self.MockRawIO() + buf = self.tp(raw) + buf.write(b"howdy!") + self.assertFalse(raw._write_stack) + buf.detach() + self.assertEqual(raw._write_stack, [b"howdy!"]) + def test_write(self): # Write to the buffered IO but don't overflow the buffer. writer = self.MockRawIO() @@ -1052,6 +1066,10 @@ class BufferedRWPairTest(unittest.TestCase): pair = self.tp(self.MockRawIO(), self.MockRawIO()) self.assertFalse(pair.closed) + def test_detach(self): + pair = self.tp(self.MockRawIO(), self.MockRawIO()) + self.assertRaises(self.UnsupportedOperation, pair.detach) + def test_constructor_max_buffer_size_deprecation(self): with support.check_warnings() as w: warnings.simplefilter("always", DeprecationWarning) @@ -1480,6 +1498,19 @@ class TextIOWrapperTest(unittest.TestCase): self.assertRaises(TypeError, t.__init__, b, newline=42) self.assertRaises(ValueError, t.__init__, b, newline='xyzzy') + def test_detach(self): + r = self.BytesIO() + b = self.BufferedWriter(r) + t = self.TextIOWrapper(b) + self.assertIs(t.detach(), b) + + t = self.TextIOWrapper(b, encoding="ascii") + t.write("howdy") + self.assertFalse(r.getvalue()) + t.detach() + self.assertEqual(r.getvalue(), b"howdy") + self.assertRaises(ValueError, t.detach) + def test_repr(self): raw = self.BytesIO("hello".encode("utf-8")) b = self.BufferedReader(raw) diff --git a/Lib/test/test_memoryio.py b/Lib/test/test_memoryio.py index ad04613daf4..d94d9dd552a 100644 --- a/Lib/test/test_memoryio.py +++ b/Lib/test/test_memoryio.py @@ -57,6 +57,10 @@ class MemorySeekTestMixin: class MemoryTestMixin: + def test_detach(self): + buf = self.ioclass() + self.assertRaises(self.UnsupportedOperation, buf.detach) + def write_ops(self, f, t): self.assertEqual(f.write(t("blah.")), 5) self.assertEqual(f.seek(0), 0) @@ -336,6 +340,9 @@ class MemoryTestMixin: class PyBytesIOTest(MemoryTestMixin, MemorySeekTestMixin, unittest.TestCase): + + UnsupportedOperation = pyio.UnsupportedOperation + @staticmethod def buftype(s): return s.encode("ascii") @@ -413,6 +420,7 @@ class PyBytesIOTest(MemoryTestMixin, MemorySeekTestMixin, unittest.TestCase): class PyStringIOTest(MemoryTestMixin, MemorySeekTestMixin, unittest.TestCase): buftype = str ioclass = pyio.StringIO + UnsupportedOperation = pyio.UnsupportedOperation EOF = "" # TextIO-specific behaviour. @@ -518,9 +526,11 @@ class PyStringIOTest(MemoryTestMixin, MemorySeekTestMixin, unittest.TestCase): class CBytesIOTest(PyBytesIOTest): ioclass = io.BytesIO + UnsupportedOperation = io.UnsupportedOperation class CStringIOTest(PyStringIOTest): ioclass = io.StringIO + UnsupportedOperation = io.UnsupportedOperation # XXX: For the Python version of io.StringIO, this is highly # dependent on the encoding used for the underlying buffer. diff --git a/Misc/NEWS b/Misc/NEWS index d6ea20999ff..4319a4a9ed2 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -12,6 +12,10 @@ What's New in Python 3.1 beta 1? Core and Builtins ----------------- +- Issue #5883: In the io module, the BufferedIOBase and TextIOBase ABCs have + received a new method, detach(). detach() disconnects the underlying stream + from the buffer or text IO and returns it. + - Issue #5859: Remove switch from '%f' to '%g'-style formatting for floats with absolute value over 1e50. Also remove length restrictions for float formatting: '%.67f' % 12.34 and '%.120e' % diff --git a/Modules/_io/bufferedio.c b/Modules/_io/bufferedio.c index 3d175c7c8cf..2c652078ee6 100644 --- a/Modules/_io/bufferedio.c +++ b/Modules/_io/bufferedio.c @@ -73,6 +73,18 @@ BufferedIOBase_unsupported(const char *message) return NULL; } +PyDoc_STRVAR(BufferedIOBase_detach_doc, + "Disconnect this buffer from its underlying raw stream and return it.\n" + "\n" + "After the raw stream has been detached, the buffer is in an unusable\n" + "state.\n"); + +static PyObject * +BufferedIOBase_detach(PyObject *self) +{ + return BufferedIOBase_unsupported("detach"); +} + PyDoc_STRVAR(BufferedIOBase_read_doc, "Read and return up to n bytes.\n" "\n" @@ -127,6 +139,7 @@ BufferedIOBase_write(PyObject *self, PyObject *args) static PyMethodDef BufferedIOBase_methods[] = { + {"detach", (PyCFunction)BufferedIOBase_detach, METH_NOARGS, BufferedIOBase_detach_doc}, {"read", BufferedIOBase_read, METH_VARARGS, BufferedIOBase_read_doc}, {"read1", BufferedIOBase_read1, METH_VARARGS, BufferedIOBase_read1_doc}, {"readinto", BufferedIOBase_readinto, METH_VARARGS, NULL}, @@ -181,6 +194,7 @@ typedef struct { PyObject *raw; int ok; /* Initialized? */ + int detached; int readable; int writable; @@ -260,15 +274,25 @@ typedef struct { #define CHECK_INITIALIZED(self) \ if (self->ok <= 0) { \ - PyErr_SetString(PyExc_ValueError, \ - "I/O operation on uninitialized object"); \ + if (self->detached) { \ + PyErr_SetString(PyExc_ValueError, \ + "raw stream has been detached"); \ + } else { \ + PyErr_SetString(PyExc_ValueError, \ + "I/O operation on uninitialized object"); \ + } \ return NULL; \ } #define CHECK_INITIALIZED_INT(self) \ if (self->ok <= 0) { \ - PyErr_SetString(PyExc_ValueError, \ - "I/O operation on uninitialized object"); \ + if (self->detached) { \ + PyErr_SetString(PyExc_ValueError, \ + "raw stream has been detached"); \ + } else { \ + PyErr_SetString(PyExc_ValueError, \ + "I/O operation on uninitialized object"); \ + } \ return -1; \ } @@ -430,6 +454,24 @@ end: return res; } +/* detach */ + +static PyObject * +BufferedIOMixin_detach(BufferedObject *self, PyObject *args) +{ + PyObject *raw, *res; + CHECK_INITIALIZED(self) + res = PyObject_CallMethodObjArgs((PyObject *)self, _PyIO_str_flush, NULL); + if (res == NULL) + return NULL; + Py_DECREF(res); + raw = self->raw; + self->raw = NULL; + self->detached = 1; + self->ok = 0; + return raw; +} + /* Inquiries */ static PyObject * @@ -1101,6 +1143,7 @@ BufferedReader_init(BufferedObject *self, PyObject *args, PyObject *kwds) PyObject *raw; self->ok = 0; + self->detached = 0; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|n:BufferedReader", kwlist, &raw, &buffer_size)) { @@ -1387,6 +1430,7 @@ _BufferedReader_peek_unlocked(BufferedObject *self, Py_ssize_t n) static PyMethodDef BufferedReader_methods[] = { /* BufferedIOMixin methods */ + {"detach", (PyCFunction)BufferedIOMixin_detach, METH_NOARGS}, {"flush", (PyCFunction)BufferedIOMixin_flush, METH_NOARGS}, {"close", (PyCFunction)BufferedIOMixin_close, METH_NOARGS}, {"seekable", (PyCFunction)BufferedIOMixin_seekable, METH_NOARGS}, @@ -1499,6 +1543,7 @@ BufferedWriter_init(BufferedObject *self, PyObject *args, PyObject *kwds) PyObject *raw; self->ok = 0; + self->detached = 0; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|nn:BufferedReader", kwlist, &raw, &buffer_size, &max_buffer_size)) { @@ -1745,6 +1790,7 @@ error: static PyMethodDef BufferedWriter_methods[] = { /* BufferedIOMixin methods */ {"close", (PyCFunction)BufferedIOMixin_close, METH_NOARGS}, + {"detach", (PyCFunction)BufferedIOMixin_detach, METH_NOARGS}, {"seekable", (PyCFunction)BufferedIOMixin_seekable, METH_NOARGS}, {"readable", (PyCFunction)BufferedIOMixin_readable, METH_NOARGS}, {"writable", (PyCFunction)BufferedIOMixin_writable, METH_NOARGS}, @@ -2089,6 +2135,7 @@ BufferedRandom_init(BufferedObject *self, PyObject *args, PyObject *kwds) PyObject *raw; self->ok = 0; + self->detached = 0; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|nn:BufferedReader", kwlist, &raw, &buffer_size, &max_buffer_size)) { @@ -2128,6 +2175,7 @@ BufferedRandom_init(BufferedObject *self, PyObject *args, PyObject *kwds) static PyMethodDef BufferedRandom_methods[] = { /* BufferedIOMixin methods */ {"close", (PyCFunction)BufferedIOMixin_close, METH_NOARGS}, + {"detach", (PyCFunction)BufferedIOMixin_detach, METH_NOARGS}, {"seekable", (PyCFunction)BufferedIOMixin_seekable, METH_NOARGS}, {"readable", (PyCFunction)BufferedIOMixin_readable, METH_NOARGS}, {"writable", (PyCFunction)BufferedIOMixin_writable, METH_NOARGS}, diff --git a/Modules/_io/textio.c b/Modules/_io/textio.c index 26fc68de9d2..f201ba73243 100644 --- a/Modules/_io/textio.c +++ b/Modules/_io/textio.c @@ -28,6 +28,19 @@ _unsupported(const char *message) return NULL; } +PyDoc_STRVAR(TextIOBase_detach_doc, + "Separate the underlying buffer from the TextIOBase and return it.\n" + "\n" + "After the underlying buffer has been detached, the TextIO is in an\n" + "unusable state.\n" + ); + +static PyObject * +TextIOBase_detach(PyObject *self) +{ + return _unsupported("detach"); +} + PyDoc_STRVAR(TextIOBase_read_doc, "Read at most n characters from stream.\n" "\n" @@ -93,6 +106,7 @@ TextIOBase_newlines_get(PyObject *self, void *context) static PyMethodDef TextIOBase_methods[] = { + {"detach", (PyCFunction)TextIOBase_detach, METH_NOARGS, TextIOBase_detach_doc}, {"read", TextIOBase_read, METH_VARARGS, TextIOBase_read_doc}, {"readline", TextIOBase_readline, METH_VARARGS, TextIOBase_readline_doc}, {"write", TextIOBase_write, METH_VARARGS, TextIOBase_write_doc}, @@ -616,6 +630,7 @@ typedef struct { PyObject_HEAD int ok; /* initialized? */ + int detached; Py_ssize_t chunk_size; PyObject *buffer; PyObject *encoding; @@ -759,6 +774,7 @@ TextIOWrapper_init(PyTextIOWrapperObject *self, PyObject *args, PyObject *kwds) int r; self->ok = 0; + self->detached = 0; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|zzzi:fileio", kwlist, &buffer, &encoding, &errors, &newline, &line_buffering)) @@ -1059,19 +1075,45 @@ TextIOWrapper_closed_get(PyTextIOWrapperObject *self, void *context); #define CHECK_INITIALIZED(self) \ if (self->ok <= 0) { \ - PyErr_SetString(PyExc_ValueError, \ - "I/O operation on uninitialized object"); \ + if (self->detached) { \ + PyErr_SetString(PyExc_ValueError, \ + "underlying buffer has been detached"); \ + } else { \ + PyErr_SetString(PyExc_ValueError, \ + "I/O operation on uninitialized object"); \ + } \ return NULL; \ } #define CHECK_INITIALIZED_INT(self) \ if (self->ok <= 0) { \ - PyErr_SetString(PyExc_ValueError, \ - "I/O operation on uninitialized object"); \ + if (self->detached) { \ + PyErr_SetString(PyExc_ValueError, \ + "underlying buffer has been detached"); \ + } else { \ + PyErr_SetString(PyExc_ValueError, \ + "I/O operation on uninitialized object"); \ + } \ return -1; \ } +static PyObject * +TextIOWrapper_detach(PyTextIOWrapperObject *self) +{ + PyObject *buffer, *res; + CHECK_INITIALIZED(self); + res = PyObject_CallMethodObjArgs((PyObject *)self, _PyIO_str_flush, NULL); + if (res == NULL) + return NULL; + Py_DECREF(res); + buffer = self->buffer; + self->buffer = NULL; + self->detached = 1; + self->ok = 0; + return buffer; +} + Py_LOCAL_INLINE(const Py_UNICODE *) findchar(const Py_UNICODE *s, Py_ssize_t size, Py_UNICODE ch) { @@ -2341,6 +2383,7 @@ TextIOWrapper_chunk_size_set(PyTextIOWrapperObject *self, } static PyMethodDef TextIOWrapper_methods[] = { + {"detach", (PyCFunction)TextIOWrapper_detach, METH_NOARGS}, {"write", (PyCFunction)TextIOWrapper_write, METH_VARARGS}, {"read", (PyCFunction)TextIOWrapper_read, METH_VARARGS}, {"readline", (PyCFunction)TextIOWrapper_readline, METH_VARARGS},