https://github.com/python/cpython/commit/426449d9834855fcf8c150889157af8c39526b81 commit: 426449d9834855fcf8c150889157af8c39526b81 branch: main author: Victor Stinner <vstin...@python.org> committer: cfbolz <cfb...@gmx.de> date: 2025-04-23T17:10:09+02:00 summary:
gh-132825: Enhance unhashable error messages for dict and set (#132828) files: A Misc/NEWS.d/next/Core_and_Builtins/2025-04-23-11-34-39.gh-issue-132825._yv0uL.rst M Lib/test/test_capi/test_abstract.py M Lib/test/test_dict.py M Lib/test/test_import/__init__.py M Lib/test/test_set.py M Objects/dictobject.c M Objects/setobject.c diff --git a/Lib/test/test_capi/test_abstract.py b/Lib/test/test_capi/test_abstract.py index 912c2de2b69930..7d548ae87c0fa6 100644 --- a/Lib/test/test_capi/test_abstract.py +++ b/Lib/test/test_capi/test_abstract.py @@ -460,7 +460,8 @@ def test_mapping_haskey(self): self.assertFalse(haskey({}, [])) self.assertEqual(cm.unraisable.exc_type, TypeError) self.assertEqual(str(cm.unraisable.exc_value), - "unhashable type: 'list'") + "cannot use 'list' as a dict key " + "(unhashable type: 'list')") with support.catch_unraisable_exception() as cm: self.assertFalse(haskey([], 1)) diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py index 7756c1f995cf2c..9485ef2889f760 100644 --- a/Lib/test/test_dict.py +++ b/Lib/test/test_dict.py @@ -3,6 +3,7 @@ import gc import pickle import random +import re import string import sys import unittest @@ -1487,6 +1488,47 @@ def make_pairs(): self.assertEqual(d.get(key3_3), 44) self.assertGreaterEqual(eq_count, 1) + def test_unhashable_key(self): + d = {'a': 1} + key = [1, 2, 3] + + def check_unhashable_key(): + msg = "cannot use 'list' as a dict key (unhashable type: 'list')" + return self.assertRaisesRegex(TypeError, re.escape(msg)) + + with check_unhashable_key(): + key in d + with check_unhashable_key(): + d[key] + with check_unhashable_key(): + d[key] = 2 + with check_unhashable_key(): + d.setdefault(key, 2) + with check_unhashable_key(): + d.pop(key) + with check_unhashable_key(): + d.get(key) + + # Only TypeError exception is overriden, + # other exceptions are left unchanged. + class HashError: + def __hash__(self): + raise KeyError('error') + + key2 = HashError() + with self.assertRaises(KeyError): + key2 in d + with self.assertRaises(KeyError): + d[key2] + with self.assertRaises(KeyError): + d[key2] = 2 + with self.assertRaises(KeyError): + d.setdefault(key2, 2) + with self.assertRaises(KeyError): + d.pop(key2) + with self.assertRaises(KeyError): + d.get(key2) + class CAPITest(unittest.TestCase): diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index a745760289b5b8..b5f4645847a1e6 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -1055,7 +1055,7 @@ class substr(str): """) popen = script_helper.spawn_python("main.py", cwd=tmp) stdout, stderr = popen.communicate() - self.assertEqual(stdout.rstrip(), b"unhashable type: 'substr'") + self.assertIn(b"unhashable type: 'substr'", stdout.rstrip()) with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f: f.write(""" @@ -1072,7 +1072,7 @@ class substr(str): popen = script_helper.spawn_python("main.py", cwd=tmp) stdout, stderr = popen.communicate() - self.assertEqual(stdout.rstrip(), b"unhashable type: 'substr'") + self.assertIn(b"unhashable type: 'substr'", stdout.rstrip()) # Various issues with sys module with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f: diff --git a/Lib/test/test_set.py b/Lib/test/test_set.py index a8531d466e56e7..c01e323553d768 100644 --- a/Lib/test/test_set.py +++ b/Lib/test/test_set.py @@ -1,16 +1,17 @@ -import unittest -from test import support -from test.support import warnings_helper +import collections.abc +import copy import gc -import weakref +import itertools import operator -import copy import pickle -from random import randrange, shuffle +import re +import unittest import warnings -import collections -import collections.abc -import itertools +import weakref +from random import randrange, shuffle +from test import support +from test.support import warnings_helper + class PassThru(Exception): pass @@ -645,6 +646,35 @@ def test_set_membership(self): self.assertRaises(KeyError, myset.remove, set(range(1))) self.assertRaises(KeyError, myset.remove, set(range(3))) + def test_unhashable_element(self): + myset = {'a'} + elem = [1, 2, 3] + + def check_unhashable_element(): + msg = "cannot use 'list' as a set element (unhashable type: 'list')" + return self.assertRaisesRegex(TypeError, re.escape(msg)) + + with check_unhashable_element(): + elem in myset + with check_unhashable_element(): + myset.add(elem) + with check_unhashable_element(): + myset.discard(elem) + + # Only TypeError exception is overriden, + # other exceptions are left unchanged. + class HashError: + def __hash__(self): + raise KeyError('error') + + elem2 = HashError() + with self.assertRaises(KeyError): + elem2 in myset + with self.assertRaises(KeyError): + myset.add(elem2) + with self.assertRaises(KeyError): + myset.discard(elem2) + class SetSubclass(set): pass diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-04-23-11-34-39.gh-issue-132825._yv0uL.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-04-23-11-34-39.gh-issue-132825._yv0uL.rst new file mode 100644 index 00000000000000..d751837c44aac6 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-04-23-11-34-39.gh-issue-132825._yv0uL.rst @@ -0,0 +1,2 @@ +Enhance unhashable key/element error messages for :class:`dict` and +:class:`set`. Patch by Victor Stinner. diff --git a/Objects/dictobject.c b/Objects/dictobject.c index ff6dbb8bed3007..c34d17b2259be3 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -2276,6 +2276,22 @@ PyDict_GetItem(PyObject *op, PyObject *key) "PyDict_GetItemRef() or PyDict_GetItemWithError()"); } +static void +dict_unhashtable_type(PyObject *key) +{ + PyObject *exc = PyErr_GetRaisedException(); + assert(exc != NULL); + if (!Py_IS_TYPE(exc, (PyTypeObject*)PyExc_TypeError)) { + PyErr_SetRaisedException(exc); + return; + } + + PyErr_Format(PyExc_TypeError, + "cannot use '%T' as a dict key (%S)", + key, exc); + Py_DECREF(exc); +} + Py_ssize_t _PyDict_LookupIndex(PyDictObject *mp, PyObject *key) { @@ -2286,6 +2302,7 @@ _PyDict_LookupIndex(PyDictObject *mp, PyObject *key) Py_hash_t hash = _PyObject_HashFast(key); if (hash == -1) { + dict_unhashtable_type(key); return -1; } @@ -2382,6 +2399,7 @@ PyDict_GetItemRef(PyObject *op, PyObject *key, PyObject **result) Py_hash_t hash = _PyObject_HashFast(key); if (hash == -1) { + dict_unhashtable_type(key); *result = NULL; return -1; } @@ -2397,6 +2415,7 @@ _PyDict_GetItemRef_Unicode_LockHeld(PyDictObject *op, PyObject *key, PyObject ** Py_hash_t hash = _PyObject_HashFast(key); if (hash == -1) { + dict_unhashtable_type(key); *result = NULL; return -1; } @@ -2434,6 +2453,7 @@ PyDict_GetItemWithError(PyObject *op, PyObject *key) } hash = _PyObject_HashFast(key); if (hash == -1) { + dict_unhashtable_type(key); return NULL; } @@ -2591,6 +2611,7 @@ setitem_take2_lock_held(PyDictObject *mp, PyObject *key, PyObject *value) assert(PyDict_Check(mp)); Py_hash_t hash = _PyObject_HashFast(key); if (hash == -1) { + dict_unhashtable_type(key); Py_DECREF(key); Py_DECREF(value); return -1; @@ -2742,6 +2763,7 @@ PyDict_DelItem(PyObject *op, PyObject *key) assert(key); Py_hash_t hash = _PyObject_HashFast(key); if (hash == -1) { + dict_unhashtable_type(key); return -1; } @@ -3064,6 +3086,7 @@ pop_lock_held(PyObject *op, PyObject *key, PyObject **result) Py_hash_t hash = _PyObject_HashFast(key); if (hash == -1) { + dict_unhashtable_type(key); if (result) { *result = NULL; } @@ -3398,6 +3421,7 @@ dict_subscript(PyObject *self, PyObject *key) hash = _PyObject_HashFast(key); if (hash == -1) { + dict_unhashtable_type(key); return NULL; } ix = _Py_dict_lookup_threadsafe(mp, key, hash, &value); @@ -4278,6 +4302,7 @@ dict_get_impl(PyDictObject *self, PyObject *key, PyObject *default_value) hash = _PyObject_HashFast(key); if (hash == -1) { + dict_unhashtable_type(key); return NULL; } ix = _Py_dict_lookup_threadsafe(self, key, hash, &val); @@ -4310,6 +4335,7 @@ dict_setdefault_ref_lock_held(PyObject *d, PyObject *key, PyObject *default_valu hash = _PyObject_HashFast(key); if (hash == -1) { + dict_unhashtable_type(key); if (result) { *result = NULL; } @@ -4737,8 +4763,8 @@ int PyDict_Contains(PyObject *op, PyObject *key) { Py_hash_t hash = _PyObject_HashFast(key); - if (hash == -1) { + dict_unhashtable_type(key); return -1; } @@ -6829,6 +6855,7 @@ _PyDict_SetItem_LockHeld(PyDictObject *dict, PyObject *name, PyObject *value) if (value == NULL) { Py_hash_t hash = _PyObject_HashFast(name); if (hash == -1) { + dict_unhashtable_type(name); return -1; } return delitem_knownhash_lock_held((PyObject *)dict, name, hash); diff --git a/Objects/setobject.c b/Objects/setobject.c index 347888389b8dcd..73cebbe7e1ecdf 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -211,11 +211,28 @@ set_add_entry(PySetObject *so, PyObject *key, Py_hash_t hash) return set_add_entry_takeref(so, Py_NewRef(key), hash); } +static void +set_unhashtable_type(PyObject *key) +{ + PyObject *exc = PyErr_GetRaisedException(); + assert(exc != NULL); + if (!Py_IS_TYPE(exc, (PyTypeObject*)PyExc_TypeError)) { + PyErr_SetRaisedException(exc); + return; + } + + PyErr_Format(PyExc_TypeError, + "cannot use '%T' as a set element (%S)", + key, exc); + Py_DECREF(exc); +} + int _PySet_AddTakeRef(PySetObject *so, PyObject *key) { Py_hash_t hash = _PyObject_HashFast(key); if (hash == -1) { + set_unhashtable_type(key); Py_DECREF(key); return -1; } @@ -384,6 +401,7 @@ set_add_key(PySetObject *so, PyObject *key) { Py_hash_t hash = _PyObject_HashFast(key); if (hash == -1) { + set_unhashtable_type(key); return -1; } return set_add_entry(so, key, hash); @@ -394,6 +412,7 @@ set_contains_key(PySetObject *so, PyObject *key) { Py_hash_t hash = _PyObject_HashFast(key); if (hash == -1) { + set_unhashtable_type(key); return -1; } return set_contains_entry(so, key, hash); @@ -404,6 +423,7 @@ set_discard_key(PySetObject *so, PyObject *key) { Py_hash_t hash = _PyObject_HashFast(key); if (hash == -1) { + set_unhashtable_type(key); return -1; } return set_discard_entry(so, key, hash); _______________________________________________ Python-checkins mailing list -- python-checkins@python.org To unsubscribe send an email to python-checkins-le...@python.org https://mail.python.org/mailman3/lists/python-checkins.python.org/ Member address: arch...@mail-archive.com