Skip to content

Commit 24153ce

Browse files
committed
bpo-12029: Exception handling now matches subclasses, not subtypes
1 parent 04e8293 commit 24153ce

5 files changed

Lines changed: 81 additions & 7 deletions

File tree

Doc/library/exceptions.rst

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ Built-in Exceptions
88
statement: except
99

1010
In Python, all exceptions must be instances of a class that derives from
11-
:class:`BaseException`. In a :keyword:`try` statement with an :keyword:`except`
12-
clause that mentions a particular class, that clause also handles any exception
13-
classes derived from that class (but not exception classes from which *it* is
14-
derived). Two exception classes that are not related via subclassing are never
15-
equivalent, even if they have the same name.
11+
:class:`BaseException`. A :keyword:`try` statement with an :keyword:`except`
12+
clause that mentions a class handles exceptions of that class and its
13+
subclasses (as defined by :func:`issubclass`). Two exception classes that
14+
are not related via subclassing are never equivalent, even if they have the
15+
same name.
1616

1717
.. index:: statement: raise
1818

Lib/test/test_exceptions.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import pickle
88
import weakref
99
import errno
10+
import abc
1011

1112
from test.support import (TESTFN, captured_stderr, check_impl_detail,
1213
check_warnings, cpython_only, gc_collect, run_unittest,
@@ -1337,6 +1338,53 @@ def test_copy_pickle(self):
13371338
self.assertEqual(exc.name, orig.name)
13381339
self.assertEqual(exc.path, orig.path)
13391340

1341+
def test_exception_registration(self):
1342+
# See issue12029
1343+
class A(Exception, metaclass=abc.ABCMeta):
1344+
pass
1345+
class B(Exception):
1346+
pass
1347+
A.register(B)
1348+
try:
1349+
raise B
1350+
except A:
1351+
pass
1352+
except B:
1353+
self.fail("Caught B, should have caught A")
1354+
1355+
def test_exception_subclasscheck(self):
1356+
# Test that a bad __subclasscheck__ does not cause failure
1357+
class RaisingMeta(type):
1358+
def __subclasscheck__(self, cls):
1359+
raise RuntimeError
1360+
class RaisingCatchingMeta(type):
1361+
def __subclasscheck__(self, cls):
1362+
try:
1363+
raise RuntimeError
1364+
except RuntimeError:
1365+
pass
1366+
def recursion():
1367+
return recursion()
1368+
class RecursingMeta(type):
1369+
def __subclasscheck__(self, cls):
1370+
recursion()
1371+
reclimit = sys.getrecursionlimit()
1372+
try:
1373+
sys.setrecursionlimit(1000)
1374+
for metaclass in [RaisingMeta, RaisingCatchingMeta, RecursingMeta]:
1375+
BadException = metaclass('BadException', (Exception,), {})
1376+
try:
1377+
raise ValueError
1378+
# this handler should cause an exception that is ignored
1379+
except BadException:
1380+
self.fail('caught a bad exception but shouldn\'t')
1381+
except ValueError:
1382+
pass
1383+
else:
1384+
self.fail('ValueError not raised')
1385+
finally:
1386+
sys.setrecursionlimit(reclimit)
1387+
13401388

13411389
if __name__ == '__main__':
13421390
unittest.main()

Lib/unittest/test/test_assertions.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import abc
12
import datetime
23
import warnings
34
import weakref
@@ -370,6 +371,16 @@ def testAssertRaises(self):
370371
'^TypeError not raised$',
371372
'^TypeError not raised : oops$'])
372373

374+
def testAssertRaisesWithMetaClass(self):
375+
# See issue33271
376+
class A(Exception, metaclass=abc.ABCMeta):
377+
pass
378+
class B(Exception):
379+
pass
380+
A.register(B)
381+
with self.assertRaises(A, msg="Exception B should be interpreted as A"):
382+
raise B
383+
373384
def testAssertRaisesRegex(self):
374385
# test error not raised
375386
self.assertMessagesCM('assertRaisesRegex', (TypeError, 'unused regex'),
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix bug where exception matching fails to match virtual subclasses because
2+
``PyErr_GivenExceptionMatches`` used ``PyType_IsSubtype`` instead of
3+
``PyObject_IsSubclass``.

Python/errors.c

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,20 @@ PyErr_GivenExceptionMatches(PyObject *err, PyObject *exc)
203203
if (PyExceptionInstance_Check(err))
204204
err = PyExceptionInstance_Class(err);
205205

206-
if (PyExceptionClass_Check(err) && PyExceptionClass_Check(exc)) {
207-
return PyType_IsSubtype((PyTypeObject *)err, (PyTypeObject *)exc);
206+
if (PyExceptionClass_Check(err) && PyExceptionClass_Check(exc)) {
207+
int res = 0;
208+
PyObject *exception, *value, *tb;
209+
PyErr_Fetch(&exception, &value, &tb);
210+
/* PyObject_IsSubclass() can recurse and therefore is
211+
not safe (see test_bad_getattr in test.pickletester). */
212+
res = PyObject_IsSubclass(err, exc);
213+
/* This function must not fail, so print the error here */
214+
if (res == -1) {
215+
PyErr_WriteUnraisable(err);
216+
res = 0;
217+
}
218+
PyErr_Restore(exception, value, tb);
219+
return res;
208220
}
209221

210222
return err == exc;

0 commit comments

Comments
 (0)