gh-132825: Enhance unhashable error messages for dict and set (#132828)

This commit is contained in:
Victor Stinner 2025-04-23 17:10:09 +02:00 committed by GitHub
parent b2e666f30a
commit 426449d983
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 136 additions and 14 deletions

View file

@ -460,7 +460,8 @@ class CAPITest(unittest.TestCase):
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))

View file

@ -3,6 +3,7 @@ import collections.abc
import gc
import pickle
import random
import re
import string
import sys
import unittest
@ -1487,6 +1488,47 @@ class DictTest(unittest.TestCase):
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):

View file

@ -1055,7 +1055,7 @@ except TypeError as e:
""")
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 @@ except TypeError as e:
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:

View file

@ -1,16 +1,17 @@
import collections.abc
import copy
import gc
import itertools
import operator
import pickle
import re
import unittest
import warnings
import weakref
from random import randrange, shuffle
from test import support
from test.support import warnings_helper
import gc
import weakref
import operator
import copy
import pickle
from random import randrange, shuffle
import warnings
import collections
import collections.abc
import itertools
class PassThru(Exception):
pass
@ -645,6 +646,35 @@ class TestSet(TestJointOps, unittest.TestCase):
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

View file

@ -0,0 +1,2 @@
Enhance unhashable key/element error messages for :class:`dict` and
:class:`set`. Patch by Victor Stinner.

View file

@ -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);

View file

@ -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);