Skip to content

Commit ec8e680

Browse files
committed
gh-99181: fix except* on unhashable exceptions
1 parent cfec5b1 commit ec8e680

File tree

2 files changed

+213
-17
lines changed

2 files changed

+213
-17
lines changed

Lib/test/test_except_star.py

+187
Original file line numberDiff line numberDiff line change
@@ -1000,5 +1000,192 @@ 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+
def setUp(self):
1019+
self.bad_types = [self.UnhashableExc,
1020+
self.AlwaysEqualExc,
1021+
self.NeverEqualExc]
1022+
1023+
def except_type(self, eg, type):
1024+
match, rest = None, None
1025+
try:
1026+
try:
1027+
raise eg
1028+
except* type as e:
1029+
match = e
1030+
except Exception as e:
1031+
rest = e
1032+
return match, rest
1033+
1034+
def test_catch_unhashable_leaf_exception(self):
1035+
for Bad in self.bad_types:
1036+
with self.subTest(Bad):
1037+
eg = ExceptionGroup("eg", [TypeError(1), Bad(2)])
1038+
match, rest = self.except_type(eg, Bad)
1039+
self.assertExceptionIsLike(
1040+
match, ExceptionGroup("eg", [Bad(2)]))
1041+
self.assertExceptionIsLike(
1042+
rest, ExceptionGroup("eg", [TypeError(1)]))
1043+
1044+
def test_propagate_unhashable_leaf(self):
1045+
for Bad in self.bad_types:
1046+
with self.subTest(Bad):
1047+
eg = ExceptionGroup("eg", [TypeError(1), Bad(2)])
1048+
match, rest = self.except_type(eg, TypeError)
1049+
self.assertExceptionIsLike(
1050+
match, ExceptionGroup("eg", [TypeError(1)]))
1051+
self.assertExceptionIsLike(
1052+
rest, ExceptionGroup("eg", [Bad(2)]))
1053+
1054+
def test_catch_nothing_unhashable_leaf(self):
1055+
for Bad in self.bad_types:
1056+
with self.subTest(Bad):
1057+
eg = ExceptionGroup("eg", [TypeError(1), Bad(2)])
1058+
match, rest = self.except_type(eg, OSError)
1059+
self.assertIsNone(match)
1060+
self.assertExceptionIsLike(rest, eg)
1061+
1062+
def test_catch_everything_unhashable_leaf(self):
1063+
for Bad in self.bad_types:
1064+
with self.subTest(Bad):
1065+
eg = ExceptionGroup("eg", [TypeError(1), Bad(2)])
1066+
match, rest = self.except_type(eg, Exception)
1067+
self.assertExceptionIsLike(match, eg)
1068+
self.assertIsNone(rest)
1069+
1070+
def test_reraise_unhashable_leaf(self):
1071+
for Bad in self.bad_types:
1072+
with self.subTest(Bad):
1073+
eg = ExceptionGroup(
1074+
"eg", [TypeError(1), Bad(2), ValueError(3)])
1075+
1076+
try:
1077+
try:
1078+
raise eg
1079+
except* TypeError:
1080+
pass
1081+
except* Bad:
1082+
raise
1083+
except Exception as e:
1084+
exc = e
1085+
1086+
self.assertExceptionIsLike(
1087+
exc, ExceptionGroup("eg", [Bad(2), ValueError(3)]))
1088+
1089+
1090+
class TestExceptStar_WeirdExceptionGroupSubclass(ExceptStarTest):
1091+
# Test that except* works with exception groups that are
1092+
# unhashable or have a bad custom __eq__
1093+
1094+
class UnhashableEG(ExceptionGroup):
1095+
hash = None
1096+
1097+
def derive(self, excs):
1098+
return type(self)(self.message, excs)
1099+
1100+
class AlwaysEqualEG(ExceptionGroup):
1101+
def __eq__(self, other):
1102+
return True
1103+
1104+
def derive(self, excs):
1105+
return type(self)(self.message, excs)
1106+
1107+
class NeverEqualEG(ExceptionGroup):
1108+
def __eq__(self, other):
1109+
return False
1110+
1111+
def derive(self, excs):
1112+
return type(self)(self.message, excs)
1113+
1114+
1115+
def setUp(self):
1116+
self.bad_types = [self.UnhashableEG,
1117+
self.AlwaysEqualEG,
1118+
self.NeverEqualEG]
1119+
1120+
def except_type(self, eg, type):
1121+
match, rest = None, None
1122+
try:
1123+
try:
1124+
raise eg
1125+
except* type as e:
1126+
match = e
1127+
except Exception as e:
1128+
rest = e
1129+
return match, rest
1130+
1131+
def test_catch_some_unhashable_exception_group_subclass(self):
1132+
for BadEG in self.bad_types:
1133+
with self.subTest(BadEG):
1134+
eg = BadEG("eg",
1135+
[TypeError(1),
1136+
BadEG("nested", [ValueError(2)])])
1137+
1138+
match, rest = self.except_type(eg, TypeError)
1139+
self.assertExceptionIsLike(match, BadEG("eg", [TypeError(1)]))
1140+
self.assertExceptionIsLike(rest,
1141+
BadEG("eg", [BadEG("nested", [ValueError(2)])]))
1142+
1143+
def test_catch_none_unhashable_exception_group_subclass(self):
1144+
for BadEG in self.bad_types:
1145+
with self.subTest(BadEG):
1146+
1147+
eg = BadEG("eg",
1148+
[TypeError(1),
1149+
BadEG("nested", [ValueError(2)])])
1150+
1151+
match, rest = self.except_type(eg, OSError)
1152+
self.assertIsNone(match)
1153+
self.assertExceptionIsLike(rest, eg)
1154+
1155+
def test_catch_all_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, Exception)
1164+
self.assertExceptionIsLike(match, eg)
1165+
self.assertIsNone(rest)
1166+
1167+
def test_reraise_unhashable_eg(self):
1168+
for BadEG in self.bad_types:
1169+
with self.subTest(BadEG):
1170+
1171+
eg = BadEG("eg",
1172+
[TypeError(1), ValueError(2),
1173+
BadEG("nested", [ValueError(3), OSError(4)])])
1174+
1175+
try:
1176+
try:
1177+
raise eg
1178+
except* ValueError:
1179+
pass
1180+
except* OSError:
1181+
raise
1182+
except Exception as e:
1183+
exc = e
1184+
1185+
self.assertExceptionIsLike(
1186+
exc, BadEG("eg", [TypeError(1),
1187+
BadEG("nested", [OSError(4)])]))
1188+
1189+
10031190
if __name__ == '__main__':
10041191
unittest.main()

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 exception 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_key = PyLong_FromVoidPtr(exc);
1031+
if (exc_key == NULL) {
1032+
return -1;
1033+
}
1034+
int res = PySet_Contains(matcher_value, exc_key);
1035+
Py_DECREF(exc_key);
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_key = PyLong_FromVoidPtr(exc);
1234+
if (exc_key == NULL) {
12281235
return -1;
12291236
}
1230-
return 0;
1237+
int res = PySet_Add(leaf_ids, exc_key);
1238+
Py_DECREF(exc_key);
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)