Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions Lib/test/test_tuple.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,45 @@ def test_lexicographic_ordering(self):
self.assertLess(a, b)
self.assertLess(b, c)

def test_hash_unhashable_note(self):
with self.assertRaisesRegex(TypeError, "unhashable type: 'list'") as cm:
hash(("alice", []))
self.assertEqual(cm.exception.__notes__,
["when hashing tuple item 1"])

with self.assertRaisesRegex(TypeError, "unhashable type: 'list'") as cm:
hash((("alice", []), 42))
self.assertEqual(cm.exception.__notes__,
["when hashing tuple item 1",
"when hashing tuple item 0"])

class BrokenHash:
def __hash__(self):
raise TypeError("custom failure")

with self.assertRaisesRegex(TypeError, "custom failure") as cm:
hash((BrokenHash(),))
self.assertEqual(cm.exception.__notes__, ["when hashing tuple item 0"])

class BrokenHashNote:
def __hash__(self):
exc = TypeError("custom failure")
exc.add_note("from custom hash")
raise exc

with self.assertRaisesRegex(TypeError, "custom failure") as cm:
hash((BrokenHashNote(),))
self.assertEqual(cm.exception.__notes__,
["from custom hash", "when hashing tuple item 0"])

class BrokenHashValueError:
def __hash__(self):
raise ValueError("custom failure")

with self.assertRaisesRegex(ValueError, "custom failure") as cm:
hash((BrokenHashValueError(),))
self.assertFalse(hasattr(cm.exception, "__notes__"))

# Notes on testing hash codes. The primary thing is that Python doesn't
# care about "random" hash codes. To the contrary, we like them to be
# very regular when possible, so that the low-order bits are as evenly
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add context notes to :exc:`TypeError` raised while hashing a
:class:`tuple`, identifying the index of the item whose hash failed.
18 changes: 18 additions & 0 deletions Objects/tupleobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "pycore_list.h" // _Py_memory_repeat()
#include "pycore_modsupport.h" // _PyArg_NoKwnames()
#include "pycore_object.h" // _PyObject_GC_TRACK()
#include "pycore_pyerrors.h" // _PyException_AddNote()
#include "pycore_stackref.h" // PyStackRef_AsPyObjectSteal()
#include "pycore_tuple.h" // _PyTupleIterObject

Expand Down Expand Up @@ -392,6 +393,23 @@ tuple_hash(PyObject *op)
for (Py_ssize_t i = 0; i < len; i++) {
Py_uhash_t lane = PyObject_Hash(item[i]);
if (lane == (Py_uhash_t)-1) {
if (PyErr_ExceptionMatches(PyExc_TypeError)) {
/* Preserve the original error while providing context for
the element that could not be hashed. */
PyObject *exc = PyErr_GetRaisedException();
PyObject *note = PyUnicode_FromFormat(
"when hashing tuple item %zd", i);
if (note != NULL) {
if (_PyException_AddNote(exc, note) < 0) {
PyErr_Clear();
}
Py_DECREF(note);
}
else {
PyErr_Clear();
}
PyErr_SetRaisedException(exc);
}
_Py_LeaveRecursiveCall();
return -1;
}
Expand Down
Loading