From 913572c2ef8a4c948bb8b67ff2064d6920e313e7 Mon Sep 17 00:00:00 2001 From: TIGirardi <55336837+TIGirardi@users.noreply.github.com> Date: Mon, 18 May 2020 00:58:10 -0300 Subject: Accept None as a key in pure python module (#42) --- immutables/map.py | 31 +-- tests/test_none_keys.py | 511 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 530 insertions(+), 12 deletions(-) create mode 100644 tests/test_none_keys.py diff --git a/immutables/map.py b/immutables/map.py index ac7ebd7..7c16139 100644 --- a/immutables/map.py +++ b/immutables/map.py @@ -46,6 +46,13 @@ def map_bitindex(bitmap, bit): W_EMPTY, W_NEWNODE, W_NOT_FOUND = range(3) void = object() +class _Unhashable: + __slots__ = () + __hash__ = None + +_NULL = _Unhashable() +del _Unhashable + class BitmapNode: @@ -70,7 +77,7 @@ class BitmapNode: key_or_null = self.array[key_idx] val_or_node = self.array[val_idx] - if key_or_null is None: + if key_or_null is _NULL: sub_node, added = val_or_node.assoc( shift + 5, hash, key, val, mutid) if val_or_node is sub_node: @@ -111,12 +118,12 @@ class BitmapNode: mutid) if mutid and mutid == self.mutid: - self.array[key_idx] = None + self.array[key_idx] = _NULL self.array[val_idx] = sub_node return self, True else: ret = self.clone(mutid) - ret.array[key_idx] = None + ret.array[key_idx] = _NULL ret.array[val_idx] = sub_node return ret, True @@ -153,7 +160,7 @@ class BitmapNode: key_or_null = self.array[key_idx] val_or_node = self.array[val_idx] - if key_or_null is None: + if key_or_null is _NULL: return val_or_node.find(shift + 5, hash, key) if key == key_or_null: @@ -173,7 +180,7 @@ class BitmapNode: key_or_null = self.array[key_idx] val_or_node = self.array[val_idx] - if key_or_null is None: + if key_or_null is _NULL: res, sub_node = val_or_node.without(shift + 5, hash, key, mutid) if res is W_EMPTY: @@ -182,7 +189,7 @@ class BitmapNode: elif res is W_NEWNODE: if (type(sub_node) is BitmapNode and sub_node.size == 2 and - sub_node.array[0] is not None): + sub_node.array[0] is not _NULL): if mutid and mutid == self.mutid: self.array[key_idx] = sub_node.array[0] @@ -231,7 +238,7 @@ class BitmapNode: for i in range(0, self.size, 2): key_or_null = self.array[i] - if key_or_null is None: + if key_or_null is _NULL: val_or_node = self.array[i + 1] yield from val_or_node.keys() else: @@ -242,7 +249,7 @@ class BitmapNode: key_or_null = self.array[i] val_or_node = self.array[i + 1] - if key_or_null is None: + if key_or_null is _NULL: yield from val_or_node.values() else: yield val_or_node @@ -252,7 +259,7 @@ class BitmapNode: key_or_null = self.array[i] val_or_node = self.array[i + 1] - if key_or_null is None: + if key_or_null is _NULL: yield from val_or_node.items() else: yield key_or_null, val_or_node @@ -269,8 +276,8 @@ class BitmapNode: pad = ' ' * (level + 2) - if key_or_null is None: - buf.append(pad + 'None:') + if key_or_null is _NULL: + buf.append(pad + 'NULL:') val_or_node.dump(buf, level + 2) else: buf.append(pad + '{!r}: {!r}'.format(key_or_null, val_or_node)) @@ -328,7 +335,7 @@ class CollisionNode: else: new_node = BitmapNode( - 2, map_bitpos(self.hash, shift), [None, self], mutid) + 2, map_bitpos(self.hash, shift), [_NULL, self], mutid) return new_node.assoc(shift, hash, key, val, mutid) def without(self, shift, hash, key, mutid): diff --git a/tests/test_none_keys.py b/tests/test_none_keys.py new file mode 100644 index 0000000..3662e9c --- /dev/null +++ b/tests/test_none_keys.py @@ -0,0 +1,511 @@ +import unittest + +from immutables.map import map_hash, map_mask, Map as PyMap +from tests.test_map import HashKey + + +none_hash = map_hash(None) +assert(none_hash != 1) +assert((none_hash >> 32) == 0) + +not_collision = 0xffffffff & (~none_hash) + +mask = 0x7ffffffff +none_collisions = [none_hash & (mask >> shift) + for shift in reversed(range(0, 32, 5))] +assert(len(none_collisions) == 7) +none_collisions = [h | (not_collision & (mask << shift)) + for shift, h in zip(range(5, 37, 5), none_collisions)] + + +class NoneCollision(HashKey): + def __init__(self, name, level): + if name is None: + raise ValueError("Can't have a NoneCollision with a None value") + super().__init__(none_collisions[level], name) + + def __eq__(self, other): + if other is None: + return False + return super().__eq__(other) + + __hash__ = HashKey.__hash__ + + +class BaseNoneTest: + Map = None + + def test_none_collisions(self): + collisions = [NoneCollision('a', level) for level in range(7)] + indices = [map_mask(none_hash, shift) for shift in range(0, 32, 5)] + + for i, c in enumerate(collisions[:-1], 1): + self.assertNotEqual(c, None) + c_hash = map_hash(c) + self.assertNotEqual(c_hash, none_hash) + for j, idx in enumerate(indices[:i]): + self.assertEqual(map_mask(c_hash, j*5), idx) + for j, idx in enumerate(indices[i:], i): + self.assertNotEqual(map_mask(c_hash, j*5), idx) + + c = collisions[-1] + self.assertNotEqual(c, None) + c_hash = map_hash(c) + self.assertEqual(c_hash, none_hash) + for i, idx in enumerate(indices): + self.assertEqual(map_mask(c_hash, i*5), idx) + + def test_none_as_key(self): + m = self.Map({None: 1}) + + self.assertEqual(len(m), 1) + self.assertTrue(None in m) + self.assertEqual(m[None], 1) + self.assertTrue(repr(m).startswith('