diff options
-rw-r--r-- | Lib/_pyio.py | 30 | ||||
-rw-r--r-- | Lib/test/test_io.py | 54 | ||||
-rw-r--r-- | Misc/NEWS | 4 | ||||
-rw-r--r-- | Modules/_io/fileio.c | 3 | ||||
-rw-r--r-- | Modules/_io/iobase.c | 6 | ||||
-rw-r--r-- | Modules/_io/textio.c | 26 |
6 files changed, 82 insertions, 41 deletions
diff --git a/Lib/_pyio.py b/Lib/_pyio.py index 66f990d..12ae4b6 100644 --- a/Lib/_pyio.py +++ b/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('') diff --git a/Lib/test/test_io.py b/Lib/test/test_io.py index 76c8536..c9f7582 100644 --- a/Lib/test/test_io.py +++ b/Lib/test/test_io.py @@ -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? ================================= diff --git a/Modules/_io/fileio.c b/Modules/_io/fileio.c index 4f450da..ff278cf 100644 --- a/Modules/_io/fileio.c +++ b/Modules/_io/fileio.c @@ -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; } diff --git a/Modules/_io/iobase.c b/Modules/_io/iobase.c index 2d664ab..6521ae2 100644 --- a/Modules/_io/iobase.c +++ b/Modules/_io/iobase.c @@ -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) { diff --git a/Modules/_io/textio.c b/Modules/_io/textio.c index b659795..08827b9 100644 --- a/Modules/_io/textio.c +++ b/Modules/_io/textio.c @@ -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) { |