Skip to content

Commit 6b4883d

Browse files
committed
PEP 3151 / issue #12555: reworking the OS and IO exception hierarchy.
1 parent 983b143 commit 6b4883d

21 files changed

+676
-441
lines changed

Include/pyerrors.h

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -45,18 +45,18 @@ typedef struct {
4545
PyObject *myerrno;
4646
PyObject *strerror;
4747
PyObject *filename;
48-
} PyEnvironmentErrorObject;
49-
5048
#ifdef MS_WINDOWS
51-
typedef struct {
52-
PyException_HEAD
53-
PyObject *myerrno;
54-
PyObject *strerror;
55-
PyObject *filename;
5649
PyObject *winerror;
57-
} PyWindowsErrorObject;
5850
#endif
51+
Py_ssize_t written; /* only for BlockingIOError, -1 otherwise */
52+
} PyOSErrorObject;
53+
54+
/* Compatibility typedefs */
55+
typedef PyOSErrorObject PyEnvironmentErrorObject;
56+
#ifdef MS_WINDOWS
57+
typedef PyOSErrorObject PyWindowsErrorObject;
5958
#endif
59+
#endif /* !Py_LIMITED_API */
6060

6161
/* Error handling definitions */
6262

@@ -132,10 +132,9 @@ PyAPI_DATA(PyObject *) PyExc_LookupError;
132132

133133
PyAPI_DATA(PyObject *) PyExc_AssertionError;
134134
PyAPI_DATA(PyObject *) PyExc_AttributeError;
135+
PyAPI_DATA(PyObject *) PyExc_BufferError;
135136
PyAPI_DATA(PyObject *) PyExc_EOFError;
136137
PyAPI_DATA(PyObject *) PyExc_FloatingPointError;
137-
PyAPI_DATA(PyObject *) PyExc_EnvironmentError;
138-
PyAPI_DATA(PyObject *) PyExc_IOError;
139138
PyAPI_DATA(PyObject *) PyExc_OSError;
140139
PyAPI_DATA(PyObject *) PyExc_ImportError;
141140
PyAPI_DATA(PyObject *) PyExc_IndexError;
@@ -160,15 +159,34 @@ PyAPI_DATA(PyObject *) PyExc_UnicodeDecodeError;
160159
PyAPI_DATA(PyObject *) PyExc_UnicodeTranslateError;
161160
PyAPI_DATA(PyObject *) PyExc_ValueError;
162161
PyAPI_DATA(PyObject *) PyExc_ZeroDivisionError;
162+
163+
PyAPI_DATA(PyObject *) PyExc_BlockingIOError;
164+
PyAPI_DATA(PyObject *) PyExc_BrokenPipeError;
165+
PyAPI_DATA(PyObject *) PyExc_ChildProcessError;
166+
PyAPI_DATA(PyObject *) PyExc_ConnectionError;
167+
PyAPI_DATA(PyObject *) PyExc_ConnectionAbortedError;
168+
PyAPI_DATA(PyObject *) PyExc_ConnectionRefusedError;
169+
PyAPI_DATA(PyObject *) PyExc_ConnectionResetError;
170+
PyAPI_DATA(PyObject *) PyExc_FileExistsError;
171+
PyAPI_DATA(PyObject *) PyExc_FileNotFoundError;
172+
PyAPI_DATA(PyObject *) PyExc_InterruptedError;
173+
PyAPI_DATA(PyObject *) PyExc_IsADirectoryError;
174+
PyAPI_DATA(PyObject *) PyExc_NotADirectoryError;
175+
PyAPI_DATA(PyObject *) PyExc_PermissionError;
176+
PyAPI_DATA(PyObject *) PyExc_ProcessLookupError;
177+
PyAPI_DATA(PyObject *) PyExc_TimeoutError;
178+
179+
180+
/* Compatibility aliases */
181+
PyAPI_DATA(PyObject *) PyExc_EnvironmentError;
182+
PyAPI_DATA(PyObject *) PyExc_IOError;
163183
#ifdef MS_WINDOWS
164184
PyAPI_DATA(PyObject *) PyExc_WindowsError;
165185
#endif
166186
#ifdef __VMS
167187
PyAPI_DATA(PyObject *) PyExc_VMSError;
168188
#endif
169189

