Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0d739d7

Browse files
committed
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).
1 parent bad0925 commit 0d739d7

6 files changed

Lines changed: 82 additions & 41 deletions

File tree

Lib/_pyio.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -243,8 +243,13 @@ def __new__(cls, *args, **kwargs):
243243
return open(*args, **kwargs)
244244

245245

246-
class UnsupportedOperation(ValueError, IOError):
247-
pass
246+
# In normal operation, both `UnsupportedOperation`s should be bound to the
247+
# same object.
248+
try:
249+
UnsupportedOperation = io.UnsupportedOperation
250+
except AttributeError:
251+
class UnsupportedOperation(ValueError, IOError):
252+
pass
248253

249254

250255
class IOBase(metaclass=abc.ABCMeta):
@@ -362,9 +367,8 @@ def _checkSeekable(self, msg=None):
362367
"""Internal: raise an IOError if file is not seekable
363368
"""
364369
if not self.seekable():
365-
raise IOError("File or stream is not seekable."
366-
if msg is None else msg)
367-
370+
raise UnsupportedOperation("File or stream is not seekable."
371+
if msg is None else msg)
368372

369373
def readable(self) -> bool:
370374
"""Return whether object was opened for reading.
@@ -377,8 +381,8 @@ def _checkReadable(self, msg=None):
377381
"""Internal: raise an IOError if file is not readable
378382
"""
379383
if not self.readable():
380-
raise IOError("File or stream is not readable."
381-
if msg is None else msg)
384+
raise UnsupportedOperation("File or stream is not readable."
385+
if msg is None else msg)
382386

383387
def writable(self) -> bool:
384388
"""Return whether object was opened for writing.
@@ -391,8 +395,8 @@ def _checkWritable(self, msg=None):
391395
"""Internal: raise an IOError if file is not writable
392396
"""
393397
if not self.writable():
394-
raise IOError("File or stream is not writable."
395-
if msg is None else msg)
398+
raise UnsupportedOperation("File or stream is not writable."
399+
if msg is None else msg)
396400

397401
@property
398402
def closed(self):
@@ -1647,7 +1651,7 @@ def _unpack_cookie(self, bigint):
16471651

16481652
def tell(self):
16491653
if not self._seekable:
1650-
raise IOError("underlying stream is not seekable")
1654+
raise UnsupportedOperation("underlying stream is not seekable")
16511655
if not self._telling:
16521656
raise IOError("telling position disabled by next() call")
16531657
self.flush()
@@ -1726,17 +1730,17 @@ def seek(self, cookie, whence=0):
17261730
if self.closed:
17271731
raise ValueError("tell on closed file")
17281732
if not self._seekable:
1729-
raise IOError("underlying stream is not seekable")
1733+
raise UnsupportedOperation("underlying stream is not seekable")
17301734
if whence == 1: # seek relative to current position
17311735
if cookie != 0:
1732-
raise IOError("can't do nonzero cur-relative seeks")
1736+
raise UnsupportedOperation("can't do nonzero cur-relative seeks")
17331737
# Seeking to the current position should attempt to
17341738
# sync the underlying buffer with the current position.
17351739
whence = 0
17361740
cookie = self.tell()
17371741
if whence == 2: # seek relative to end of file
17381742
if cookie != 0:
1739-
raise IOError("can't do nonzero end-relative seeks")
1743+
raise UnsupportedOperation("can't do nonzero end-relative seeks")
17401744
self.flush()
17411745
position = self.buffer.seek(0, 2)
17421746
self._set_decoded_chars('')

Lib/test/test_io.py

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,23 @@ class PyMockFileIO(MockFileIO, pyio.BytesIO):
179179
pass
180180

181181

182+
class MockUnseekableIO:
183+
def seekable(self):
184+
return False
185+
186+
def seek(self, *args):
187+
raise self.UnsupportedOperation("not seekable")
188+
189+
def tell(self, *args):
190+
raise self.UnsupportedOperation("not seekable")
191+
192+
class CMockUnseekableIO(MockUnseekableIO, io.BytesIO):
193+
UnsupportedOperation = io.UnsupportedOperation
194+
195+
class PyMockUnseekableIO(MockUnseekableIO, pyio.BytesIO):
196+
UnsupportedOperation = pyio.UnsupportedOperation
197+
198+
182199
class MockNonBlockWriterIO:
183200

184201
def __init__(self):
@@ -304,16 +321,26 @@ def large_file_ops(self, f):
304321

