]> granicus.if.org Git - python/commitdiff
implement a detach() method for BufferedIOBase and TextIOBase #5883
authorBenjamin Peterson <benjamin@python.org>
Fri, 1 May 2009 20:40:59 +0000 (20:40 +0000)
committerBenjamin Peterson <benjamin@python.org>
Fri, 1 May 2009 20:40:59 +0000 (20:40 +0000)
Doc/library/io.rst
Lib/_pyio.py
Lib/test/test_io.py
Lib/test/test_memoryio.py
Misc/NEWS
Modules/_io/bufferedio.c
Modules/_io/textio.c

index c839691572350cacb223798cbacf3a5641971435..4f6ee5a46868b4f71ef24682f437cfa47b0bc8f8 100644 (file)
@@ -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
index e580366df7c81713be023b1cca09a081f170e9af..e3e7c3da00c4c3a9833eb4df81bd5d812e8bca14 100644 (file)
@@ -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")
index a8c878eadfb734303cc5503d2b87083dfaf405bf..1a525dc91359b4481e65d2f23e44b3f618c395e1 100644 (file)
@@ -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)
index ad04613daf4d71c05e6952a433591c5aa6f7a333..d94d9dd552a218d0dd250b0a822b60ecbcee5643 100644 (file)
@@ -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.
index d6ea20999ff3b62d08e36b2a7ab24a82a4982057..4319a4a9ed27996b78a826301c63e0a96ac85992 100644 (file)
--- 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' %
index 3d175c7c8cf16298300218ee365a590202e00155..2c652078ee671ad94d6538feab0b9681687a6b3c 100644 (file)
@@ -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},
index 26fc68de9d2c8334606dcbe931225edf65fa8058..f201ba732435fa12915c3aefc521da8aec88616b 100644 (file)
@@ -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},