Issue #9293: I/O streams now raise io.UnsupportedOperation
when an
unsupported operation is attempted (for example, writing to a file open only for reading).
This commit is contained in:
parent
bad092556e
commit
0d739d7047
30
Lib/_pyio.py
30
Lib/_pyio.py
@ -243,8 +243,13 @@ class OpenWrapper:
|
|||||||
return open(*args, **kwargs)
|
return open(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class UnsupportedOperation(ValueError, IOError):
|
# In normal operation, both `UnsupportedOperation`s should be bound to the
|
||||||
pass
|
# same object.
|
||||||
|
try:
|
||||||
|
UnsupportedOperation = io.UnsupportedOperation
|
||||||
|
except AttributeError:
|
||||||
|
class UnsupportedOperation(ValueError, IOError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class IOBase(metaclass=abc.ABCMeta):
|
class IOBase(metaclass=abc.ABCMeta):
|
||||||
@ -362,9 +367,8 @@ class IOBase(metaclass=abc.ABCMeta):
|
|||||||
"""Internal: raise an IOError if file is not seekable
|
"""Internal: raise an IOError if file is not seekable
|
||||||
"""
|
"""
|
||||||
if not self.seekable():
|
if not self.seekable():
|
||||||
raise IOError("File or stream is not seekable."
|
raise UnsupportedOperation("File or stream is not seekable."
|
||||||
if msg is None else msg)
|
if msg is None else msg)
|
||||||
|
|
||||||
|
|
||||||
def readable(self) -> bool:
|
def readable(self) -> bool:
|
||||||
"""Return whether object was opened for reading.
|
"""Return whether object was opened for reading.
|
||||||
@ -377,8 +381,8 @@ class IOBase(metaclass=abc.ABCMeta):
|
|||||||
"""Internal: raise an IOError if file is not readable
|
"""Internal: raise an IOError if file is not readable
|
||||||
"""
|
"""
|
||||||
if not self.readable():
|
if not self.readable():
|
||||||
raise IOError("File or stream is not readable."
|
raise UnsupportedOperation("File or stream is not readable."
|
||||||
if msg is None else msg)
|
if msg is None else msg)
|
||||||
|
|
||||||
def writable(self) -> bool:
|
def writable(self) -> bool:
|
||||||
"""Return whether object was opened for writing.
|
"""Return whether object was opened for writing.
|
||||||
@ -391,8 +395,8 @@ class IOBase(metaclass=abc.ABCMeta):
|
|||||||
"""Internal: raise an IOError if file is not writable
|
"""Internal: raise an IOError if file is not writable
|
||||||
"""
|
"""
|
||||||
if not self.writable():
|
if not self.writable():
|
||||||
raise IOError("File or stream is not writable."
|
raise UnsupportedOperation("File or stream is not writable."
|
||||||
if msg is None else msg)
|
if msg is None else msg)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def closed(self):
|
def closed(self):
|
||||||
@ -1647,7 +1651,7 @@ class TextIOWrapper(TextIOBase):
|
|||||||
|
|
||||||
def tell(self):
|
def tell(self):
|
||||||
if not self._seekable:
|
if not self._seekable:
|
||||||
raise IOError("underlying stream is not seekable")
|
raise UnsupportedOperation("underlying stream is not seekable")
|
||||||
if not self._telling:
|
if not self._telling:
|
||||||
raise IOError("telling position disabled by next() call")
|
raise IOError("telling position disabled by next() call")
|
||||||
self.flush()
|
self.flush()
|
||||||
@ -1726,17 +1730,17 @@ class TextIOWrapper(TextIOBase):
|
|||||||
if self.closed:
|
if self.closed:
|
||||||
raise ValueError("tell on closed file")
|
raise ValueError("tell on closed file")
|
||||||
if not self._seekable:
|
if not self._seekable:
|
||||||
raise IOError("underlying stream is not seekable")
|
raise UnsupportedOperation("underlying stream is not seekable")
|
||||||
if whence == 1: # seek relative to current position
|
if whence == 1: # seek relative to current position
|
||||||
if cookie != 0:
|
if cookie != 0:
|
||||||
raise IOError("can't do nonzero cur-relative seeks")
|
raise UnsupportedOperation("can't do nonzero cur-relative seeks")
|
||||||
# Seeking to the current position should attempt to
|
# Seeking to the current position should attempt to
|
||||||
# sync the underlying buffer with the current position.
|
# sync the underlying buffer with the current position.
|
||||||
whence = 0
|
whence = 0
|
||||||
cookie = self.tell()
|
cookie = self.tell()
|
||||||
if whence == 2: # seek relative to end of file
|
if whence == 2: # seek relative to end of file
|
||||||
if cookie != 0:
|
if cookie != 0:
|
||||||
raise IOError("can't do nonzero end-relative seeks")
|
raise UnsupportedOperation("can't do nonzero end-relative seeks")
|
||||||
self.flush()
|
self.flush()
|
||||||
position = self.buffer.seek(0, 2)
|
position = self.buffer.seek(0, 2)
|
||||||
self._set_decoded_chars('')
|
self._set_decoded_chars('')
|
||||||
|
@ -179,6 +179,23 @@ class PyMockFileIO(MockFileIO, pyio.BytesIO):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MockUnseekableIO:
|
||||||
|
def seekable(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def seek(self, *args):
|
||||||
|
raise self.UnsupportedOperation("not seekable")
|
||||||
|
|
||||||
|
def tell(self, *args):
|
||||||
|
raise self.UnsupportedOperation("not seekable")
|
||||||
|
|
||||||
|
class CMockUnseekableIO(MockUnseekableIO, io.BytesIO):
|
||||||
|
UnsupportedOperation = io.UnsupportedOperation
|
||||||
|
|
||||||
|
class PyMockUnseekableIO(MockUnseekableIO, pyio.BytesIO):
|
||||||
|
UnsupportedOperation = pyio.UnsupportedOperation
|
||||||
|
|
||||||
|
|
||||||
class MockNonBlockWriterIO:
|
class MockNonBlockWriterIO:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -304,16 +321,26 @@ class IOTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_invalid_operations(self):
|
def test_invalid_operations(self):
|
||||||
# Try writing on a file opened in read mode and vice-versa.
|
# Try writing on a file opened in read mode and vice-versa.
|
||||||
|
exc = self.UnsupportedOperation
|
||||||
for mode in ("w", "wb"):
|
for mode in ("w", "wb"):
|
||||||
with self.open(support.TESTFN, mode) as fp:
|
with self.open(support.TESTFN, mode) as fp:
|
||||||
self.assertRaises(IOError, fp.read)
|
self.assertRaises(exc, fp.read)
|
||||||
self.assertRaises(IOError, fp.readline)
|
self.assertRaises(exc, fp.readline)
|
||||||
|
with self.open(support.TESTFN, "wb", buffering=0) as fp:
|
||||||
|
self.assertRaises(exc, fp.read)
|
||||||
|
self.assertRaises(exc, fp.readline)
|
||||||
|
with self.open(support.TESTFN, "rb", buffering=0) as fp:
|
||||||
|
self.assertRaises(exc, fp.write, b"blah")
|
||||||
|
self.assertRaises(exc, fp.writelines, [b"blah\n"])
|
||||||
with self.open(support.TESTFN, "rb") as fp:
|
with self.open(support.TESTFN, "rb") as fp:
|
||||||
self.assertRaises(IOError, fp.write, b"blah")
|
self.assertRaises(exc, fp.write, b"blah")
|
||||||
self.assertRaises(IOError, fp.writelines, [b"blah\n"])
|
self.assertRaises(exc, fp.writelines, [b"blah\n"])
|
||||||
with self.open(support.TESTFN, "r") as fp:
|
with self.open(support.TESTFN, "r") as fp:
|
||||||
self.assertRaises(IOError, fp.write, "blah")
|
self.assertRaises(exc, fp.write, "blah")
|
||||||
self.assertRaises(IOError, fp.writelines, ["blah\n"])
|
self.assertRaises(exc, fp.writelines, ["blah\n"])
|
||||||
|
# Non-zero seeking from current or end pos
|
||||||
|
self.assertRaises(exc, fp.seek, 1, self.SEEK_CUR)
|
||||||
|
self.assertRaises(exc, fp.seek, -1, self.SEEK_END)
|
||||||
|
|
||||||
def test_raw_file_io(self):
|
def test_raw_file_io(self):
|
||||||
with self.open(support.TESTFN, "wb", buffering=0) as f:
|
with self.open(support.TESTFN, "wb", buffering=0) as f:
|
||||||
@ -670,6 +697,11 @@ class CommonBufferedTests:
|
|||||||
b.close()
|
b.close()
|
||||||
self.assertRaises(ValueError, b.flush)
|
self.assertRaises(ValueError, b.flush)
|
||||||
|
|
||||||
|
def test_unseekable(self):
|
||||||
|
bufio = self.tp(self.MockUnseekableIO(b"A" * 10))
|
||||||
|
self.assertRaises(self.UnsupportedOperation, bufio.tell)
|
||||||
|
self.assertRaises(self.UnsupportedOperation, bufio.seek, 0)
|
||||||
|
|
||||||
|
|
||||||
class BufferedReaderTest(unittest.TestCase, CommonBufferedTests):
|
class BufferedReaderTest(unittest.TestCase, CommonBufferedTests):
|
||||||
read_mode = "rb"
|
read_mode = "rb"
|
||||||
@ -1433,6 +1465,9 @@ class BufferedRandomTest(BufferedReaderTest, BufferedWriterTest):
|
|||||||
BufferedReaderTest.test_misbehaved_io(self)
|
BufferedReaderTest.test_misbehaved_io(self)
|
||||||
BufferedWriterTest.test_misbehaved_io(self)
|
BufferedWriterTest.test_misbehaved_io(self)
|
||||||
|
|
||||||
|
# You can't construct a BufferedRandom over a non-seekable stream.
|
||||||
|
test_unseekable = None
|
||||||
|
|
||||||
class CBufferedRandomTest(BufferedRandomTest):
|
class CBufferedRandomTest(BufferedRandomTest):
|
||||||
tp = io.BufferedRandom
|
tp = io.BufferedRandom
|
||||||
|
|
||||||
@ -2177,6 +2212,11 @@ class TextIOWrapperTest(unittest.TestCase):
|
|||||||
txt.close()
|
txt.close()
|
||||||
self.assertRaises(ValueError, txt.flush)
|
self.assertRaises(ValueError, txt.flush)
|
||||||
|
|
||||||
|
def test_unseekable(self):
|
||||||
|
txt = self.TextIOWrapper(self.MockUnseekableIO(self.testdata))
|
||||||
|
self.assertRaises(self.UnsupportedOperation, txt.tell)
|
||||||
|
self.assertRaises(self.UnsupportedOperation, txt.seek, 0)
|
||||||
|
|
||||||
class CTextIOWrapperTest(TextIOWrapperTest):
|
class CTextIOWrapperTest(TextIOWrapperTest):
|
||||||
|
|
||||||
def test_initialization(self):
|
def test_initialization(self):
|
||||||
@ -2550,7 +2590,7 @@ def test_main():
|
|||||||
# Put the namespaces of the IO module we are testing and some useful mock
|
# Put the namespaces of the IO module we are testing and some useful mock
|
||||||
# classes in the __dict__ of each test.
|
# classes in the __dict__ of each test.
|
||||||
mocks = (MockRawIO, MisbehavedRawIO, MockFileIO, CloseFailureIO,
|
mocks = (MockRawIO, MisbehavedRawIO, MockFileIO, CloseFailureIO,
|
||||||
MockNonBlockWriterIO)
|
MockNonBlockWriterIO, MockUnseekableIO)
|
||||||
all_members = io.__all__ + ["IncrementalNewlineDecoder"]
|
all_members = io.__all__ + ["IncrementalNewlineDecoder"]
|
||||||
c_io_ns = {name : getattr(io, name) for name in all_members}
|
c_io_ns = {name : getattr(io, name) for name in all_members}
|
||||||
py_io_ns = {name : getattr(pyio, name) for name in all_members}
|
py_io_ns = {name : getattr(pyio, name) for name in all_members}
|
||||||
|
@ -13,6 +13,10 @@ Core and Builtins
|
|||||||
Library
|
Library
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
- Issue #9293: I/O streams now raise ``io.UnsupportedOperation`` when an
|
||||||
|
unsupported operation is attempted (for example, writing to a file open
|
||||||
|
only for reading).
|
||||||
|
|
||||||
|
|
||||||
What's New in Python 3.2 Alpha 2?
|
What's New in Python 3.2 Alpha 2?
|
||||||
=================================
|
=================================
|
||||||
|
@ -417,7 +417,8 @@ err_closed(void)
|
|||||||
static PyObject *
|
static PyObject *
|
||||||
err_mode(char *action)
|
err_mode(char *action)
|
||||||
{
|
{
|
||||||
PyErr_Format(PyExc_ValueError, "File not open for %s", action);
|
PyErr_Format(IO_STATE->unsupported_operation,
|
||||||
|
"File not open for %s", action);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -317,7 +317,7 @@ _PyIOBase_check_seekable(PyObject *self, PyObject *args)
|
|||||||
return NULL;
|
return NULL;
|
||||||
if (res != Py_True) {
|
if (res != Py_True) {
|
||||||
Py_CLEAR(res);
|
Py_CLEAR(res);
|
||||||
PyErr_SetString(PyExc_IOError, "File or stream is not seekable.");
|
iobase_unsupported("File or stream is not seekable.");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
if (args == Py_True) {
|
if (args == Py_True) {
|
||||||
@ -346,7 +346,7 @@ _PyIOBase_check_readable(PyObject *self, PyObject *args)
|
|||||||
return NULL;
|
return NULL;
|
||||||
if (res != Py_True) {
|
if (res != Py_True) {
|
||||||
Py_CLEAR(res);
|
Py_CLEAR(res);
|
||||||
PyErr_SetString(PyExc_IOError, "File or stream is not readable.");
|
iobase_unsupported("File or stream is not readable.");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
if (args == Py_True) {
|
if (args == Py_True) {
|
||||||
@ -375,7 +375,7 @@ _PyIOBase_check_writable(PyObject *self, PyObject *args)
|
|||||||
return NULL;
|
return NULL;
|
||||||
if (res != Py_True) {
|
if (res != Py_True) {
|
||||||
Py_CLEAR(res);
|
Py_CLEAR(res);
|
||||||
PyErr_SetString(PyExc_IOError, "File or stream is not writable.");
|
iobase_unsupported("File or stream is not writable.");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
if (args == Py_True) {
|
if (args == Py_True) {
|
||||||
|
@ -1259,10 +1259,8 @@ textiowrapper_write(textio *self, PyObject *args)
|
|||||||
|
|
||||||
CHECK_CLOSED(self);
|
CHECK_CLOSED(self);
|
||||||
|
|
||||||
if (self->encoder == NULL) {
|
if (self->encoder == NULL)
|
||||||
PyErr_SetString(PyExc_IOError, "not writable");
|
return _unsupported("not writable");
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
Py_INCREF(text);
|
Py_INCREF(text);
|
||||||
|
|
||||||
@ -1399,7 +1397,7 @@ textiowrapper_read_chunk(textio *self)
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
if (self->decoder == NULL) {
|
if (self->decoder == NULL) {
|
||||||
PyErr_SetString(PyExc_IOError, "not readable");
|
_unsupported("not readable");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1489,10 +1487,8 @@ textiowrapper_read(textio *self, PyObject *args)
|
|||||||
|
|
||||||
CHECK_CLOSED(self);
|
CHECK_CLOSED(self);
|
||||||
|
|
||||||
if (self->decoder == NULL) {
|
if (self->decoder == NULL)
|
||||||
PyErr_SetString(PyExc_IOError, "not readable");
|
return _unsupported("not readable");
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_textiowrapper_writeflush(self) < 0)
|
if (_textiowrapper_writeflush(self) < 0)
|
||||||
return NULL;
|
return NULL;
|
||||||
@ -1983,8 +1979,7 @@ textiowrapper_seek(textio *self, PyObject *args)
|
|||||||
Py_INCREF(cookieObj);
|
Py_INCREF(cookieObj);
|
||||||
|
|
||||||
if (!self->seekable) {
|
if (!self->seekable) {
|
||||||
PyErr_SetString(PyExc_IOError,
|
_unsupported("underlying stream is not seekable");
|
||||||
"underlying stream is not seekable");
|
|
||||||
goto fail;
|
goto fail;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1995,8 +1990,7 @@ textiowrapper_seek(textio *self, PyObject *args)
|
|||||||
goto fail;
|
goto fail;
|
||||||
|
|
||||||
if (cmp == 0) {
|
if (cmp == 0) {
|
||||||
PyErr_SetString(PyExc_IOError,
|
_unsupported("can't do nonzero cur-relative seeks");
|
||||||
"can't do nonzero cur-relative seeks");
|
|
||||||
goto fail;
|
goto fail;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2016,8 +2010,7 @@ textiowrapper_seek(textio *self, PyObject *args)
|
|||||||
goto fail;
|
goto fail;
|
||||||
|
|
||||||
if (cmp == 0) {
|
if (cmp == 0) {
|
||||||
PyErr_SetString(PyExc_IOError,
|
_unsupported("can't do nonzero end-relative seeks");
|
||||||
"can't do nonzero end-relative seeks");
|
|
||||||
goto fail;
|
goto fail;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2151,8 +2144,7 @@ textiowrapper_tell(textio *self, PyObject *args)
|
|||||||
CHECK_CLOSED(self);
|
CHECK_CLOSED(self);
|
||||||
|
|
||||||
if (!self->seekable) {
|
if (!self->seekable) {
|
||||||
PyErr_SetString(PyExc_IOError,
|
_unsupported("underlying stream is not seekable");
|
||||||
"underlying stream is not seekable");
|
|
||||||
goto fail;
|
goto fail;
|
||||||
}
|
}
|
||||||
if (!self->telling) {
|
if (!self->telling) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user