Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 426449d

Browse files
authored
gh-132825: Enhance unhashable error messages for dict and set (#132828)
1 parent b2e666f commit 426449d

File tree

7 files changed

+135
-13
lines changed

7 files changed

+135
-13
lines changed

Lib/test/test_capi/test_abstract.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,8 @@ def test_mapping_haskey(self):
460460
self.assertFalse(haskey({}, []))
461461
self.assertEqual(cm.unraisable.exc_type, TypeError)
462462
self.assertEqual(str(cm.unraisable.exc_value),
463-
"unhashable type: 'list'")
463+
"cannot use 'list' as a dict key "
464+
"(unhashable type: 'list')")
464465

465466
with support.catch_unraisable_exception() as cm:
466467
self.assertFalse(haskey([], 1))

Lib/test/test_dict.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import gc
44
import pickle
55
import random
6+
import re
67
import string
78
import sys
89
import unittest
@@ -1487,6 +1488,47 @@ def make_pairs():
14871488
self.assertEqual(d.get(key3_3), 44)
14881489
self.assertGreaterEqual(eq_count, 1)
14891490

1491+
def test_unhashable_key(self):
1492+
d = {'a': 1}
1493+
key = [1, 2, 3]
1494+
1495+
def check_unhashable_key():
1496+
msg = "cannot use 'list' as a dict key (unhashable type: 'list')"
1497+
return self.assertRaisesRegex(TypeError, re.escape(msg))
1498+
1499+
with check_unhashable_key():
1500+
key in d
1501+
with check_unhashable_key():
1502+
d[key]
1503+
with check_unhashable_key():
1504+
d[key] = 2
1505+
with check_unhashable_key():
1506+
d.setdefault(key, 2)
1507+
with check_unhashable_key():
1508+
d.pop(key)
1509+
with check_unhashable_key():
1510+
d.get(key)
1511+
1512+
# Only TypeError exception is overriden,
1513+
# other exceptions are left unchanged.
1514+
class HashError:
1515+
def __hash__(self):
1516+
raise KeyError('error')
1517+
1518+
key2 = HashError()
1519+
with self.assertRaises(KeyError):
1520+
key2 in d
1521+
with self.assertRaises(KeyError):
1522+
d[key2]
1523+
with self.assertRaises(KeyError):
1524+
d[key2] = 2
1525+
with self.assertRaises(KeyError):
1526+
d.setdefault(key2, 2)
1527+
with self.assertRaises(KeyError):
1528+
d.pop(key2)
1529+
with self.assertRaises(KeyError):
1530+
d.get(key2)
1531+
14901532

14911533
class CAPITest(unittest.TestCase):
14921534

Lib/test/test_import/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,7 +1055,7 @@ class substr(str):
10551055
""")
10561056
popen = script_helper.spawn_python("main.py", cwd=tmp)
10571057
stdout, stderr = popen.communicate()
1058-
self.assertEqual(stdout.rstrip(), b"unhashable type: 'substr'")
1058+
self.assertIn(b"unhashable type: 'substr'", stdout.rstrip())
10591059

10601060
with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f:
10611061
f.write("""
@@ -1072,7 +1072,7 @@ class substr(str):
10721072

10731073
popen = script_helper.spawn_python("main.py", cwd=tmp)
10741074
stdout, stderr = popen.communicate()
1075-
self.assertEqual(stdout.rstrip(), b"unhashable type: 'substr'")
1075+
self.assertIn(b"unhashable type: 'substr'", stdout.rstrip())
10761076

10771077
# Various issues with sys module
10781078
with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f:

Lib/test/test_set.py

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
import unittest
2-
from test import support
3-
from test.support import warnings_helper
1+
import collections.abc
2+
import copy
43
import gc
5-
import weakref
4+
import itertools
65
import operator
7-
import copy
86
import pickle
9-
from random import randrange, shuffle
7+
import re
8+
import unittest
109
import warnings
11-
import collections
12-
import collections.abc
13-
import itertools
10+
import weakref
11+
from random import randrange, shuffle
12+
from test import support
13+
from test.support import warnings_helper
14+
1415

1516
class PassThru(Exception):
1617
pass
@@ -645,6 +646,35 @@ def test_set_membership(self):
645646
self.assertRaises(KeyError, myset.remove, set(range(1)))
646647
self.assertRaises(KeyError, myset.remove, set(range(3)))
647648

