implement a detach() method for BufferedIOBase and TextIOBase #5883
This commit is contained in:
parent
155374d95d
commit
d2e0c7955f
|
@ -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
|
||||
|
|
51
Lib/_pyio.py
51
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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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' %
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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},
|
||||
|
|
Loading…
Reference in New Issue