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)
|
||||
|
||||
|
||||
class UnsupportedOperation(ValueError, IOError):
|
||||
pass
|
||||
# In normal operation, both `UnsupportedOperation`s should be bound to the
|
||||
# same object.
|
||||
try:
|
||||
UnsupportedOperation = io.UnsupportedOperation
|
||||
except AttributeError:
|
||||
class UnsupportedOperation(ValueError, IOError):
|
||||
pass
|
||||
|
||||
|
||||
class IOBase(metaclass=abc.ABCMeta):
|
||||
|
@ -362,9 +367,8 @@ class IOBase(metaclass=abc.ABCMeta):
|
|||
"""Internal: raise an IOError if file is not seekable
|
||||
"""
|
||||
if not self.seekable():
|
||||
raise IOError("File or stream is not seekable."
|
||||
if msg is None else msg)
|
||||
|
||||
raise UnsupportedOperation("File or stream is not seekable."
|
||||
if msg is None else msg)
|
||||
|
||||
def readable(self) -> bool:
|
||||
"""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
|
||||
"""
|
||||
if not self.readable():
|
||||
raise IOError("File or stream is not readable."
|
||||
if msg is None else msg)
|
||||
raise UnsupportedOperation("File or stream is not readable."
|
||||
if msg is None else msg)
|
||||
|
||||
def writable(self) -> bool:
|
||||
"""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
|
||||
"""
|
||||
if not self.writable():
|
||||
raise IOError("File or stream is not writable."
|
||||
if msg is None else msg)
|
||||
raise UnsupportedOperation("File or stream is not writable."
|
||||
if msg is None else msg)
|
||||
|
||||
@property
|
||||
def closed(self):
|
||||
|
@ -1647,7 +1651,7 @@ class TextIOWrapper(TextIOBase):
|
|||
|
||||
def tell(self):
|
||||
if not self._seekable:
|
||||
raise IOError("underlying stream is not seekable")
|
||||
raise UnsupportedOperation("underlying stream is not seekable")
|
||||
if not self._telling:
|
||||
raise IOError("telling position disabled by next() call")
|
||||
self.flush()
|
||||
|
@ -1726,17 +1730,17 @@ class TextIOWrapper(TextIOBase):
|
|||
if self.closed:
|
||||
raise ValueError("tell on closed file")
|
||||
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 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
|
||||
# sync the underlying buffer with the current position.
|
||||
whence = 0
|
||||
cookie = self.tell()
|
||||
if whence == 2: # seek relative to end of file
|
||||
if cookie != 0:
|
||||
raise IOError("can't do nonzero end-relative seeks")
|
||||
raise UnsupportedOperation("can't do nonzero end-relative seeks")
|
||||
self.flush()
|
||||
position = self.buffer.seek(0, 2)
|
||||
self._set_decoded_chars('')
|
||||
|
|
|
@ -179,6 +179,23 @@ class PyMockFileIO(MockFileIO, pyio.BytesIO):
|
|||
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:
|
||||
|
||||
def __init__(self):
|
||||
|
@ -304,16 +321,26 @@ class IOTest(unittest.TestCase):
|
|||
|
||||
def test_invalid_operations(self):
|
||||
# Try writing on a file opened in read mode and vice-versa.
|
||||
exc = self.UnsupportedOperation
|
||||
for mode in ("w", "wb"):
|
||||
with self.open(support.TESTFN, mode) as fp:
|
||||
self.assertRaises(IOError, fp.read)
|
||||
self.assertRaises(IOError, fp.readline)
|
||||
self.assertRaises(exc, fp.read)
|
||||
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:
|
||||
self.assertRaises(IOError, fp.write, b"blah")
|
||||
self.assertRaises(IOError, fp.writelines, [b"blah\n"])
|
||||
self.assertRaises(exc, fp.write, b"blah")
|
||||
self.assertRaises(exc, fp.writelines, [b"blah\n"])
|
||||
with self.open(support.TESTFN, "r") as fp:
|
||||
self.assertRaises(IOError, fp.write, "blah")
|
||||
self.assertRaises(IOError, fp.writelines, ["blah\n"])
|
||||
self.assertRaises(exc, fp.write, "blah")
|
||||
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):
|
||||
with self.open(support.TESTFN, "wb", buffering=0) as f:
|
||||
|
@ -670,6 +697,11 @@ class CommonBufferedTests:
|
|||
b.close()
|
||||
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):
|
||||
read_mode = "rb"
|
||||
|
@ -1433,6 +1465,9 @@ class BufferedRandomTest(BufferedReaderTest, BufferedWriterTest):
|
|||
BufferedReaderTest.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):
|
||||
tp = io.BufferedRandom
|
||||
|
||||
|
@ -2177,6 +2212,11 @@ class TextIOWrapperTest(unittest.TestCase):
|
|||
txt.close()
|
||||
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):
|
||||
|
||||
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
|
||||
# classes in the __dict__ of each test.
|
||||
mocks = (MockRawIO, MisbehavedRawIO, MockFileIO, CloseFailureIO,
|
||||
MockNonBlockWriterIO)
|
||||
MockNonBlockWriterIO, MockUnseekableIO)
|
||||
all_members = io.__all__ + ["IncrementalNewlineDecoder"]
|
||||
c_io_ns = {name : getattr(io, 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
|
||||
-------
|
||||
|
||||
- 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?
|
||||
=================================
|
||||
|
|
|
@ -417,7 +417,8 @@ err_closed(void)
|
|||
static PyObject *
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -317,7 +317,7 @@ _PyIOBase_check_seekable(PyObject *self, PyObject *args)
|
|||
return NULL;
|
||||
if (res != Py_True) {
|
||||
Py_CLEAR(res);
|
||||
PyErr_SetString(PyExc_IOError, "File or stream is not seekable.");
|
||||
iobase_unsupported("File or stream is not seekable.");
|
||||
return NULL;
|
||||
}
|
||||
if (args == Py_True) {
|
||||
|
@ -346,7 +346,7 @@ _PyIOBase_check_readable(PyObject *self, PyObject *args)
|
|||
return NULL;
|
||||
if (res != Py_True) {
|
||||
Py_CLEAR(res);
|
||||
PyErr_SetString(PyExc_IOError, "File or stream is not readable.");
|
||||
iobase_unsupported("File or stream is not readable.");
|
||||
return NULL;
|
||||
}
|
||||
if (args == Py_True) {
|
||||
|
@ -375,7 +375,7 @@ _PyIOBase_check_writable(PyObject *self, PyObject *args)
|
|||
return NULL;
|
||||
if (res != Py_True) {
|
||||
Py_CLEAR(res);
|
||||
PyErr_SetString(PyExc_IOError, "File or stream is not writable.");
|
||||
iobase_unsupported("File or stream is not writable.");
|
||||
return NULL;
|
||||
}
|
||||
if (args == Py_True) {
|
||||
|
|
|
@ -1259,10 +1259,8 @@ textiowrapper_write(textio *self, PyObject *args)
|
|||
|
||||
CHECK_CLOSED(self);
|
||||
|
||||
if (self->encoder == NULL) {
|
||||
PyErr_SetString(PyExc_IOError, "not writable");
|
||||
return NULL;
|
||||
}
|
||||
if (self->encoder == NULL)
|
||||
return _unsupported("not writable");
|
||||
|
||||
Py_INCREF(text);
|
||||
|
||||
|
@ -1399,7 +1397,7 @@ textiowrapper_read_chunk(textio *self)
|
|||
*/
|
||||
|
||||
if (self->decoder == NULL) {
|
||||
PyErr_SetString(PyExc_IOError, "not readable");
|
||||
_unsupported("not readable");
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
@ -1489,10 +1487,8 @@ textiowrapper_read(textio *self, PyObject *args)
|
|||
|
||||
CHECK_CLOSED(self);
|
||||
|
||||
if (self->decoder == NULL) {
|
||||
PyErr_SetString(PyExc_IOError, "not readable");
|
||||
return NULL;
|
||||
}
|
||||
if (self->decoder == NULL)
|
||||
return _unsupported("not readable");
|
||||
|
||||
if (_textiowrapper_writeflush(self) < 0)
|
||||
return NULL;
|
||||
|
@ -1983,8 +1979,7 @@ textiowrapper_seek(textio *self, PyObject *args)
|
|||
Py_INCREF(cookieObj);
|
||||
|
||||
if (!self->seekable) {
|
||||
PyErr_SetString(PyExc_IOError,
|
||||
"underlying stream is not seekable");
|
||||
_unsupported("underlying stream is not seekable");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
|
@ -1995,8 +1990,7 @@ textiowrapper_seek(textio *self, PyObject *args)
|
|||
goto fail;
|
||||
|
||||
if (cmp == 0) {
|
||||
PyErr_SetString(PyExc_IOError,
|
||||
"can't do nonzero cur-relative seeks");
|
||||
_unsupported("can't do nonzero cur-relative seeks");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
|
@ -2016,8 +2010,7 @@ textiowrapper_seek(textio *self, PyObject *args)
|
|||
goto fail;
|
||||
|
||||
if (cmp == 0) {
|
||||
PyErr_SetString(PyExc_IOError,
|
||||
"can't do nonzero end-relative seeks");
|
||||
_unsupported("can't do nonzero end-relative seeks");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
|
@ -2151,8 +2144,7 @@ textiowrapper_tell(textio *self, PyObject *args)
|
|||
CHECK_CLOSED(self);
|
||||
|
||||
if (!self->seekable) {
|
||||
PyErr_SetString(PyExc_IOError,
|
||||
"underlying stream is not seekable");
|
||||
_unsupported("underlying stream is not seekable");
|
||||
goto fail;
|
||||
}
|
||||
if (!self->telling) {
|
||||
|
|
Loading…
Reference in New Issue