649+
def test_unhashable_element(self):
650+
myset = {'a'}
651+
elem = [1, 2, 3]
652+
653+
def check_unhashable_element():
654+
msg = "cannot use 'list' as a set element (unhashable type: 'list')"
655+
return self.assertRaisesRegex(TypeError, re.escape(msg))
656+
657+
with check_unhashable_element():
658+
elem in myset
659+
with check_unhashable_element():
660+
myset.add(elem)
661+
with check_unhashable_element():
662+
myset.discard(elem)
663+
664+
# Only TypeError exception is overriden,
665+
# other exceptions are left unchanged.
666+
class HashError:
667+
def __hash__(self):
668+
raise KeyError('error')
669+
670+
elem2 = HashError()
671+
with self.assertRaises(KeyError):
672+
elem2 in myset
673+
with self.assertRaises(KeyError):
674+
myset.add(elem2)
675+
with self.assertRaises(KeyError):
676+
myset.discard(elem2)
677+
648678

649679
class SetSubclass(set):
650680
pass
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Enhance unhashable key/element error messages for :class:`dict` and
2+
:class:`set`. Patch by Victor Stinner.

Objects/dictobject.c

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2276,6 +2276,22 @@ PyDict_GetItem(PyObject *op, PyObject *key)
22762276
"PyDict_GetItemRef() or PyDict_GetItemWithError()");
22772277
}
22782278