170-
PyAPI_DATA(PyObject *) PyExc_BufferError;
171-
172190
PyAPI_DATA(PyObject *) PyExc_RecursionErrorInst;
173191

174192
/* Predefined warning categories */

Lib/_pyio.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,8 @@
2323
# defined in io.py. We don't use real inheritance though, because we don't
2424
# want to inherit the C implementations.
2525

26-
27-
class BlockingIOError(IOError):
28-
29-
"""Exception raised when I/O would block on a non-blocking I/O stream."""
30-
31-
def __init__(self, errno, strerror, characters_written=0):
32-
super().__init__(errno, strerror)
33-
if not isinstance(characters_written, int):
34-
raise TypeError("characters_written must be a integer")
35-
self.characters_written = characters_written
26+
# Rebind for compatibility
27+
BlockingIOError = BlockingIOError
3628

3729

3830
def open(file, mode="r", buffering=-1, encoding=None, errors=None,

Lib/multiprocessing/connection.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ def _recv_bytes(self, maxsize=None, sentinels=()):
321321
firstchunk = overlapped.getbuffer()
322322
assert lenfirstchunk == len(firstchunk)
323323
except IOError as e:
324-
if e.errno == win32.ERROR_BROKEN_PIPE:
324+
if e.winerror == win32.ERROR_BROKEN_PIPE:
325325
raise EOFError
326326
raise
327327
buf.write(firstchunk)
@@ -669,7 +669,7 @@ def accept(self):
669669
try:
670670
win32.ConnectNamedPipe(handle, win32.NULL)
671671
except WindowsError as e:
672-
if e.args[0] != win32.ERROR_PIPE_CONNECTED:
672+
if e.winerror != win32.ERROR_PIPE_CONNECTED:
673673
raise
674674
return PipeConnection(handle)
675675

@@ -692,8 +692,8 @@ def PipeClient(address):
692692
0, win32.NULL, win32.OPEN_EXISTING, 0, win32.NULL
693693
)
694694
except WindowsError as e:
695-
if e.args[0] not in (win32.ERROR_SEM_TIMEOUT,
696-
win32.ERROR_PIPE_BUSY) or _check_timeout(t):
695+
if e.winerror not in (win32.ERROR_SEM_TIMEOUT,
696+
win32.ERROR_PIPE_BUSY) or _check_timeout(t):
697697
raise
698698
else:
699699
break

Lib/test/exception_hierarchy.txt

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,6 @@ BaseException
1111
+-- AssertionError
1212
+-- AttributeError
1313
+-- BufferError
14-
+-- EnvironmentError
15-
| +-- IOError
16-
| +-- OSError
17-
| +-- WindowsError (Windows)
18-
| +-- VMSError (VMS)
1914
+-- EOFError
2015
+-- ImportError
2116
+-- LookupError
@@ -24,6 +19,22 @@ BaseException
2419
+-- MemoryError
2520
+-- NameError
2621
| +-- UnboundLocalError
22+
+-- OSError
23+
| +-- BlockingIOError
24+
| +-- ChildProcessError
25+
| +-- ConnectionError
26+
| | +-- BrokenPipeError
27+
| | +-- ConnectionAbortedError
28+
| | +-- ConnectionRefusedError
29+
| | +-- ConnectionResetError
30+
| +-- FileExistsError
31+
| +-- FileNotFoundError
32+
| +-- InterruptedError
33+
| +-- IsADirectoryError
34+
| +-- NotADirectoryError
35+
| +-- PermissionError
36+
| +-- ProcessLookupError
37+
| +-- TimeoutError
2738
+-- ReferenceError
2839
+-- RuntimeError
2940
| +-- NotImplementedError

Lib/test/test_concurrent_futures.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def create_future(state=PENDING, exception=None, result=None):
3434
RUNNING_FUTURE = create_future(state=RUNNING)
3535
CANCELLED_FUTURE = create_future(state=CANCELLED)
3636
CANCELLED_AND_NOTIFIED_FUTURE = create_future(state=CANCELLED_AND_NOTIFIED)
37-
EXCEPTION_FUTURE = create_future(state=FINISHED, exception=IOError())
37+
EXCEPTION_FUTURE = create_future(state=FINISHED, exception=OSError())
3838
SUCCESSFUL_FUTURE = create_future(state=FINISHED, result=42)
3939