305322
def test_invalid_operations(self):
306323
# Try writing on a file opened in read mode and vice-versa.
324+
exc = self.UnsupportedOperation
307325
for mode in ("w", "wb"):
308326
with self.open(support.TESTFN, mode) as fp:
309-
self.assertRaises(IOError, fp.read)
310-
self.assertRaises(IOError, fp.readline)
327+
self.assertRaises(exc, fp.read)
328+
self.assertRaises(exc, fp.readline)
329+
with self.open(support.TESTFN, "wb", buffering=0) as fp:
330+
self.assertRaises(exc, fp.read)
331+
self.assertRaises(exc, fp.readline)
332+
with self.open(support.TESTFN, "rb", buffering=0) as fp:
333+
self.assertRaises(exc, fp.write, b"blah")
334+
self.assertRaises(exc, fp.writelines, [b"blah\n"])
311335
with self.open(support.TESTFN, "rb") as fp:
312-
self.assertRaises(IOError, fp.write, b"blah")
313-
self.assertRaises(IOError, fp.writelines, [b"blah\n"])
336+
self.assertRaises(exc, fp.write, b"blah")
337+
self.assertRaises(exc, fp.writelines, [b"blah\n"])
314338
with self.open(support.TESTFN, "r") as fp:
315-
self.assertRaises(IOError, fp.write, "blah")
316-
self.assertRaises(IOError, fp.writelines, ["blah\n"])
339+
self.assertRaises(exc, fp.write, "blah")
340+
self.assertRaises(exc, fp.writelines, ["blah\n"])
341+
# Non-zero seeking from current or end pos
342+
self.assertRaises(exc, fp.seek, 1, self.SEEK_CUR)
343+
self.assertRaises(exc, fp.seek, -1, self.SEEK_END)
317344

318345
def test_raw_file_io(self):
319346
with self.open(support.TESTFN, "wb", buffering=0) as f:
@@ -670,6 +697,11 @@ def test_multi_close(self):
670697
b.close()
671698
self.assertRaises(ValueError, b.flush)
672699

700+
def test_unseekable(self):
701+
bufio = self.tp(self.MockUnseekableIO(b"A" * 10))
702+
self.assertRaises(self.UnsupportedOperation, bufio.tell)
703+
self.assertRaises(self.UnsupportedOperation, bufio.seek, 0)
704+
673705

674706
class BufferedReaderTest(unittest.TestCase, CommonBufferedTests):
675707
read_mode = "rb"
@@ -1433,6 +1465,9 @@ def test_misbehaved_io(self):
14331465
BufferedReaderTest.test_misbehaved_io(self)
14341466
BufferedWriterTest.test_misbehaved_io(self)
14351467

1468+
# You can't construct a BufferedRandom over a non-seekable stream.
1469+
test_unseekable = None
1470+
14361471
class CBufferedRandomTest(BufferedRandomTest):
14371472
tp = io.BufferedRandom
14381473

@@ -2177,6 +2212,11 @@ def test_multi_close(self):
21772212
txt.close()
21782213
self.assertRaises(ValueError, txt.flush)
21792214

2215+
def test_unseekable(self):
2216+
txt = self.TextIOWrapper(self.MockUnseekableIO(self.testdata))
2217+
self.assertRaises(self.UnsupportedOperation, txt.tell)
2218+
self.assertRaises(self.UnsupportedOperation, txt.seek, 0)
2219+
21802220
class CTextIOWrapperTest(TextIOWrapperTest):
21812221

21822222
def test_initialization(self):
@@ -2550,7 +2590,7 @@ def test_main():
25502590
# Put the namespaces of the IO module we are testing and some useful mock
25512591
# classes in the __dict__ of each test.
25522592
mocks = (MockRawIO, MisbehavedRawIO, MockFileIO, CloseFailureIO,
2553-
MockNonBlockWriterIO)
2593+
MockNonBlockWriterIO, MockUnseekableIO)
25542594
all_members = io.__all__ + ["IncrementalNewlineDecoder"]
25552595
c_io_ns = {name : getattr(io, name) for name in all_members}
25562596
py_io_ns = {name : getattr(pyio, name) for name in all_members}

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ Core and Builtins
1313
Library
1414
-------
1515

16+
- Issue #9293: I/O streams now raise ``io.UnsupportedOperation`` when an
17+
unsupported operation is attempted (for example, writing to a file open
18+
only for reading).
19+
1620

1721
What's New in Python 3.2 Alpha 2?
1822
=================================

