summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Doc/library/io.rst26
-rw-r--r--Lib/_pyio.py51
-rw-r--r--Lib/test/test_io.py31
-rw-r--r--Lib/test/test_memoryio.py10
-rw-r--r--Misc/NEWS4
-rw-r--r--Modules/_io/bufferedio.c56
-rw-r--r--Modules/_io/textio.c51
7 files changed, 214 insertions, 15 deletions
diff --git a/Doc/library/io.rst b/Doc/library/io.rst
index c839691..4f6ee5a 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 e580366..e3e7c3d 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 a8c878e..1a525dc 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 ad04613..d94d9dd 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 d6ea209..4319a4a 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 3d175c7..2c65207 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 26fc68d..f201ba7 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},