Skip to content

Commit 3321b66

Browse files
author
Luke Lovett
committed
PYTHON-977 - Fix __hash__ method on BSON types that inherit from Python builtin types.
In Python 2, objects automatically inherit the __hash__ of their parent class. In Python 3, objects that override __eq__ do not automatically inherit __hash__, so these objects were not hashable under Python 3. Additionally, mutable BSON types and types that overide __eq__ but did not explicitly define __hash__ had broken __hash__ methods under Python 2. This commit unifies the hashing behavior between Python versions and fixes the __hash__ methods such that two BSON objects hash the same only if they are equal. N.B.: bson.code.Code and bson.regex.Regex are no longer hashable under Python 2 because they are mutable.
1 parent 4edbd03 commit 3321b66

File tree

10 files changed

+40
-0
lines changed

10 files changed

+40
-0
lines changed

bson/binary.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,9 @@ def __eq__(self, other):
171171
# subclass of str...
172172
return False
173173

174+
def __hash__(self):
175+
return super(Binary, self).__hash__() ^ hash(self.__subtype)
176+
174177
def __ne__(self, other):
175178
return not self == other
176179

bson/code.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,5 +77,7 @@ def __eq__(self, other):
7777
return (self.__scope, str(self)) == (other.__scope, str(other))
7878
return False
7979

80+
__hash__ = None
81+
8082
def __ne__(self, other):
8183
return not self == other

bson/max_key.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ class MaxKey(object):
2828
def __eq__(self, other):
2929
return isinstance(other, MaxKey)
3030

31+
def __hash__(self):
32+
return hash(self._type_marker)
33+
3134
def __ne__(self, other):
3235
return not self == other
3336

bson/min_key.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ class MinKey(object):
2828
def __eq__(self, other):
2929
return isinstance(other, MinKey)
3030

31+
def __hash__(self):
32+
return hash(self._type_marker)
33+
3134
def __ne__(self, other):
3235
return not self == other
3336

bson/regex.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ def __eq__(self, other):
104104
else:
105105
return NotImplemented
106106

107+
__hash__ = None
108+
107109
def __ne__(self, other):
108110
return not self == other
109111

bson/timestamp.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
UPPERBOUND = 4294967296
2525

26+
2627
class Timestamp(object):
2728
"""MongoDB internal timestamps used in the opLog.
2829
"""
@@ -81,6 +82,9 @@ def __eq__(self, other):
8182
else:
8283
return NotImplemented
8384

85+
def __hash__(self):
86+
return hash(self.time) ^ hash(self.inc)
87+
8488
def __ne__(self, other):
8589
return not self == other
8690

test/test_binary.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,13 @@ def test_repr(self):
129129
self.assertEqual(repr(five),
130130
"Binary(%s, 100)" % (repr(b"test"),))
131131

132+
def test_hash(self):
133+
one = Binary(b"hello world")
134+
two = Binary(b"hello world", 42)
135+
self.assertEqual(hash(Binary(b"hello world")), hash(one))
136+
self.assertNotEqual(hash(one), hash(two))
137+
self.assertEqual(hash(Binary(b"hello world", 42)), hash(two))
138+
132139
def test_legacy_java_uuid(self):
133140
# Test decoding
134141
data = self.java_data

test/test_bson.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,9 @@ def test_regex_from_native(self):
759759
unicode_regex = re.compile('', re.U)
760760
self.assertEqual(re.U, Regex.from_native(unicode_regex).flags)
761761

762+
def test_regex_hash(self):
763+
self.assertRaises(TypeError, hash, Regex('hello'))
764+
762765
def test_exception_wrapping(self):
763766
# No matter what exception is raised while trying to decode BSON,
764767
# the final exception always matches InvalidBSON.
@@ -815,6 +818,11 @@ def test_minkey_maxkey_comparison(self):
815818
self.assertTrue(MaxKey() != MinKey())
816819
self.assertFalse(MaxKey() == MinKey())
817820

821+
def test_minkey_maxkey_hash(self):
822+
self.assertEqual(hash(MaxKey()), hash(MaxKey()))
823+
self.assertEqual(hash(MinKey()), hash(MinKey()))
824+
self.assertNotEqual(hash(MaxKey()), hash(MinKey()))
825+
818826
def test_timestamp_comparison(self):
819827
# Timestamp is initialized with time, inc. Time is the more
820828
# significant comparand.

test/test_code.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ def test_equality(self):
7777
self.assertFalse(b != Code("hello"))
7878
self.assertFalse(b != Code("hello", {}))
7979

80+
def test_hash(self):
81+
self.assertRaises(TypeError, hash, Code("hello world"))
82+
8083
def test_scope_preserved(self):
8184
a = Code("hello")
8285
b = Code("hello", {"foo": 5})

test/test_timestamp.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ def test_equality(self):
6969
# Explicitly test inequality
7070
self.assertFalse(t != Timestamp(1, 1))
7171

72+
def test_hash(self):
73+
self.assertEqual(hash(Timestamp(1, 2)), hash(Timestamp(1, 2)))
74+
self.assertNotEqual(hash(Timestamp(1, 2)), hash(Timestamp(1, 3)))
75+
self.assertNotEqual(hash(Timestamp(1, 2)), hash(Timestamp(2, 2)))
76+
7277
def test_repr(self):
7378
t = Timestamp(0, 0)
7479
self.assertEqual(repr(t), "Timestamp(0, 0)")

0 commit comments

Comments
 (0)