Kaydet (Commit) 0d739d70 authored tarafından Antoine Pitrou's avatar Antoine Pitrou

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).
üst bad09255
...@@ -243,8 +243,13 @@ class OpenWrapper: ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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: ...@@ -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): ...@@ -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): ...@@ -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(): ...@@ -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 ...@@ -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) ...@@ -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) ...@@ -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) ...@@ -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) ...@@ -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) ...@@ -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) ...@@ -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) ...@@ -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) ...@@ -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) ...@@ -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) ...@@ -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) ...@@ -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) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment