Skip to content

Commit 53663a6

Browse files
committed
Issue 2235: __hash__ is once again inherited by default, but inheritance can be blocked explicitly so that collections.Hashable remains meaningful
1 parent 9ace15c commit 53663a6

14 files changed

Lines changed: 134 additions & 134 deletions

Include/object.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,7 @@ PyAPI_FUNC(PyObject *) PyObject_GenericGetAttr(PyObject *, PyObject *);
475475
PyAPI_FUNC(int) PyObject_GenericSetAttr(PyObject *,
476476
PyObject *, PyObject *);
477477
PyAPI_FUNC(long) PyObject_Hash(PyObject *);
478+
PyAPI_FUNC(long) PyObject_HashNotImplemented(PyObject *);
478479
PyAPI_FUNC(int) PyObject_IsTrue(PyObject *);
479480
PyAPI_FUNC(int) PyObject_Not(PyObject *);
480481
PyAPI_FUNC(int) PyCallable_Check(PyObject *);

Lib/UserString.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,10 @@ def __init__(self, string=""):
150150
warnpy3k('the class UserString.MutableString has been removed in '
151151
'Python 3.0', stacklevel=2)
152152
self.data = string
153-
def __hash__(self):
154-
raise TypeError, "unhashable type (it is mutable)"
153+
154+
# We inherit object.__hash__, so we must deny this explicitly
155+
__hash__ = None
156+
155157
def __setitem__(self, index, sub):
156158
if isinstance(index, slice):
157159
if isinstance(sub, UserString):

Lib/decimal.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3697,10 +3697,8 @@ def _regard_flags(self, *flags):
36973697
for flag in flags:
36983698
self._ignored_flags.remove(flag)
36993699

3700-
def __hash__(self):
3701-
"""A Context cannot be hashed."""
3702-
# We inherit object.__hash__, so we must deny this explicitly
3703-
raise TypeError("Cannot hash a Context.")
3700+
# We inherit object.__hash__, so we must deny this explicitly
3701+
__hash__ = None
37043702

37053703
def Etiny(self):
37063704
"""Returns Etiny (= Emin - prec + 1)"""

Lib/sets.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -439,10 +439,8 @@ def __getstate__(self):
439439
def __setstate__(self, data):
440440
self._data, = data
441441

442-
def __hash__(self):
443-
"""A Set cannot be hashed."""
444-
# We inherit object.__hash__, so we must deny this explicitly
445-
raise TypeError, "Can't hash a Set, only an ImmutableSet."
442+
# We inherit object.__hash__, so we must deny this explicitly
443+
__hash__ = None
446444

447445
# In-place union, intersection, differences.
448446
# Subtle: The xyz_update() functions deliberately return None,

Lib/test/seq_tests.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,8 +214,7 @@ class AllEq:
214214
# So instances of AllEq must be found in all non-empty sequences.
215215
def __eq__(self, other):
216216
return True
217-
def __hash__(self):
218-
raise NotImplemented
217+
__hash__ = None # Can't meet hash invariant requirements
219218
self.assert_(AllEq() not in self.type2test([]))
220219
self.assert_(AllEq() in self.type2test([1]))
221220

Lib/test/test_descr.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3283,12 +3283,20 @@ class D(B, C):
32833283
self.assertEqual(hash(d), 144)
32843284
D.__hash__ = lambda self: 100
32853285
self.assertEqual(hash(d), 100)
3286+
D.__hash__ = None
3287+
self.assertRaises(TypeError, hash, d)
32863288
del D.__hash__
32873289
self.assertEqual(hash(d), 144)
3290+
B.__hash__ = None
3291+
self.assertRaises(TypeError, hash, d)
32883292
del B.__hash__
32893293
self.assertEqual(hash(d), 314)
3294+
C.__hash__ = None
3295+
self.assertRaises(TypeError, hash, d)
32903296
del C.__hash__
32913297
self.assertEqual(hash(d), 42)
3298+
A.__hash__ = None
3299+
self.assertRaises(TypeError, hash, d)
32923300
del A.__hash__
32933301
self.assertEqual(hash(d), orig_hash)
32943302
d.foo = 42

