Skip to content

Commit c43714f

Browse files
authored
gh-99181: fix except* on unhashable exceptions (GH-99192)
1 parent a751bf5 commit c43714f

File tree

3 files changed

+226
-17
lines changed

3 files changed

+226
-17
lines changed

Lib/test/test_except_star.py

+199
Original file line numberDiff line numberDiff line change
@@ -1000,5 +1000,204 @@ def test_exc_info_restored(self):
10001000
self.assertEqual(sys.exc_info(), (None, None, None))
10011001

10021002

1003+
class TestExceptStar_WeirdLeafExceptions(ExceptStarTest):
1004+
# Test that except* works when leaf exceptions are
1005+
# unhashable or have a bad custom __eq__
1006+
1007+
class UnhashableExc(ValueError):
1008+
__hash__ = None
1009+
1010+
class AlwaysEqualExc(ValueError):
1011+
def __eq__(self, other):
1012+
return True
1013+
1014+
class NeverEqualExc(ValueError):
1015+
def __eq__(self, other):
1016+
return False
1017+
1018+
class BrokenEqualExc(ValueError):
1019+
def __eq__(self, other):
1020+
raise RuntimeError()
1021+
1022+
def setUp(self):
1023+
self.bad_types = [self.UnhashableExc,
1024+
self.AlwaysEqualExc,
1025+
self.NeverEqualExc,
1026+
self.BrokenEqualExc]
1027+
1028+
def except_type(self, eg, type):
1029+
match, rest = None, None
1030+
try:
1031+
try:
1032+
raise eg
1033+
except* type as e:
1034+
match = e
1035+
except Exception as e:
1036+
rest = e
1037+
return match, rest
1038+
1039+
def test_catch_unhashable_leaf_exception(self):
1040+
for Bad in self.bad_types:
1041+
with self.subTest(Bad):
1042+
eg = ExceptionGroup("eg", [TypeError(1), Bad(2)])
1043+
match, rest = self.except_type(eg, Bad)
1044+
self.assertExceptionIsLike(
1045+
match, ExceptionGroup("eg", [Bad(2)]))
1046+
self.assertExceptionIsLike(
1047+
rest, ExceptionGroup("eg", [TypeError(1)]))
1048+
1049+
def test_propagate_unhashable_leaf(self):
1050+
for Bad in self.bad_types:
1051+
with self.subTest(Bad):
1052+
eg = ExceptionGroup("eg", [TypeError(1), Bad(2)])
1053+
match, rest = self.except_type(eg, TypeError)
1054+
self.assertExceptionIsLike(
1055+
match, ExceptionGroup("eg", [TypeError(1)]))
1056+
self.assertExceptionIsLike(
1057+
rest, ExceptionGroup("eg", [Bad(2)]))
1058+
1059+
def test_catch_nothing_unhashable_leaf(self):
1060+
for Bad in self.bad_types:
1061+
with self.subTest(Bad):
1062+
eg = ExceptionGroup("eg", [TypeError(1), Bad(2)])
1063+
match, rest = self.except_type(eg, OSError)
1064+
self.assertIsNone(match)
1065+
self.assertExceptionIsLike(rest, eg)
1066+
1067+
def test_catch_everything_unhashable_leaf(self):
1068+
for Bad in self.bad_types:
1069+
with self.subTest(Bad):
1070+
eg = ExceptionGroup("eg", [TypeError(1), Bad(2)])
1071+
match, rest = self.except_type(eg, Exception)
1072+
self.assertExceptionIsLike(match, eg)
1073+
self.assertIsNone(rest)
1074+
1075+
def test_reraise_unhashable_leaf(self):
1076+
for Bad in self.bad_types:
1077+
with self.subTest(Bad):
1078+
eg = ExceptionGroup(
1079+
"eg", [TypeError(1), Bad(2), ValueError(3)])
1080+
1081+
try:
1082+
try:
1083+
raise eg
1084+
except* TypeError:
1085+
pass
1086+
except* Bad:
1087+
raise
1088+
except Exception as e:
1089+
exc = e
1090+
1091+
self.assertExceptionIsLike(
1092+
exc, ExceptionGroup("eg", [Bad(2), ValueError(3)]))
1093+
1094+
1095+
class TestExceptStar_WeirdExceptionGroupSubclass(ExceptStarTest):
1096+
# Test that except* works with exception groups that are
1097+
# unhashable or have a bad custom __eq__
1098+
1099+
class UnhashableEG(ExceptionGroup):
1100+
__hash__ = None
1101+
1102+
def derive(self, excs):
1103+
return type(self)(self.message, excs)
1104+
1105+
class AlwaysEqualEG(ExceptionGroup):
1106+
def __eq__(self, other):
1107+
return True
1108+
1109+
def derive(self, excs):
1110+
return type(self)(self.message, excs)
1111+
1112+
class NeverEqualEG(ExceptionGroup):
1113+
def __eq__(self, other):
1114+
return False
1115+
1116+
def derive(self, excs):
1117+
return type(self)(self.message, excs)
1118+
1119+
class BrokenEqualEG(ExceptionGroup):
1120+
def __eq__(self, other):
1121+
raise RuntimeError()
1122+
1123+
def derive(self, excs):
1124+
return type(self)(self.message, excs)
1125+
1126+
def setUp(self):
1127+
self.bad_types = [self.UnhashableEG,
1128+
self.AlwaysEqualEG,
1129+
self.NeverEqualEG,
1130+
self.BrokenEqualEG]
1131+
1132+
def except_type(self, eg, type):
1133+
match, rest = None, None
1134+
try:
1135+
try:
1136+
raise eg
1137+
except* type as e:
1138+
match = e
1139+
except Exception as e:
1140+
rest = e
1141+
return match, rest
1142+
1143+
def test_catch_some_unhashable_exception_group_subclass(self):
1144+
for BadEG in self.bad_types:
1145+
with self.subTest(BadEG):
1146+
eg = BadEG("eg",
1147+
[TypeError(1),
1148+
BadEG("nested", [ValueError(2)])])
1149+
1150+
match, rest = self.except_type(eg, TypeError)
1151+
self.assertExceptionIsLike(match, BadEG("eg", [TypeError(1)]))
1152+
self.assertExceptionIsLike(rest,
1153+
BadEG("eg", [BadEG("nested", [ValueError(2)])]))
1154+
1155+
def test_catch_none_unhashable_exception_group_subclass(self):
1156+
for BadEG in self.bad_types:
1157+
with self.subTest(BadEG):
1158+
1159+
eg = BadEG("eg",
1160+
[TypeError(1),
1161+
BadEG("nested", [ValueError(2)])])
1162+
1163+
match, rest = self.except_type(eg, OSError)
1164+
self.assertIsNone(match)
1165+
self.assertExceptionIsLike(rest, eg)
1166+
1167+
def test_catch_all_unhashable_exception_group_subclass(self):
1168+
for BadEG in self.bad_types:
1169+
with self.subTest(BadEG):
1170+
1171+
eg = BadEG("eg",
1172+
[TypeError(1),
1173+
BadEG("nested", [ValueError(2)])])
1174+
1175+
match, rest = self.except_type(eg, Exception)
1176+
self.assertExceptionIsLike(match, eg)
1177+
self.assertIsNone(rest)
1178+
1179+
def test_reraise_unhashable_eg(self):
1180+
for BadEG in self.bad_types:
1181+
with self.subTest(BadEG):
1182+
1183+
eg = BadEG("eg",
1184+
[TypeError(1), ValueError(2),
1185+
BadEG("nested", [ValueError(3), OSError(4)])])
1186+
1187+
try:
1188+
try:
1189+
raise eg
1190+
except* ValueError:
1191+
pass
1192+
except* OSError:
1193+
raise
1194+
except Exception as e:
1195+
exc = e
1196+
1197+
self.assertExceptionIsLike(
1198+
exc, BadEG("eg", [TypeError(1),
1199+
BadEG("nested", [OSError(4)])]))
1200+
1201+
10031202
if __name__ == '__main__':
10041203
unittest.main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix failure in :keyword:`except* <except_star>` with unhashable exceptions.