2279+
static void
2280+
dict_unhashtable_type(PyObject *key)
2281+
{
2282+
PyObject *exc = PyErr_GetRaisedException();
2283+
assert(exc != NULL);
2284+
if (!Py_IS_TYPE(exc, (PyTypeObject*)PyExc_TypeError)) {
2285+
PyErr_SetRaisedException(exc);
2286+
return;
2287+
}
2288+
2289+
PyErr_Format(PyExc_TypeError,
2290+
"cannot use '%T' as a dict key (%S)",
2291+
key, exc);
2292+
Py_DECREF(exc);
2293+
}
2294+
22792295
Py_ssize_t
22802296
_PyDict_LookupIndex(PyDictObject *mp, PyObject *key)
22812297
{
@@ -2286,6 +2302,7 @@ _PyDict_LookupIndex(PyDictObject *mp, PyObject *key)
22862302

22872303
Py_hash_t hash = _PyObject_HashFast(key);
22882304
if (hash == -1) {
2305+
dict_unhashtable_type(key);
22892306
return -1;
22902307
}
22912308

@@ -2382,6 +2399,7 @@ PyDict_GetItemRef(PyObject *op, PyObject *key, PyObject **result)
23822399

23832400
Py_hash_t hash = _PyObject_HashFast(key);
23842401
if (hash == -1) {
2402+
dict_unhashtable_type(key);
23852403
*result = NULL;
23862404
return -1;
23872405
}
@@ -2397,6 +2415,7 @@ _PyDict_GetItemRef_Unicode_LockHeld(PyDictObject *op, PyObject *key, PyObject **
23972415

23982416
Py_hash_t hash = _PyObject_HashFast(key);
23992417
if (hash == -1) {
2418+
dict_unhashtable_type(key);
24002419
*result = NULL;
24012420
return -1;
24022421
}
@@ -2434,6 +2453,7 @@ PyDict_GetItemWithError(PyObject *op, PyObject *key)
24342453
}
24352454
hash = _PyObject_HashFast(key);
24362455
if (hash == -1) {
2456+
dict_unhashtable_type(key);
24372457
return NULL;
24382458
}
24392459

@@ -2591,6 +2611,7 @@ setitem_take2_lock_held(PyDictObject *mp, PyObject *key, PyObject *value)
25912611
assert(PyDict_Check(mp));
25922612
Py_hash_t hash = _PyObject_HashFast(key);
25932613
if (hash == -1) {
2614+
dict_unhashtable_type(key);
25942615
Py_DECREF(key);
25952616
Py_DECREF(value);
25962617
return -1;
@@ -2742,6 +2763,7 @@ PyDict_DelItem(PyObject *op, PyObject *key)
27422763
assert(key);
27432764
Py_hash_t hash = _PyObject_HashFast(key);
27442765
if (hash == -1) {
2766+
dict_unhashtable_type(key);
27452767
return -1;
27462768
}
27472769

@@ -3064,6 +3086,7 @@ pop_lock_held(PyObject *op, PyObject *key, PyObject **result)
30643086

30653087
Py_hash_t hash = _PyObject_HashFast(key);
30663088
if (hash == -1) {
3089+
dict_unhashtable_type(key);
30673090
if (result) {
30683091
*result = NULL;
30693092
}
@@ -3398,6 +3421,7 @@ dict_subscript(PyObject *self, PyObject *key)
33983421

33993422
hash = _PyObject_HashFast(key);
34003423
if (hash == -1) {
3424+
dict_unhashtable_type(key);
34013425
return NULL;
34023426
}
34033427
ix = _Py_dict_lookup_threadsafe(mp, key, hash, &value);
@@ -4278,6 +4302,7 @@ dict_get_impl(PyDictObject *self, PyObject *key, PyObject *default_value)
42784302

42794303
hash = _PyObject_HashFast(key);
42804304
if (hash == -1) {
4305+
dict_unhashtable_type(key);
42814306
return NULL;
42824307
}
42834308
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
43104335

43114336
hash = _PyObject_HashFast(key);
43124337
if (hash == -1) {
4338+
dict_unhashtable_type(key);
43134339
if (result) {
43144340
*result = NULL;
43154341
}
@@ -4737,8 +4763,8 @@ int
47374763
PyDict_Contains(PyObject *op, PyObject *key)
47384764
{
47394765
Py_hash_t hash = _PyObject_HashFast(key);
4740-
47414766
if (hash == -1) {
4767+
dict_unhashtable_type(key);
47424768
return -1;
47434769
}
47444770

@@ -6829,6 +6855,7 @@ _PyDict_SetItem_LockHeld(PyDictObject *dict, PyObject *name, PyObject *value)
68296855
if (value == NULL) {
68306856
Py_hash_t hash = _PyObject_HashFast(name);
68316857
if (hash == -1) {
6858+
dict_unhashtable_type(name);
68326859
return -1;
68336860
}
68346861
return delitem_knownhash_lock_held((PyObject *)dict, name, hash);

Objects/setobject.c

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,11 +211,28 @@ set_add_entry(PySetObject *so, PyObject *key, Py_hash_t hash)
211211
return set_add_entry_takeref(so, Py_NewRef(key), hash);
212212
}
213213

214+
static void
215+
set_unhashtable_type(PyObject *key)
216+
{
217+
PyObject *exc = PyErr_GetRaisedException();
218+
assert(exc != NULL);
219+
if (!Py_IS_TYPE(exc, (PyTypeObject*)PyExc_TypeError)) {
220+
PyErr_SetRaisedException(exc);
221+
return;
222+
}
223+
224+
PyErr_Format(PyExc_TypeError,
225+
"cannot use '%T' as a set element (%S)",
226+
key, exc);
227+
Py_DECREF(exc);
228+
}
229+
214230
int
215231
_PySet_AddTakeRef(PySetObject *so, PyObject *key)
216232
{
217233
Py_hash_t hash = _PyObject_HashFast(key);
218234
if (hash == -1) {
235+
set_unhashtable_type(key);
219236
Py_DECREF(key);
220237
return -1;
221238
}
@@ -384,6 +401,7 @@ set_add_key(PySetObject *so, PyObject *key)
384401
{
385402
Py_hash_t hash = _PyObject_HashFast(key);
386403
if (hash == -1) {
404+
set_unhashtable_type(key);
387405
return -1;
388406
}
389407
return set_add_entry(so, key, hash);
@@ -394,6 +412,7 @@ set_contains_key(PySetObject *so, PyObject *key)
394412
{
395413
Py_hash_t hash = _PyObject_HashFast(key);
396414
if (hash == -1) {
415+
set_unhashtable_type(key);
397416
return -1;
398417
}
399418
return set_contains_entry(so, key, hash);
@@ -404,6 +423,7 @@ set_discard_key(PySetObject *so, PyObject *key)
404423
{
405424
Py_hash_t hash = _PyObject_HashFast(key);
406425
if (hash == -1) {
426+
set_unhashtable_type(key);
407427
return -1;
408428
}
409429
return set_discard_entry(so, key, hash);

0 commit comments

Comments
 (0)