implement a detach() method for BufferedIOBase and TextIOBase #5883

This commit is contained in:
Benjamin Peterson 2009-05-01 20:40:59 +00:00
parent 155374d95d
commit d2e0c7955f
7 changed files with 214 additions and 15 deletions

View File

@ -361,6 +361,17 @@ I/O Base Classes
:class:`BufferedIOBase` provides or overrides these methods in addition to :class:`BufferedIOBase` provides or overrides these methods in addition to
those from :class:`IOBase`: 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]) .. method:: read([n])
Read and return up to *n* bytes. If the argument is omitted, ``None``, or 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. *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]]) .. 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 A string, a tuple of strings, or ``None``, indicating the newlines
translated so far. 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) .. method:: read(n)
Read and return at most *n* characters from the stream as a single Read and return at most *n* characters from the stream as a single

View File

@ -642,6 +642,15 @@ class BufferedIOBase(IOBase):
""" """
self._unsupported("write") 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) io.BufferedIOBase.register(BufferedIOBase)
@ -689,13 +698,21 @@ class _BufferedIOMixin(BufferedIOBase):
self.raw.flush() self.raw.flush()
def close(self): def close(self):
if not self.closed: if not self.closed and self.raw is not None:
try: try:
self.flush() self.flush()
except IOError: except IOError:
pass # If flush() fails, just give up pass # If flush() fails, just give up
self.raw.close() 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 ### ### Inquiries ###
def seekable(self): def seekable(self):
@ -1236,6 +1253,15 @@ class TextIOBase(IOBase):
""" """
self._unsupported("readline") 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 @property
def encoding(self): def encoding(self):
"""Subclasses should override.""" """Subclasses should override."""
@ -1448,6 +1474,7 @@ class TextIOWrapper(TextIOBase):
self._telling = self._seekable self._telling = self._seekable
def close(self): def close(self):
if self.buffer is not None:
try: try:
self.flush() self.flush()
except IOError: except IOError:
@ -1647,6 +1674,14 @@ class TextIOWrapper(TextIOBase):
self.seek(pos) self.seek(pos)
return self.buffer.truncate() 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): def seek(self, cookie, whence=0):
if self.closed: if self.closed:
raise ValueError("tell on closed file") raise ValueError("tell on closed file")
@ -1865,3 +1900,7 @@ class StringIO(TextIOWrapper):
@property @property
def encoding(self): def encoding(self):
return None return None
def detach(self):
# This doesn't make sense on StringIO.
self._unsupported("detach")

View File

@ -526,6 +526,12 @@ class PyIOTest(IOTest):
class CommonBufferedTests: class CommonBufferedTests:
# Tests common to BufferedReader, BufferedWriter and BufferedRandom # 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): def test_fileno(self):
rawio = self.MockRawIO() rawio = self.MockRawIO()
bufio = self.tp(rawio) bufio = self.tp(rawio)
@ -811,6 +817,14 @@ class BufferedWriterTest(unittest.TestCase, CommonBufferedTests):
bufio.flush() bufio.flush()
self.assertEquals(b"".join(rawio._write_stack), b"abcghi") 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): def test_write(self):
# Write to the buffered IO but don't overflow the buffer. # Write to the buffered IO but don't overflow the buffer.
writer = self.MockRawIO() writer = self.MockRawIO()
@ -1052,6 +1066,10 @@ class BufferedRWPairTest(unittest.TestCase):
pair = self.tp(self.MockRawIO(), self.MockRawIO()) pair = self.tp(self.MockRawIO(), self.MockRawIO())
self.assertFalse(pair.closed) 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): def test_constructor_max_buffer_size_deprecation(self):
with support.check_warnings() as w: with support.check_warnings() as w:
warnings.simplefilter("always", DeprecationWarning) warnings.simplefilter("always", DeprecationWarning)
@ -1480,6 +1498,19 @@ class TextIOWrapperTest(unittest.TestCase):
self.assertRaises(TypeError, t.__init__, b, newline=42) self.assertRaises(TypeError, t.__init__, b, newline=42)
self.assertRaises(ValueError, t.__init__, b, newline='xyzzy') 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): def test_repr(self):
raw = self.BytesIO("hello".encode("utf-8")) raw = self.BytesIO("hello".encode("utf-8"))
b = self.BufferedReader(raw) b = self.BufferedReader(raw)

View File

