From ec8e68061161f9c615a6b7daafc2f24836d20f15 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 7 Nov 2022 09:29:14 +0000 Subject: [PATCH 1/6] gh-99181: fix except* on unhashable exceptions --- Lib/test/test_except_star.py | 187 +++++++++++++++++++++++++++++++++++ Objects/exceptions.c | 43 ++++---- 2 files changed, 213 insertions(+), 17 deletions(-) diff --git a/Lib/test/test_except_star.py b/Lib/test/test_except_star.py index dbe8eff32924ed..c851478d856966 100644 --- a/Lib/test/test_except_star.py +++ b/Lib/test/test_except_star.py @@ -1000,5 +1000,192 @@ def test_exc_info_restored(self): self.assertEqual(sys.exc_info(), (None, None, None)) +class TestExceptStar_WeirdLeafExceptions(ExceptStarTest): + # Test that except* works when leaf exceptions are + # unhashable or have a bad custom __eq__ + + class UnhashableExc(ValueError): + hash = None + + class AlwaysEqualExc(ValueError): + def __eq__(self, other): + return True + + class NeverEqualExc(ValueError): + def __eq__(self, other): + return False + + def setUp(self): + self.bad_types = [self.UnhashableExc, + self.AlwaysEqualExc, + self.NeverEqualExc] + + def except_type(self, eg, type): + match, rest = None, None + try: + try: + raise eg + except* type as e: + match = e + except Exception as e: + rest = e + return match, rest + + def test_catch_unhashable_leaf_exception(self): + for Bad in self.bad_types: + with self.subTest(Bad): + eg = ExceptionGroup("eg", [TypeError(1), Bad(2)]) + match, rest = self.except_type(eg, Bad) + self.assertExceptionIsLike( + match, ExceptionGroup("eg", [Bad(2)])) + self.assertExceptionIsLike( + rest, ExceptionGroup("eg", [TypeError(1)])) + + def test_propagate_unhashable_leaf(self): + for Bad in self.bad_types: + with self.subTest(Bad): + eg = ExceptionGroup("eg", [TypeError(1), Bad(2)]) + match, rest = self.except_type(eg, TypeError) + self.assertExceptionIsLike( + match, ExceptionGroup("eg", [TypeError(1)])) + self.assertExceptionIsLike( + rest, ExceptionGroup("eg", [Bad(2)])) + + def test_catch_nothing_unhashable_leaf(self): + for Bad in self.bad_types: + with self.subTest(Bad): + eg = ExceptionGroup("eg", [TypeError(1), Bad(2)]) + match, rest = self.except_type(eg, OSError) + self.assertIsNone(match) + self.assertExceptionIsLike(rest, eg) + + def test_catch_everything_unhashable_leaf(self): + for Bad in self.bad_types: + with self.subTest(Bad): + eg = ExceptionGroup("eg", [TypeError(1), Bad(2)]) + match, rest = self.except_type(eg, Exception) + self.assertExceptionIsLike(match, eg) + self.assertIsNone(rest) + + def test_reraise_unhashable_leaf(self): + for Bad in self.bad_types: + with self.subTest(Bad): + eg = ExceptionGroup( + "eg", [TypeError(1), Bad(2), ValueError(3)]) + + try: + try: + raise eg + except* TypeError: + pass + except* Bad: + raise + except Exception as e: + exc = e + + self.assertExceptionIsLike( + exc, ExceptionGroup("eg", [Bad(2), ValueError(3)])) + + +class TestExceptStar_WeirdExceptionGroupSubclass(ExceptStarTest): + # Test that except* works with exception groups that are + # unhashable or have a bad custom __eq__ + + class UnhashableEG(ExceptionGroup): + hash = None + + def derive(self, excs): + return type(self)(self.message, excs) + + class AlwaysEqualEG(ExceptionGroup): + def __eq__(self, other): + return True + + def derive(self, excs): + return type(self)(self.message, excs) + + class NeverEqualEG(ExceptionGroup): + def __eq__(self, other): + return False + + def derive(self, excs): + return type(self)(self.message, excs) + + + def setUp(self): + self.bad_types = [self.UnhashableEG, + self.AlwaysEqualEG, + self.NeverEqualEG] + + def except_type(self, eg, type): + match, rest = None, None + try: + try: + raise eg + except* type as e: + match = e + except Exception as e: + rest = e + return match, rest + + def test_catch_some_unhashable_exception_group_subclass(self): + for BadEG in self.bad_types: + with self.subTest(BadEG): + eg = BadEG("eg", + [TypeError(1), + BadEG("nested", [ValueError(2)])]) + + match, rest = self.except_type(eg, TypeError) + self.assertExceptionIsLike(match, BadEG("eg", [TypeError(1)])) + self.assertExceptionIsLike(rest, + BadEG("eg", [BadEG("nested", [ValueError(2)])])) + + def test_catch_none_unhashable_exception_group_subclass(self): + for BadEG in self.bad_types: + with self.subTest(BadEG): + + eg = BadEG("eg", + [TypeError(1), + BadEG("nested", [ValueError(2)])]) + + match, rest = self.except_type(eg, OSError) + self.assertIsNone(match) + self.assertExceptionIsLike(rest, eg) + + def test_catch_all_unhashable_exception_group_subclass(self): + for BadEG in self.bad_types: + with self.subTest(BadEG): + + eg = BadEG("eg", + [TypeError(1), + BadEG("nested", [ValueError(2)])]) + + match, rest = self.except_type(eg, Exception) + self.assertExceptionIsLike(match, eg) + self.assertIsNone(rest) + + def test_reraise_unhashable_eg(self): + for BadEG in self.bad_types: + with self.subTest(BadEG): + + eg = BadEG("eg", + [TypeError(1), ValueError(2), + BadEG("nested", [ValueError(3), OSError(4)])]) + + try: + try: + raise eg + except* ValueError: + pass + except* OSError: + raise + except Exception as e: + exc = e + + self.assertExceptionIsLike( + exc, BadEG("eg", [TypeError(1), + BadEG("nested", [OSError(4)])])) + + if __name__ == '__main__': unittest.main() diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 4b4f31a209b669..f0ff6fbd6b4770 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -962,11 +962,11 @@ typedef enum { EXCEPTION_GROUP_MATCH_BY_TYPE = 0, /* A PyFunction returning True for matching exceptions */ EXCEPTION_GROUP_MATCH_BY_PREDICATE = 1, - /* A set of leaf exceptions to include in the result. + /* A set of the IDs of leaf exception to include in the result. * This matcher type is used internally by the interpreter * to construct reraised exceptions. */ - EXCEPTION_GROUP_MATCH_INSTANCES = 2 + EXCEPTION_GROUP_MATCH_INSTANCE_IDS = 2 } _exceptiongroup_split_matcher_type; static int @@ -1024,10 +1024,16 @@ exceptiongroup_split_check_match(PyObject *exc, Py_DECREF(exc_matches); return is_true; } - case EXCEPTION_GROUP_MATCH_INSTANCES: { + case EXCEPTION_GROUP_MATCH_INSTANCE_IDS: { assert(PySet_Check(matcher_value)); if (!_PyBaseExceptionGroup_Check(exc)) { - return PySet_Contains(matcher_value, exc); + PyObject *exc_key = PyLong_FromVoidPtr(exc); + if (exc_key == NULL) { + return -1; + } + int res = PySet_Contains(matcher_value, exc_key); + Py_DECREF(exc_key); + return res; } return 0; } @@ -1212,32 +1218,35 @@ BaseExceptionGroup_subgroup(PyObject *self, PyObject *args) } static int -collect_exception_group_leaves(PyObject *exc, PyObject *leaves) +collect_exception_group_leaf_ids(PyObject *exc, PyObject *leaf_ids) { if (Py_IsNone(exc)) { return 0; } assert(PyExceptionInstance_Check(exc)); - assert(PySet_Check(leaves)); + assert(PySet_Check(leaf_ids)); - /* Add all leaf exceptions in exc to the leaves set */ + /* Add IDs of all leaf exceptions in exc to the leaf_ids set */ if (!_PyBaseExceptionGroup_Check(exc)) { - if (PySet_Add(leaves, exc) < 0) { + PyObject *exc_key = PyLong_FromVoidPtr(exc); + if (exc_key == NULL) { return -1; } - return 0; + int res = PySet_Add(leaf_ids, exc_key); + Py_DECREF(exc_key); + return res; } PyBaseExceptionGroupObject *eg = _PyBaseExceptionGroupObject_cast(exc); Py_ssize_t num_excs = PyTuple_GET_SIZE(eg->excs); /* recursive calls */ for (Py_ssize_t i = 0; i < num_excs; i++) { PyObject *e = PyTuple_GET_ITEM(eg->excs, i); - if (_Py_EnterRecursiveCall(" in collect_exception_group_leaves")) { + if (_Py_EnterRecursiveCall(" in collect_exception_group_leaf_ids")) { return -1; } - int res = collect_exception_group_leaves(e, leaves); + int res = collect_exception_group_leaf_ids(e, leaf_ids); _Py_LeaveRecursiveCall(); if (res < 0) { return -1; @@ -1258,8 +1267,8 @@ exception_group_projection(PyObject *eg, PyObject *keep) assert(_PyBaseExceptionGroup_Check(eg)); assert(PyList_CheckExact(keep)); - PyObject *leaves = PySet_New(NULL); - if (!leaves) { + PyObject *leaf_ids = PySet_New(NULL); + if (!leaf_ids) { return NULL; } @@ -1268,8 +1277,8 @@ exception_group_projection(PyObject *eg, PyObject *keep) PyObject *e = PyList_GET_ITEM(keep, i); assert(e != NULL); assert(_PyBaseExceptionGroup_Check(e)); - if (collect_exception_group_leaves(e, leaves) < 0) { - Py_DECREF(leaves); + if (collect_exception_group_leaf_ids(e, leaf_ids) < 0) { + Py_DECREF(leaf_ids); return NULL; } } @@ -1277,9 +1286,9 @@ exception_group_projection(PyObject *eg, PyObject *keep) _exceptiongroup_split_result split_result; bool construct_rest = false; int err = exceptiongroup_split_recursive( - eg, EXCEPTION_GROUP_MATCH_INSTANCES, leaves, + eg, EXCEPTION_GROUP_MATCH_INSTANCE_IDS, leaf_ids, construct_rest, &split_result); - Py_DECREF(leaves); + Py_DECREF(leaf_ids); if (err < 0) { return NULL; } From 64406d65089566fa2768223d1be93fea4abfc869 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 7 Nov 2022 10:06:10 +0000 Subject: [PATCH 2/6] fix typo in comment --- Objects/exceptions.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index f0ff6fbd6b4770..cdb6b1d28a6aa9 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -962,7 +962,7 @@ typedef enum { EXCEPTION_GROUP_MATCH_BY_TYPE = 0, /* A PyFunction returning True for matching exceptions */ EXCEPTION_GROUP_MATCH_BY_PREDICATE = 1, - /* A set of the IDs of leaf exception to include in the result. + /* A set of the IDs of leaf exceptions to include in the result. * This matcher type is used internally by the interpreter * to construct reraised exceptions. */ From e725f63c5377321e2063b5e5f96bb1b68e85b5ee Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 7 Nov 2022 10:19:39 +0000 Subject: [PATCH 3/6] rename exc_key --> exc_id --- Objects/exceptions.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index cdb6b1d28a6aa9..fd63095d0396b3 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -1027,12 +1027,12 @@ exceptiongroup_split_check_match(PyObject *exc, case EXCEPTION_GROUP_MATCH_INSTANCE_IDS: { assert(PySet_Check(matcher_value)); if (!_PyBaseExceptionGroup_Check(exc)) { - PyObject *exc_key = PyLong_FromVoidPtr(exc); - if (exc_key == NULL) { + PyObject *exc_id = PyLong_FromVoidPtr(exc); + if (exc_id == NULL) { return -1; } - int res = PySet_Contains(matcher_value, exc_key); - Py_DECREF(exc_key); + int res = PySet_Contains(matcher_value, exc_id); + Py_DECREF(exc_id); return res; } return 0; @@ -1230,12 +1230,12 @@ collect_exception_group_leaf_ids(PyObject *exc, PyObject *leaf_ids) /* Add IDs of all leaf exceptions in exc to the leaf_ids set */ if (!_PyBaseExceptionGroup_Check(exc)) { - PyObject *exc_key = PyLong_FromVoidPtr(exc); - if (exc_key == NULL) { + PyObject *exc_id = PyLong_FromVoidPtr(exc); + if (exc_id == NULL) { return -1; } - int res = PySet_Add(leaf_ids, exc_key); - Py_DECREF(exc_key); + int res = PySet_Add(leaf_ids, exc_id); + Py_DECREF(exc_id); return res; } PyBaseExceptionGroupObject *eg = _PyBaseExceptionGroupObject_cast(exc); From 9f8a78dea5ba23cf1e0178e9981c86c0aa7bf54e Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Mon, 7 Nov 2022 10:29:43 +0000 Subject: [PATCH 4/6] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2022-11-07-10-29-41.gh-issue-99181.bfG4bI.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2022-11-07-10-29-41.gh-issue-99181.bfG4bI.rst diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-11-07-10-29-41.gh-issue-99181.bfG4bI.rst b/Misc/NEWS.d/next/Core and Builtins/2022-11-07-10-29-41.gh-issue-99181.bfG4bI.rst new file mode 100644 index 00000000000000..aa6160dd5a5efd --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2022-11-07-10-29-41.gh-issue-99181.bfG4bI.rst @@ -0,0 +1 @@ +Fix failure in :keyword:`except* ` with unhashable exceptions. From 094bb0c91e45a2ce746f879f61da58f9e316df88 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 7 Nov 2022 10:52:46 +0000 Subject: [PATCH 5/6] hash-->__hash__ --- Lib/test/test_except_star.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_except_star.py b/Lib/test/test_except_star.py index c851478d856966..00589b1f777aa5 100644 --- a/Lib/test/test_except_star.py +++ b/Lib/test/test_except_star.py @@ -1005,7 +1005,7 @@ class TestExceptStar_WeirdLeafExceptions(ExceptStarTest): # unhashable or have a bad custom __eq__ class UnhashableExc(ValueError): - hash = None + __hash__ = None class AlwaysEqualExc(ValueError): def __eq__(self, other): @@ -1092,7 +1092,7 @@ class TestExceptStar_WeirdExceptionGroupSubclass(ExceptStarTest): # unhashable or have a bad custom __eq__ class UnhashableEG(ExceptionGroup): - hash = None + __hash__ = None def derive(self, excs): return type(self)(self.message, excs) From 9d4bea83852bd08987da65da779463904ab19f47 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 7 Nov 2022 18:12:54 +0000 Subject: [PATCH 6/6] add test with broken __eq__ --- Lib/test/test_except_star.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_except_star.py b/Lib/test/test_except_star.py index 00589b1f777aa5..9de72dbd5a3264 100644 --- a/Lib/test/test_except_star.py +++ b/Lib/test/test_except_star.py @@ -1015,10 +1015,15 @@ class NeverEqualExc(ValueError): def __eq__(self, other): return False + class BrokenEqualExc(ValueError): + def __eq__(self, other): + raise RuntimeError() + def setUp(self): self.bad_types = [self.UnhashableExc, self.AlwaysEqualExc, - self.NeverEqualExc] + self.NeverEqualExc, + self.BrokenEqualExc] def except_type(self, eg, type): match, rest = None, None @@ -1111,11 +1116,18 @@ def __eq__(self, other): def derive(self, excs): return type(self)(self.message, excs) + class BrokenEqualEG(ExceptionGroup): + def __eq__(self, other): + raise RuntimeError() + + def derive(self, excs): + return type(self)(self.message, excs) def setUp(self): self.bad_types = [self.UnhashableEG, self.AlwaysEqualEG, - self.NeverEqualEG] + self.NeverEqualEG, + self.BrokenEqualEG] def except_type(self, eg, type): match, rest = None, None