Objects/exceptions.c

+26-17
Original file line numberDiff line numberDiff line change
@@ -962,11 +962,11 @@ typedef enum {
962962
EXCEPTION_GROUP_MATCH_BY_TYPE = 0,
963963
/* A PyFunction returning True for matching exceptions */
964964
EXCEPTION_GROUP_MATCH_BY_PREDICATE = 1,
965-
/* A set of leaf exceptions to include in the result.
965+
/* A set of the IDs of leaf exceptions to include in the result.
966966
* This matcher type is used internally by the interpreter
967967
* to construct reraised exceptions.
968968
*/
969-
EXCEPTION_GROUP_MATCH_INSTANCES = 2
969+
EXCEPTION_GROUP_MATCH_INSTANCE_IDS = 2
970970
} _exceptiongroup_split_matcher_type;
971971

972972
static int
@@ -1024,10 +1024,16 @@ exceptiongroup_split_check_match(PyObject *exc,
10241024
Py_DECREF(exc_matches);
10251025
return is_true;
10261026
}
1027-
case EXCEPTION_GROUP_MATCH_INSTANCES: {
1027+
case EXCEPTION_GROUP_MATCH_INSTANCE_IDS: {
10281028
assert(PySet_Check(matcher_value));
10291029
if (!_PyBaseExceptionGroup_Check(exc)) {
1030-
return PySet_Contains(matcher_value, exc);
1030+
PyObject *exc_id = PyLong_FromVoidPtr(exc);
1031+
if (exc_id == NULL) {
1032+
return -1;
1033+
}
1034+
int res = PySet_Contains(matcher_value, exc_id);
1035+
Py_DECREF(exc_id);
1036+
return res;
10311037
}
10321038
return 0;
10331039
}
@@ -1212,32 +1218,35 @@ BaseExceptionGroup_subgroup(PyObject *self, PyObject *args)
12121218
}
12131219