@ -57,6 +57,10 @@ class MemorySeekTestMixin:
class MemoryTestMixin: class MemoryTestMixin:
def test_detach(self):
buf = self.ioclass()
self.assertRaises(self.UnsupportedOperation, buf.detach)
def write_ops(self, f, t): def write_ops(self, f, t):
self.assertEqual(f.write(t("blah.")), 5) self.assertEqual(f.write(t("blah.")), 5)
self.assertEqual(f.seek(0), 0) self.assertEqual(f.seek(0), 0)
@ -336,6 +340,9 @@ class MemoryTestMixin:
class PyBytesIOTest(MemoryTestMixin, MemorySeekTestMixin, unittest.TestCase): class PyBytesIOTest(MemoryTestMixin, MemorySeekTestMixin, unittest.TestCase):
UnsupportedOperation = pyio.UnsupportedOperation
@staticmethod @staticmethod
def buftype(s): def buftype(s):
return s.encode("ascii") return s.encode("ascii")
@ -413,6 +420,7 @@ class PyBytesIOTest(MemoryTestMixin, MemorySeekTestMixin, unittest.TestCase):
class PyStringIOTest(MemoryTestMixin, MemorySeekTestMixin, unittest.TestCase): class PyStringIOTest(MemoryTestMixin, MemorySeekTestMixin, unittest.TestCase):
buftype = str buftype = str
ioclass = pyio.StringIO ioclass = pyio.StringIO
UnsupportedOperation = pyio.UnsupportedOperation
EOF = "" EOF = ""
# TextIO-specific behaviour. # TextIO-specific behaviour.
@ -518,9 +526,11 @@ class PyStringIOTest(MemoryTestMixin, MemorySeekTestMixin, unittest.TestCase):
class CBytesIOTest(PyBytesIOTest): class CBytesIOTest(PyBytesIOTest):
ioclass = io.BytesIO ioclass = io.BytesIO
UnsupportedOperation = io.UnsupportedOperation
class CStringIOTest(PyStringIOTest): class CStringIOTest(PyStringIOTest):
ioclass = io.StringIO ioclass = io.StringIO
UnsupportedOperation = io.UnsupportedOperation
# XXX: For the Python version of io.StringIO, this is highly # XXX: For the Python version of io.StringIO, this is highly
# dependent on the encoding used for the underlying buffer. # dependent on the encoding used for the underlying buffer.

View File

@ -12,6 +12,10 @@ What's New in Python 3.1 beta 1?
Core and Builtins 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 - Issue #5859: Remove switch from '%f' to '%g'-style formatting for
floats with absolute value over 1e50. Also remove length floats with absolute value over 1e50. Also remove length
restrictions for float formatting: '%.67f' % 12.34 and '%.120e' % restrictions for float formatting: '%.67f' % 12.34 and '%.120e' %

View File

