# HG changeset patch # Parent 4e84e45e191b3b063387fdd68075f47f4576e215 Clarify documentation and test for UnsupportedOperation diff -r 4e84e45e191b Doc/library/io.rst --- a/Doc/library/io.rst Fri Dec 19 11:21:56 2014 -0500 +++ b/Doc/library/io.rst Sun Dec 21 05:12:39 2014 +0000 @@ -265,8 +265,8 @@ .. method:: fileno() Return the underlying file descriptor (an integer) of the stream if it - exists. An :exc:`OSError` is raised if the IO object does not use a file - descriptor. + exists. :exc:`UnsupportedOperation` is raised if the IO object + does not use a file descriptor. .. method:: flush() @@ -281,7 +281,7 @@ .. method:: readable() Return ``True`` if the stream can be read from. If ``False``, :meth:`read` - will raise :exc:`OSError`. + will raise :exc:`UnsupportedOperation`. .. method:: readline(size=-1) @@ -345,7 +345,8 @@ .. method:: writable() Return ``True`` if the stream supports writing. If ``False``, - :meth:`write` and :meth:`truncate` will raise :exc:`OSError`. + :meth:`write` and :meth:`truncate` will raise + :exc:`UnsupportedOperation`. .. method:: writelines(lines) diff -r 4e84e45e191b Lib/_pyio.py --- a/Lib/_pyio.py Fri Dec 19 11:21:56 2014 -0500 +++ b/Lib/_pyio.py Sun Dec 21 05:12:39 2014 +0000 @@ -384,7 +384,7 @@ def seekable(self): """Return a bool indicating whether object supports random access. - If False, seek(), tell() and truncate() will raise UnsupportedOperation. + If False, seek(), tell() and truncate() will raise OSError. This method may need to do a test seek(). """ return False @@ -457,7 +457,8 @@ def fileno(self): """Returns underlying file descriptor (an int) if one exists. - An OSError is raised if the IO object does not use a file descriptor. + UnsupportedOperation is raised if the IO object does not use a file + descriptor. """ self._unsupported("fileno") @@ -781,12 +782,6 @@ def seekable(self): return self.raw.seekable() - def readable(self): - return self.raw.readable() - - def writable(self): - return self.raw.writable() - @property def raw(self): return self._raw @@ -970,6 +965,9 @@ self._reset_read_buf() self._read_lock = Lock() + def readable(self): + return self.raw.readable() + def _reset_read_buf(self): self._read_buf = b"" self._read_pos = 0 @@ -1169,6 +1167,9 @@ self._write_buf = bytearray() self._write_lock = Lock() + def writable(self): + return self.raw.writable() + def write(self, b): if self.closed: raise ValueError("write to closed file") diff -r 4e84e45e191b Lib/test/test_io.py --- a/Lib/test/test_io.py Fri Dec 19 11:21:56 2014 -0500 +++ b/Lib/test/test_io.py Sun Dec 21 05:12:39 2014 +0000 @@ -203,6 +203,9 @@ def tell(self, *args): raise self.UnsupportedOperation("not seekable") + def truncate(self, *args): + raise self.UnsupportedOperation("not seekable") + class CMockUnseekableIO(MockUnseekableIO, io.BytesIO): UnsupportedOperation = io.UnsupportedOperation @@ -361,6 +364,85 @@ self.assertRaises(exc, fp.seek, 1, self.SEEK_CUR) self.assertRaises(exc, fp.seek, -1, self.SEEK_END) + def test_optional_abilities(self): + # Test for UnsupportedOperation when optional APIs are not supported + def pipe_reader(): + [r, w] = os.pipe() + os.close(w) # So that read() is harmless + return self.FileIO(r, "r") + def pipe_writer(): + [r, w] = os.pipe() + self.addCleanup(os.close, r) # Ensure small write() is possible + return self.FileIO(w, "w") + def buffered_reader(): + return self.BufferedReader(self.MockUnseekableIO()) + def buffered_writer(): + return self.BufferedWriter(self.MockUnseekableIO()) + def buffered_random(): + return self.BufferedRandom(self.BytesIO()) + def buffered_rw_pair(): + return self.BufferedRWPair(self.MockUnseekableIO(), + self.MockUnseekableIO()) + def text_reader(): + class UnseekableReader(self.MockUnseekableIO): + writable = self.BufferedIOBase.writable + write = self.BufferedIOBase.write + return self.TextIOWrapper(UnseekableReader()) + def text_writer(): + class UnseekableWriter(self.MockUnseekableIO): + readable = self.BufferedIOBase.readable + read = self.BufferedIOBase.read + return self.TextIOWrapper(UnseekableWriter()) + tests = ( + (pipe_reader, "fr"), (pipe_writer, "fw"), + (buffered_reader, "r"), (buffered_writer, "w"), + (buffered_random, "rws"), (buffered_rw_pair, "rw"), + (text_reader, "r"), (text_writer, "w"), + (self.BytesIO, "rws"), (self.StringIO, "rws"), + ) + for [test, abilities] in tests: + with self.subTest(test), test() as obj: + readable = obj.readable() + self.assertEqual("r" in abilities, readable) + writable = obj.writable() + self.assertEqual("w" in abilities, writable) + seekable = obj.seekable() + self.assertEqual("s" in abilities, seekable) + if isinstance(obj, self.TextIOBase): + data = "3" + elif isinstance(obj, (self.BufferedIOBase, self.RawIOBase)): + data = b"3" + else: + self.fail("Unknown base class") + + if "f" in abilities: + obj.fileno() + else: + self.assertRaises(self.UnsupportedOperation, obj.fileno) + if readable: + obj.read(1) + obj.read() + else: + self.assertRaises(self.UnsupportedOperation, obj.read, 1) + self.assertRaises(self.UnsupportedOperation, obj.read) + if writable: + obj.write(data) + else: + self.assertRaises(self.UnsupportedOperation, + obj.write, data) + if seekable: + obj.tell() + obj.seek(0) + else: + self.assertRaises(OSError, obj.tell) + self.assertRaises(OSError, obj.seek, 0) + if writable and seekable: + obj.truncate() + obj.truncate(0) + else: + self.assertRaises(OSError, obj.truncate) + self.assertRaises(OSError, obj.truncate, 0) + def test_open_handles_NUL_chars(self): fn_with_NUL = 'foo\0bar' self.assertRaises(ValueError, self.open, fn_with_NUL, 'w') @@ -736,13 +818,9 @@ super().flush() rawio = self.MockRawIO() bufio = MyBufferedIO(rawio) - writable = bufio.writable() del bufio support.gc_collect() - if writable: - self.assertEqual(record, [1, 2, 3]) - else: - self.assertEqual(record, [1, 2]) + self.assertEqual(record, [1, 2, 3]) def test_context_manager(self): # Test usability as a context manager diff -r 4e84e45e191b Modules/_io/bufferedio.c --- a/Modules/_io/bufferedio.c Fri Dec 19 11:21:56 2014 -0500 +++ b/Modules/_io/bufferedio.c Sun Dec 21 05:12:39 2014 +0000 @@ -1773,7 +1773,6 @@ {"close", (PyCFunction)buffered_close, METH_NOARGS}, {"seekable", (PyCFunction)buffered_seekable, METH_NOARGS}, {"readable", (PyCFunction)buffered_readable, METH_NOARGS}, - {"writable", (PyCFunction)buffered_writable, METH_NOARGS}, {"fileno", (PyCFunction)buffered_fileno, METH_NOARGS}, {"isatty", (PyCFunction)buffered_isatty, METH_NOARGS}, {"_dealloc_warn", (PyCFunction)buffered_dealloc_warn, METH_O}, @@ -2168,7 +2167,6 @@ {"close", (PyCFunction)buffered_close, METH_NOARGS}, {"detach", (PyCFunction)buffered_detach, METH_NOARGS}, {"seekable", (PyCFunction)buffered_seekable, METH_NOARGS}, - {"readable", (PyCFunction)buffered_readable, METH_NOARGS}, {"writable", (PyCFunction)buffered_writable, METH_NOARGS}, {"fileno", (PyCFunction)buffered_fileno, METH_NOARGS}, {"isatty", (PyCFunction)buffered_isatty, METH_NOARGS}, diff -r 4e84e45e191b Modules/_io/fileio.c --- a/Modules/_io/fileio.c Fri Dec 19 11:21:56 2014 -0500 +++ b/Modules/_io/fileio.c Sun Dec 21 05:12:39 2014 +0000 @@ -1109,13 +1109,13 @@ "\n" "Only makes one system call, so less data may be returned than requested\n" "In non-blocking mode, returns None if no data is available.\n" -"On end-of-file, returns ''."); +"On end-of-file, returns b''."); PyDoc_STRVAR(readall_doc, "readall() -> bytes. read all data from the file, returned as bytes.\n" "\n" "In non-blocking mode, returns as much as is immediately available,\n" -"or None if no data is available. On end-of-file, returns ''."); +"or None if no data is available. On end-of-file, returns b''."); PyDoc_STRVAR(write_doc, "write(b: bytes) -> int. Write bytes b to file, return number written.\n" diff -r 4e84e45e191b Modules/_io/iobase.c --- a/Modules/_io/iobase.c Fri Dec 19 11:21:56 2014 -0500 +++ b/Modules/_io/iobase.c Sun Dec 21 05:12:39 2014 +0000 @@ -307,7 +307,7 @@ PyDoc_STRVAR(iobase_seekable_doc, "Return whether object supports random access.\n" "\n" - "If False, seek(), tell() and truncate() will raise UnsupportedOperation.\n" + "If False, seek(), tell() and truncate() will raise OSError.\n" "This method may need to do a test seek()."); static PyObject * @@ -416,7 +416,8 @@ PyDoc_STRVAR(iobase_fileno_doc, "Returns underlying file descriptor if one exists.\n" "\n" - "An IOError is raised if the IO object does not use a file descriptor.\n"); + "UnsupportedOperation is raised if the IO object does not use a file\n" + "descriptor.\n"); static PyObject * iobase_fileno(PyObject *self, PyObject *args)