12141220
static int
1215-
collect_exception_group_leaves(PyObject *exc, PyObject *leaves)
1221+
collect_exception_group_leaf_ids(PyObject *exc, PyObject *leaf_ids)
12161222
{
12171223
if (Py_IsNone(exc)) {
12181224
return 0;
12191225
}
12201226

12211227
assert(PyExceptionInstance_Check(exc));
1222-
assert(PySet_Check(leaves));
1228+
assert(PySet_Check(leaf_ids));
12231229

1224-
/* Add all leaf exceptions in exc to the leaves set */
1230+
/* Add IDs of all leaf exceptions in exc to the leaf_ids set */
12251231

12261232
if (!_PyBaseExceptionGroup_Check(exc)) {
1227-
if (PySet_Add(leaves, exc) < 0) {
1233+
PyObject *exc_id = PyLong_FromVoidPtr(exc);
1234+
if (exc_id == NULL) {
12281235
return -1;
12291236
}
1230-
return 0;
1237+
int res = PySet_Add(leaf_ids, exc_id);
1238+
Py_DECREF(exc_id);
1239+
return res;
12311240
}
12321241
PyBaseExceptionGroupObject *eg = _PyBaseExceptionGroupObject_cast(exc);
12331242
Py_ssize_t num_excs = PyTuple_GET_SIZE(eg->excs);
12341243
/* recursive calls */
12351244
for (Py_ssize_t i = 0; i < num_excs; i++) {
12361245
PyObject *e = PyTuple_GET_ITEM(eg->excs, i);
1237-
if (_Py_EnterRecursiveCall(" in collect_exception_group_leaves")) {
1246+
if (_Py_EnterRecursiveCall(" in collect_exception_group_leaf_ids")) {
12381247
return -1;
12391248
}
1240-
int res = collect_exception_group_leaves(e, leaves);
1249+
int res = collect_exception_group_leaf_ids(e, leaf_ids);
12411250
_Py_LeaveRecursiveCall();
12421251
if (res < 0) {
12431252
return -1;
@@ -1258,8 +1267,8 @@ exception_group_projection(PyObject *eg, PyObject *keep)
12581267
assert(_PyBaseExceptionGroup_Check(eg));
12591268
assert(PyList_CheckExact(keep));
12601269

1261-
PyObject *leaves = PySet_New(NULL);
1262-
if (!leaves) {
1270+
PyObject *leaf_ids = PySet_New(NULL);
1271+
if (!leaf_ids) {
12631272
return NULL;
12641273
}
12651274

@@ -1268,18 +1277,18 @@ exception_group_projection(PyObject *eg, PyObject *keep)
12681277
PyObject *e = PyList_GET_ITEM(keep, i);
12691278
assert(e != NULL);
12701279
assert(_PyBaseExceptionGroup_Check(e));
1271-
if (collect_exception_group_leaves(e, leaves) < 0) {
1272-
Py_DECREF(leaves);
1280+
if (collect_exception_group_leaf_ids(e, leaf_ids) < 0) {
1281+
Py_DECREF(leaf_ids);
12731282
return NULL;
12741283
}
12751284
}
12761285

12771286
_exceptiongroup_split_result split_result;
12781287
bool construct_rest = false;
12791288
int err = exceptiongroup_split_recursive(
1280-
eg, EXCEPTION_GROUP_MATCH_INSTANCES, leaves,
1289+
eg, EXCEPTION_GROUP_MATCH_INSTANCE_IDS, leaf_ids,
12811290
construct_rest, &split_result);
1282-
Py_DECREF(leaves);
1291+
Py_DECREF(leaf_ids);
12831292
if (err < 0) {
12841293
return NULL;
12851294
}

0 commit comments

Comments
 (0)