@ -73,6 +73,18 @@ BufferedIOBase_unsupported(const char *message)
return NULL; 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, PyDoc_STRVAR(BufferedIOBase_read_doc,
"Read and return up to n bytes.\n" "Read and return up to n bytes.\n"
"\n" "\n"
@ -127,6 +139,7 @@ BufferedIOBase_write(PyObject *self, PyObject *args)
static PyMethodDef BufferedIOBase_methods[] = { static PyMethodDef BufferedIOBase_methods[] = {
{"detach", (PyCFunction)BufferedIOBase_detach, METH_NOARGS, BufferedIOBase_detach_doc},
{"read", BufferedIOBase_read, METH_VARARGS, BufferedIOBase_read_doc}, {"read", BufferedIOBase_read, METH_VARARGS, BufferedIOBase_read_doc},
{"read1", BufferedIOBase_read1, METH_VARARGS, BufferedIOBase_read1_doc}, {"read1", BufferedIOBase_read1, METH_VARARGS, BufferedIOBase_read1_doc},
{"readinto", BufferedIOBase_readinto, METH_VARARGS, NULL}, {"readinto", BufferedIOBase_readinto, METH_VARARGS, NULL},
@ -181,6 +194,7 @@ typedef struct {
PyObject *raw; PyObject *raw;
int ok; /* Initialized? */ int ok; /* Initialized? */
int detached;
int readable; int readable;
int writable; int writable;
@ -260,15 +274,25 @@ typedef struct {
#define CHECK_INITIALIZED(self) \ #define CHECK_INITIALIZED(self) \
if (self->ok <= 0) { \ if (self->ok <= 0) { \
if (self->detached) { \
PyErr_SetString(PyExc_ValueError, \
"raw stream has been detached"); \
} else { \
PyErr_SetString(PyExc_ValueError, \ PyErr_SetString(PyExc_ValueError, \
"I/O operation on uninitialized object"); \ "I/O operation on uninitialized object"); \
} \
return NULL; \ return NULL; \
} }
#define CHECK_INITIALIZED_INT(self) \ #define CHECK_INITIALIZED_INT(self) \
if (self->ok <= 0) { \ if (self->ok <= 0) { \
if (self->detached) { \
PyErr_SetString(PyExc_ValueError, \
"raw stream has been detached"); \
} else { \
PyErr_SetString(PyExc_ValueError, \ PyErr_SetString(PyExc_ValueError, \
"I/O operation on uninitialized object"); \ "I/O operation on uninitialized object"); \
} \
return -1; \ return -1; \
} }
@ -430,6 +454,24 @@ end:
return res; 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 */ /* Inquiries */
static PyObject * static PyObject *
@ -1101,6 +1143,7 @@ BufferedReader_init(BufferedObject *self, PyObject *args, PyObject *kwds)
PyObject *raw; PyObject *raw;
self->ok = 0; self->ok = 0;
self->detached = 0;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|n:BufferedReader", kwlist, if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|n:BufferedReader", kwlist,
&raw, &buffer_size)) { &raw, &buffer_size)) {
@ -1387,6 +1430,7 @@ _BufferedReader_peek_unlocked(BufferedObject *self, Py_ssize_t n)
static PyMethodDef BufferedReader_methods[] = { static PyMethodDef BufferedReader_methods[] = {
/* BufferedIOMixin methods */ /* BufferedIOMixin methods */
{"detach", (PyCFunction)BufferedIOMixin_detach, METH_NOARGS},
{"flush", (PyCFunction)BufferedIOMixin_flush, METH_NOARGS}, {"flush", (PyCFunction)BufferedIOMixin_flush, METH_NOARGS},
{"close", (PyCFunction)BufferedIOMixin_close, METH_NOARGS}, {"close", (PyCFunction)BufferedIOMixin_close, METH_NOARGS},
{"seekable", (PyCFunction)BufferedIOMixin_seekable, METH_NOARGS}, {"seekable", (PyCFunction)BufferedIOMixin_seekable, METH_NOARGS},
@ -1499,6 +1543,7 @@ BufferedWriter_init(BufferedObject *self, PyObject *args, PyObject *kwds)
PyObject *raw; PyObject *raw;
self->ok = 0; self->ok = 0;
self->detached = 0;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|nn:BufferedReader", kwlist, if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|nn:BufferedReader", kwlist,
&raw, &buffer_size, &max_buffer_size)) { &raw, &buffer_size, &max_buffer_size)) {
@ -1745,6 +1790,7 @@ error:
static PyMethodDef BufferedWriter_methods[] = { static PyMethodDef BufferedWriter_methods[] = {
/* BufferedIOMixin methods */ /* BufferedIOMixin methods */
{"close", (PyCFunction)BufferedIOMixin_close, METH_NOARGS}, {"close", (PyCFunction)BufferedIOMixin_close, METH_NOARGS},
{"detach", (PyCFunction)BufferedIOMixin_detach, METH_NOARGS},
{"seekable", (PyCFunction)BufferedIOMixin_seekable, METH_NOARGS}, {"seekable", (PyCFunction)BufferedIOMixin_seekable, METH_NOARGS},
{"readable", (PyCFunction)BufferedIOMixin_readable, METH_NOARGS}, {"readable", (PyCFunction)BufferedIOMixin_readable, METH_NOARGS},
{"writable", (PyCFunction)BufferedIOMixin_writable, METH_NOARGS}, {"writable", (PyCFunction)BufferedIOMixin_writable, METH_NOARGS},
@ -2089,6 +2135,7 @@ BufferedRandom_init(BufferedObject *self, PyObject *args, PyObject *kwds)
PyObject *raw; PyObject *raw;
self->ok = 0; self->ok = 0;
self->detached = 0;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|nn:BufferedReader", kwlist, if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|nn:BufferedReader", kwlist,
&raw, &buffer_size, &max_buffer_size)) { &raw, &buffer_size, &max_buffer_size)) {
@ -2128,6 +2175,7 @@ BufferedRandom_init(BufferedObject *self, PyObject *args, PyObject *kwds)
static PyMethodDef BufferedRandom_methods[] = { static PyMethodDef BufferedRandom_methods[] = {
/* BufferedIOMixin methods */ /* BufferedIOMixin methods */
{"close", (PyCFunction)BufferedIOMixin_close, METH_NOARGS}, {"close", (PyCFunction)BufferedIOMixin_close, METH_NOARGS},
{"detach", (PyCFunction)BufferedIOMixin_detach, METH_NOARGS},
{"seekable", (PyCFunction)BufferedIOMixin_seekable, METH_NOARGS}, {"seekable", (PyCFunction)BufferedIOMixin_seekable, METH_NOARGS},
{"readable", (PyCFunction)BufferedIOMixin_readable, METH_NOARGS}, {"readable", (PyCFunction)BufferedIOMixin_readable, METH_NOARGS},
{"writable", (PyCFunction)BufferedIOMixin_writable, METH_NOARGS}, {"writable", (PyCFunction)BufferedIOMixin_writable, METH_NOARGS},

View File

@ -28,6 +28,19 @@ _unsupported(const char *message)
return NULL; 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, PyDoc_STRVAR(TextIOBase_read_doc,
"Read at most n characters from stream.\n" "Read at most n characters from stream.\n"
"\n" "\n"
@ -93,6 +106,7 @@ TextIOBase_newlines_get(PyObject *self, void *context)
static PyMethodDef TextIOBase_methods[] = { static PyMethodDef TextIOBase_methods[] = {
{"detach", (PyCFunction)TextIOBase_detach, METH_NOARGS, TextIOBase_detach_doc},
{"read", TextIOBase_read, METH_VARARGS, TextIOBase_read_doc}, {"read", TextIOBase_read, METH_VARARGS, TextIOBase_read_doc},
{"readline", TextIOBase_readline, METH_VARARGS, TextIOBase_readline_doc}, {"readline", TextIOBase_readline, METH_VARARGS, TextIOBase_readline_doc},
{"write", TextIOBase_write, METH_VARARGS, TextIOBase_write_doc}, {"write", TextIOBase_write, METH_VARARGS, TextIOBase_write_doc},
@ -616,6 +630,7 @@ typedef struct
{ {
PyObject_HEAD PyObject_HEAD
int ok; /* initialized? */ int ok; /* initialized? */
int detached;
Py_ssize_t chunk_size; Py_ssize_t chunk_size;
PyObject *buffer; PyObject *buffer;
PyObject *encoding; PyObject *encoding;
@ -759,6 +774,7 @@ TextIOWrapper_init(PyTextIOWrapperObject *self, PyObject *args, PyObject *kwds)
int r; int r;
self->ok = 0; self->ok = 0;
self->detached = 0;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|zzzi:fileio", if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|zzzi:fileio",
kwlist, &buffer, &encoding, &errors, kwlist, &buffer, &encoding, &errors,
&newline, &line_buffering)) &newline, &line_buffering))
@ -1059,19 +1075,45 @@ TextIOWrapper_closed_get(PyTextIOWrapperObject *self, void *context);
#define CHECK_INITIALIZED(self) \ #define CHECK_INITIALIZED(self) \
if (self->ok <= 0) { \ if (self->ok <= 0) { \
if (self->detached) { \
PyErr_SetString(PyExc_ValueError, \
"underlying buffer has been detached"); \
} else { \
PyErr_SetString(PyExc_ValueError, \ PyErr_SetString(PyExc_ValueError, \
"I/O operation on uninitialized object"); \ "I/O operation on uninitialized object"); \
} \
return NULL; \ return NULL; \
} }
#define CHECK_INITIALIZED_INT(self) \ #define CHECK_INITIALIZED_INT(self) \
if (self->ok <= 0) { \ if (self->ok <= 0) { \
if (self->detached) { \
PyErr_SetString(PyExc_ValueError, \
"underlying buffer has been detached"); \
} else { \
PyErr_SetString(PyExc_ValueError, \ PyErr_SetString(PyExc_ValueError, \
"I/O operation on uninitialized object"); \ "I/O operation on uninitialized object"); \
} \
return -1; \ 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 *) Py_LOCAL_INLINE(const Py_UNICODE *)
findchar(const Py_UNICODE *s, Py_ssize_t size, Py_UNICODE ch) 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[] = { static PyMethodDef TextIOWrapper_methods[] = {
{"detach", (PyCFunction)TextIOWrapper_detach, METH_NOARGS},
{"write", (PyCFunction)TextIOWrapper_write, METH_VARARGS}, {"write", (PyCFunction)TextIOWrapper_write, METH_VARARGS},
{"read", (PyCFunction)TextIOWrapper_read, METH_VARARGS}, {"read", (PyCFunction)TextIOWrapper_read, METH_VARARGS},
{"readline", (PyCFunction)TextIOWrapper_readline, METH_VARARGS}, {"readline", (PyCFunction)TextIOWrapper_readline, METH_VARARGS},