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.
This commit is contained in:
parent
4edbd03d2b
commit
3321b66d34
@ -171,6 +171,9 @@ class Binary(bytes):
|
||||
# subclass of str...
|
||||
return False
|
||||
|
||||
def __hash__(self):
|
||||
return super(Binary, self).__hash__() ^ hash(self.__subtype)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
|
||||
@ -77,5 +77,7 @@ class Code(str):
|
||||
return (self.__scope, str(self)) == (other.__scope, str(other))
|
||||
return False
|
||||
|
||||
__hash__ = None
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
@ -28,6 +28,9 @@ class MaxKey(object):
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, MaxKey)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self._type_marker)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
|
||||
@ -28,6 +28,9 @@ class MinKey(object):
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, MinKey)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self._type_marker)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
|
||||
@ -104,6 +104,8 @@ class Regex(object):
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
__hash__ = None
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
|
||||
@ -23,6 +23,7 @@ from bson.tz_util import utc
|
||||
|
||||
UPPERBOUND = 4294967296
|
||||
|
||||
|
||||
class Timestamp(object):
|
||||
"""MongoDB internal timestamps used in the opLog.
|
||||
"""
|
||||
@ -81,6 +82,9 @@ class Timestamp(object):
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.time) ^ hash(self.inc)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
|
||||
@ -129,6 +129,13 @@ class TestBinary(unittest.TestCase):
|
||||
self.assertEqual(repr(five),
|
||||
"Binary(%s, 100)" % (repr(b"test"),))
|
||||
|
||||
def test_hash(self):
|
||||
one = Binary(b"hello world")
|
||||
two = Binary(b"hello world", 42)
|
||||
self.assertEqual(hash(Binary(b"hello world")), hash(one))
|
||||
self.assertNotEqual(hash(one), hash(two))
|
||||
self.assertEqual(hash(Binary(b"hello world", 42)), hash(two))
|
||||
|
||||
def test_legacy_java_uuid(self):
|
||||
# Test decoding
|
||||
data = self.java_data
|
||||
|
||||
@ -759,6 +759,9 @@ class TestBSON(unittest.TestCase):
|
||||
unicode_regex = re.compile('', re.U)
|
||||
self.assertEqual(re.U, Regex.from_native(unicode_regex).flags)
|
||||
|
||||
def test_regex_hash(self):
|
||||
self.assertRaises(TypeError, hash, Regex('hello'))
|
||||
|
||||
def test_exception_wrapping(self):
|
||||
# No matter what exception is raised while trying to decode BSON,
|
||||
# the final exception always matches InvalidBSON.
|
||||
@ -815,6 +818,11 @@ class TestBSON(unittest.TestCase):
|
||||
self.assertTrue(MaxKey() != MinKey())
|
||||
self.assertFalse(MaxKey() == MinKey())
|
||||
|
||||
def test_minkey_maxkey_hash(self):
|
||||
self.assertEqual(hash(MaxKey()), hash(MaxKey()))
|
||||
self.assertEqual(hash(MinKey()), hash(MinKey()))
|
||||
self.assertNotEqual(hash(MaxKey()), hash(MinKey()))
|
||||
|
||||
def test_timestamp_comparison(self):
|
||||
# Timestamp is initialized with time, inc. Time is the more
|
||||
# significant comparand.
|
||||
|
||||
@ -77,6 +77,9 @@ class TestCode(unittest.TestCase):
|
||||
self.assertFalse(b != Code("hello"))
|
||||
self.assertFalse(b != Code("hello", {}))
|
||||
|
||||
def test_hash(self):
|
||||
self.assertRaises(TypeError, hash, Code("hello world"))
|
||||
|
||||
def test_scope_preserved(self):
|
||||
a = Code("hello")
|
||||
b = Code("hello", {"foo": 5})
|
||||
|
||||
@ -69,6 +69,11 @@ class TestTimestamp(unittest.TestCase):
|
||||
# Explicitly test inequality
|
||||
self.assertFalse(t != Timestamp(1, 1))
|
||||
|
||||
def test_hash(self):
|
||||
self.assertEqual(hash(Timestamp(1, 2)), hash(Timestamp(1, 2)))
|
||||
self.assertNotEqual(hash(Timestamp(1, 2)), hash(Timestamp(1, 3)))
|
||||
self.assertNotEqual(hash(Timestamp(1, 2)), hash(Timestamp(2, 2)))
|
||||
|
||||
def test_repr(self):
|
||||
t = Timestamp(0, 0)
|
||||
self.assertEqual(repr(t), "Timestamp(0, 0)")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user