4040

@@ -501,7 +501,7 @@ def test_repr(self):
501501
'<Future at 0x[0-9a-f]+ state=cancelled>')
502502
self.assertRegex(
503503
repr(EXCEPTION_FUTURE),
504-
'<Future at 0x[0-9a-f]+ state=finished raised IOError>')
504+
'<Future at 0x[0-9a-f]+ state=finished raised OSError>')
505505
self.assertRegex(
506506
repr(SUCCESSFUL_FUTURE),
507507
'<Future at 0x[0-9a-f]+ state=finished returned int>')
@@ -512,7 +512,7 @@ def test_cancel(self):
512512
f2 = create_future(state=RUNNING)
513513
f3 = create_future(state=CANCELLED)
514514
f4 = create_future(state=CANCELLED_AND_NOTIFIED)
515-
f5 = create_future(state=FINISHED, exception=IOError())
515+
f5 = create_future(state=FINISHED, exception=OSError())
516516
f6 = create_future(state=FINISHED, result=5)
517517

518518
self.assertTrue(f1.cancel())
@@ -566,7 +566,7 @@ def test_result_with_timeout(self):
566566
CANCELLED_FUTURE.result, timeout=0)
567567
self.assertRaises(futures.CancelledError,
568568
CANCELLED_AND_NOTIFIED_FUTURE.result, timeout=0)
569-
self.assertRaises(IOError, EXCEPTION_FUTURE.result, timeout=0)
569+
self.assertRaises(OSError, EXCEPTION_FUTURE.result, timeout=0)
570570
self.assertEqual(SUCCESSFUL_FUTURE.result(timeout=0), 42)
571571

572572
def test_result_with_success(self):
@@ -605,7 +605,7 @@ def test_exception_with_timeout(self):
605605
self.assertRaises(futures.CancelledError,
606606
CANCELLED_AND_NOTIFIED_FUTURE.exception, timeout=0)
607607
self.assertTrue(isinstance(EXCEPTION_FUTURE.exception(timeout=0),
608-
IOError))
608+
OSError))
609609
self.assertEqual(SUCCESSFUL_FUTURE.exception(timeout=0), None)
610610

611611
def test_exception_with_success(self):
@@ -614,14 +614,14 @@ def notification():
614614
time.sleep(1)
615615
with f1._condition:
616616
f1._state = FINISHED
617-
f1._exception = IOError()
617+
f1._exception = OSError()
618618
f1._condition.notify_all()
619619

620620
f1 = create_future(state=PENDING)
621621
t = threading.Thread(target=notification)
622622
t.start()
623623

624-
self.assertTrue(isinstance(f1.exception(timeout=5), IOError))
624+
self.assertTrue(isinstance(f1.exception(timeout=5), OSError))
625625

626626
@test.support.reap_threads
627627
def test_main():

Lib/test/test_exceptions.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ def testRaising(self):
4646
fp.close()
4747
unlink(TESTFN)
4848

49-
self.raise_catch(IOError, "IOError")
50-
self.assertRaises(IOError, open, 'this file does not exist', 'r')
49+
self.raise_catch(OSError, "OSError")
50+
self.assertRaises(OSError, open, 'this file does not exist', 'r')
5151