Lib/test/test_hash.py

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
# test the invariant that
22
# iff a==b then hash(a)==hash(b)
33
#
4+
# Also test that hash implementations are inherited as expected
45

56
import unittest
67
from test import test_support
8+
from collections import Hashable
79

810

911
class HashEqualityTestCase(unittest.TestCase):
@@ -39,8 +41,83 @@ def test_coerced_floats(self):
3941
self.same_hash(float(0.5), complex(0.5, 0.0))
4042

4143

44+
_default_hash = object.__hash__
45+
class DefaultHash(object): pass
46+
47+
_FIXED_HASH_VALUE = 42
48+
class FixedHash(object):
49+
def __hash__(self):
50+
return _FIXED_HASH_VALUE
51+
52+
class OnlyEquality(object):
53+
def __eq__(self, other):
54+
return self is other
55+
56+
class OnlyInequality(object):
57+
def __ne__(self, other):
58+
return self is not other
59+
60+
class OnlyCmp(object):
61+
def __cmp__(self, other):
62+
return cmp(id(self), id(other))
63+
64+
class InheritedHashWithEquality(FixedHash, OnlyEquality): pass
65+
class InheritedHashWithInequality(FixedHash, OnlyInequality): pass
66+
class InheritedHashWithCmp(FixedHash, OnlyCmp): pass
67+
68+
class NoHash(object):
69+
__hash__ = None
70+
71+
class HashInheritanceTestCase(unittest.TestCase):
72+
default_expected = [object(),
73+
DefaultHash(),
74+
]
75+
fixed_expected = [FixedHash(),
76+
InheritedHashWithEquality(),
77+
InheritedHashWithInequality(),
78+
InheritedHashWithCmp(),
79+
]
80+
# TODO: Change these to expecting an exception
81+
# when forward porting to Py3k
82+
warning_expected = [OnlyEquality(),
83+
OnlyInequality(),
84+
OnlyCmp(),
85+
]
86+
error_expected = [NoHash()]
87+
88+
def test_default_hash(self):
89+
for obj in self.default_expected:
90+
self.assertEqual(hash(obj), _default_hash(obj))
91+
92+
def test_fixed_hash(self):
93+
for obj in self.fixed_expected:
94+
self.assertEqual(hash(obj), _FIXED_HASH_VALUE)
95+
96+
def test_warning_hash(self):
97+
for obj in self.warning_expected:
98+
# TODO: Check for the expected Py3k warning here
99+
obj_hash = hash(obj)
100+
self.assertEqual(obj_hash, _default_hash(obj))
101+
102+
def test_error_hash(self):
103+
for obj in self.error_expected:
104+
self.assertRaises(TypeError, hash, obj)
105+
106+
def test_hashable(self):
107+
objects = (self.default_expected +
108+
self.fixed_expected +
109+
self.warning_expected)
110+
for obj in objects:
111+
self.assert_(isinstance(obj, Hashable), repr(obj))
112+
113+
def test_not_hashable(self):
114+
for obj in self.error_expected:
115+
self.assertFalse(isinstance(obj, Hashable), repr(obj))
116+
117+
42118
def test_main():
43-
test_support.run_unittest(HashEqualityTestCase)
119+
test_support.run_unittest(HashEqualityTestCase,
120+
HashInheritanceTestCase)
44121

45122

46123
if __name__ == "__main__":

Lib/test/test_richcmp.py

Lines changed: 2 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,7 @@ def __getitem__(self, i):
4848
def __setitem__(self, i, v):
4949
self.data[i] = v
5050

51-
def __hash__(self):
52-
raise TypeError, "Vectors cannot be hashed"
51+
__hash__ = None # Vectors cannot be hashed
5352

5453
def __nonzero__(self):
5554
raise TypeError, "Vectors cannot be used in Boolean contexts"
@@ -85,35 +84,6 @@ def __cast(self, other):
8584
raise ValueError, "Cannot compare vectors of different length"
8685
return other
8786