Modules/_io/fileio.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,8 @@ err_closed(void)
417417
static PyObject *
418418
err_mode(char *action)
419419
{
420-
PyErr_Format(PyExc_ValueError, "File not open for %s", action);
420+
PyErr_Format(IO_STATE->unsupported_operation,
421+
"File not open for %s", action);
421422
return NULL;
422423
}
423424

Modules/_io/iobase.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ _PyIOBase_check_seekable(PyObject *self, PyObject *args)
317317
return NULL;
318318
if (res != Py_True) {
319319
Py_CLEAR(res);
320-
PyErr_SetString(PyExc_IOError, "File or stream is not seekable.");
320+
iobase_unsupported("File or stream is not seekable.");
321321
return NULL;
322322
}
323323
if (args == Py_True) {
@@ -346,7 +346,7 @@ _PyIOBase_check_readable(PyObject *self, PyObject *args)
346346
return NULL;
347347
if (res != Py_True) {
348348
Py_CLEAR(res);
349-
PyErr_SetString(PyExc_IOError, "File or stream is not readable.");
349+
iobase_unsupported("File or stream is not readable.");
350350
return NULL;
351351
}
352352
if (args == Py_True) {
@@ -375,7 +375,7 @@ _PyIOBase_check_writable(PyObject *self, PyObject *args)
375375
return NULL;
376376
if (res != Py_True) {
377377
Py_CLEAR(res);
378-
PyErr_SetString(PyExc_IOError, "File or stream is not writable.");
378+
iobase_unsupported("File or stream is not writable.");
379379
return NULL;
380380
}
381381
if (args == Py_True) {

Modules/_io/textio.c

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1259,10 +1259,8 @@ textiowrapper_write(textio *self, PyObject *args)
12591259

12601260
CHECK_CLOSED(self);
12611261

1262-
if (self->encoder == NULL) {
1263-
PyErr_SetString(PyExc_IOError, "not writable");
1264-
return NULL;
1265-
}
1262+
if (self->encoder == NULL)
1263+
return _unsupported("not writable");
12661264

12671265
Py_INCREF(text);
12681266

@@ -1399,7 +1397,7 @@ textiowrapper_read_chunk(textio *self)
13991397
*/
14001398

14011399
if (self->decoder == NULL) {
1402-
PyErr_SetString(PyExc_IOError, "not readable");
1400+
_unsupported("not readable");
14031401
return -1;
14041402
}
14051403

@@ -1489,10 +1487,8 @@ textiowrapper_read(textio *self, PyObject *args)
14891487

14901488
CHECK_CLOSED(self);
14911489

1492-
if (self->decoder == NULL) {
1493-
PyErr_SetString(PyExc_IOError, "not readable");
1494-
return NULL;
1495-
}
1490+
if (self->decoder == NULL)
1491+
return _unsupported("not readable");
14961492

14971493
if (_textiowrapper_writeflush(self) < 0)
14981494
return NULL;
@@ -1983,8 +1979,7 @@ textiowrapper_seek(textio *self, PyObject *args)
19831979
Py_INCREF(cookieObj);
19841980

19851981
if (!self->seekable) {
1986-
PyErr_SetString(PyExc_IOError,
1987-
"underlying stream is not seekable");
1982+
_unsupported("underlying stream is not seekable");
19881983
goto fail;
19891984
}
19901985

@@ -1995,8 +1990,7 @@ textiowrapper_seek(textio *self, PyObject *args)
19951990
goto fail;
19961991

19971992
if (cmp == 0) {
1998-
PyErr_SetString(PyExc_IOError,
1999-
"can't do nonzero cur-relative seeks");
1993+
_unsupported("can't do nonzero cur-relative seeks");
20001994
goto fail;
20011995
}
20021996

@@ -2016,8 +2010,7 @@ textiowrapper_seek(textio *self, PyObject *args)
20162010
goto fail;
20172011

20182012
if (cmp == 0) {
2019-
PyErr_SetString(PyExc_IOError,
2020-
"can't do nonzero end-relative seeks");
2013+
_unsupported("can't do nonzero end-relative seeks");
20212014
goto fail;
20222015
}
20232016

@@ -2151,8 +2144,7 @@ textiowrapper_tell(textio *self, PyObject *args)
21512144
CHECK_CLOSED(self);
21522145

21532146
if (!self->seekable) {
2154-
PyErr_SetString(PyExc_IOError,
2155-
"underlying stream is not seekable");
2147+
_unsupported("underlying stream is not seekable");
21562148
goto fail;
21572149
}
21582150
if (!self->telling) {

0 commit comments

Comments
 (0)