5252
self.raise_catch(ImportError, "ImportError")
5353
self.assertRaises(ImportError, __import__, "undefined_module")
@@ -192,11 +192,35 @@ def test_WindowsError(self):
192192
except NameError:
193193
pass
194194
else:
195-
self.assertEqual(str(WindowsError(1001)), "1001")
196-
self.assertEqual(str(WindowsError(1001, "message")),
197-
"[Error 1001] message")
198-
self.assertEqual(WindowsError(1001, "message").errno, 22)
199-
self.assertEqual(WindowsError(1001, "message").winerror, 1001)
195+
self.assertIs(WindowsError, OSError)
196+
self.assertEqual(str(OSError(1001)), "1001")
197+
self.assertEqual(str(OSError(1001, "message")),
198+
"[Errno 1001] message")
199+
# POSIX errno (9 aka EBADF) is untranslated
200+
w = OSError(9, 'foo', 'bar')
201+
self.assertEqual(w.errno, 9)
202+
self.assertEqual(w.winerror, None)
203+
self.assertEqual(str(w), "[Errno 9] foo: 'bar'")
204+
# ERROR_PATH_NOT_FOUND (win error 3) becomes ENOENT (2)
205+
w = OSError(0, 'foo', 'bar', 3)
206+
self.assertEqual(w.errno, 2)
207+
self.assertEqual(w.winerror, 3)
208+
self.assertEqual(w.strerror, 'foo')
209+
self.assertEqual(w.filename, 'bar')
210+
self.assertEqual(str(w), "[Error 3] foo: 'bar'")
211+
# Unknown win error becomes EINVAL (22)
212+
w = OSError(0, 'foo', None, 1001)
213+
self.assertEqual(w.errno, 22)
214+
self.assertEqual(w.winerror, 1001)
215+
self.assertEqual(w.strerror, 'foo')
216+
self.assertEqual(w.filename, None)
217+
self.assertEqual(str(w), "[Error 1001] foo")
218+
# Non-numeric "errno"
219+
w = OSError('bar', 'foo')
220+
self.assertEqual(w.errno, 'bar')
221+
self.assertEqual(w.winerror, None)
222+
self.assertEqual(w.strerror, 'foo')
223+
self.assertEqual(w.filename, None)
200224

201225
def testAttributes(self):
202226
# test that exception attributes are happy
@@ -274,11 +298,12 @@ def testAttributes(self):
274298
'start' : 0, 'end' : 1}),
275299
]
276300
try:
301+
# More tests are in test_WindowsError
277302
exceptionList.append(
278303
(WindowsError, (1, 'strErrorStr', 'filenameStr'),
279304
{'args' : (1, 'strErrorStr'),
280-
'strerror' : 'strErrorStr', 'winerror' : 1,
281-
'errno' : 22, 'filename' : 'filenameStr'})
305+
'strerror' : 'strErrorStr', 'winerror' : None,
306+
'errno' : 1, 'filename' : 'filenameStr'})
282307
)
283308
except NameError:
284309
pass

Lib/test/test_http_cookiejar.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -248,18 +248,19 @@ def test_lwp_valueless_cookie(self):
248248
self.assertEqual(c._cookies["www.acme.com"]["/"]["boo"].value, None)
249249

250250
def test_bad_magic(self):
251-
# IOErrors (eg. file doesn't exist) are allowed to propagate
251+
# OSErrors (eg. file doesn't exist) are allowed to propagate
252252
filename = test.support.TESTFN
253253
for cookiejar_class in LWPCookieJar, MozillaCookieJar:
254254
c = cookiejar_class()
255255
try:
256256
c.load(filename="for this test to work, a file with this "
257257
"filename should not exist")
258-
except IOError as exc:
259-
# exactly IOError, not LoadError
260-
self.assertIs(exc.__class__, IOError)
258+
except OSError as exc:
259+
# an OSError subclass (likely FileNotFoundError), but not
260+
# LoadError
261+
self.assertIsNot(exc.__class__, LoadError)
261262
else:
262-
self.fail("expected IOError for invalid filename")
263+
self.fail("expected OSError for invalid filename")
263264
# Invalid contents of cookies file (eg. bad magic string)
264265
# causes a LoadError.
265266
try:

Lib/test/test_io.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2643,12 +2643,6 @@ def test_io_after_close(self):
26432643

26442644
def test_blockingioerror(self):
26452645
# Various BlockingIOError issues
2646-
self.assertRaises(TypeError, self.BlockingIOError)
2647-
self.assertRaises(TypeError, self.BlockingIOError, 1)
2648-
self.assertRaises(TypeError, self.BlockingIOError, 1, 2, 3, 4)
2649-
self.assertRaises(TypeError, self.BlockingIOError, 1, "", None)
2650-
b = self.BlockingIOError(1, "")
2651-
self.assertEqual(b.characters_written, 0)
26522646
class C(str):
26532647
pass
26542648
c = C("")

Lib/test/test_mmap.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -563,8 +563,7 @@ def test_prot_readonly(self):
563563
f.close()
564564

565565
def test_error(self):
566-
self.assertTrue(issubclass(mmap.error, EnvironmentError))
567-
self.assertIn("mmap.error", str(mmap.error))
566+
self.assertIs(mmap.error, OSError)
568567

569568
def test_io_methods(self):
570569
data = b"0123456789"

0 commit comments

Comments
 (0)