88-
89-
class SimpleOrder(object):
90-
"""
91-
A simple class that defines order but not full comparison.
92-
"""
93-
94-
def __init__(self, value):
95-
self.value = value
96-
97-
def __lt__(self, other):
98-
if not isinstance(other, SimpleOrder):
99-
return True
100-
return self.value < other.value
101-
102-
def __gt__(self, other):
103-
if not isinstance(other, SimpleOrder):
104-
return False
105-
return self.value > other.value
106-
107-
108-
class DumbEqualityWithoutHash(object):
109-
"""
110-
A class that define __eq__, but no __hash__: it shouldn't be hashable.
111-
"""
112-
113-
def __eq__(self, other):
114-
return False
115-
116-
11787
opmap = {
11888
"lt": (lambda a,b: a< b, operator.lt, operator.__lt__),
11989
"le": (lambda a,b: a<=b, operator.le, operator.__le__),
@@ -359,39 +329,8 @@ def __lt__(self, other):
359329
for op in opmap["lt"]:
360330
self.assertIs(op(x, y), True)
361331

362-
363-
class HashableTest(unittest.TestCase):
364-
"""
365-
Test hashability of classes with rich operators defined.
366-
"""
367-
368-
def test_simpleOrderHashable(self):
369-
"""
370-
A class that only defines __gt__ and/or __lt__ should be hashable.
371-
"""
372-
a = SimpleOrder(1)
373-
b = SimpleOrder(2)
374-
self.assert_(a < b)
375-
self.assert_(b > a)
376-
self.assert_(a.__hash__ is not None)
377-
378-
def test_notHashableException(self):
379-
"""
380-
If a class is not hashable, it should raise a TypeError with an
381-
understandable message.
382-
"""
383-
a = DumbEqualityWithoutHash()
384-
try:
385-
hash(a)
386-
except TypeError, e:
387-
self.assertEquals(str(e),
388-
"unhashable type: 'DumbEqualityWithoutHash'")
389-
else:
390-
raise test_support.TestFailed("Should not be here")
391-
392-
393332
def test_main():
394-
test_support.run_unittest(VectorTest, NumberTest, MiscTest, DictTest, ListTest, HashableTest)
333+
test_support.run_unittest(VectorTest, NumberTest, MiscTest, DictTest, ListTest)
395334

396335
if __name__ == "__main__":
397336
test_main()

Modules/_collectionsmodule.c

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -608,13 +608,6 @@ deque_traverse(dequeobject *deque, visitproc visit, void *arg)
608608
return 0;
609609
}
610610

611-
static long
612-
deque_nohash(PyObject *self)
613-
{
614-
PyErr_SetString(PyExc_TypeError, "deque objects are unhashable");
615-
return -1;
616-
}
617-
618611
static PyObject *
619612
deque_copy(PyObject *deque)
620613
{
@@ -917,7 +910,7 @@ static PyTypeObject deque_type = {
917910
0, /* tp_as_number */
918911
&deque_as_sequence, /* tp_as_sequence */
919912
0, /* tp_as_mapping */
920-
deque_nohash, /* tp_hash */
913+
(hashfunc)PyObject_HashNotImplemented, /* tp_hash */
921914
0, /* tp_call */
922915
0, /* tp_str */
923916
PyObject_GenericGetAttr, /* tp_getattro */

Objects/dictobject.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2240,7 +2240,7 @@ PyTypeObject PyDict_Type = {
22402240
0, /* tp_as_number */
22412241
&dict_as_sequence, /* tp_as_sequence */
22422242
&dict_as_mapping, /* tp_as_mapping */
2243-
0, /* tp_hash */
2243+
(hashfunc)PyObject_HashNotImplemented, /* tp_hash */
22442244
0, /* tp_call */
22452245
0, /* tp_str */
22462246
PyObject_GenericGetAttr, /* tp_getattro */

0 commit comments

Comments
 (0)