Skip to content

Commit

Permalink
Merge pull request #58 from zopefoundation/allow_None
Browse files Browse the repository at this point in the history
Allow None as a special key (sorted smaller than all others).
  • Loading branch information
jimfulton committed Jan 11, 2017
2 parents eb8a9f1 + f7991e6 commit e7991d0
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 32 deletions.
47 changes: 24 additions & 23 deletions BTrees/_base.py
Expand Up @@ -22,7 +22,7 @@

from .Interfaces import BTreesConflictError
from ._compat import PY3
from ._compat import cmp
from ._compat import compare
from ._compat import int_types
from ._compat import xrange

Expand Down Expand Up @@ -113,7 +113,8 @@ def _search(self, key):
k = keys[i]
if k is key or k == key:
return i
if k < key:

if compare(k, key) < 0:
low = i + 1
else:
high = i
Expand Down Expand Up @@ -256,7 +257,7 @@ def advance(self):
def _no_default_comparison(key):
# Enforce test that key has non-default comparison.
if key is None:
raise TypeError("Can't use None as a key")
return
if type(key) is object:
raise TypeError("Can't use object() as keys")
lt = getattr(key, '__lt__', None)
Expand Down Expand Up @@ -458,8 +459,8 @@ def merge_output(it):
it.advance()

while i_old.active and i_com.active and i_new.active:
cmpOC = cmp(i_old.key, i_com.key)
cmpON = cmp(i_old.key, i_new.key)
cmpOC = compare(i_old.key, i_com.key)
cmpON = compare(i_old.key, i_new.key)
if cmpOC == 0:
if cmpON == 0:
if i_com.value == i_old.value:
Expand Down Expand Up @@ -497,7 +498,7 @@ def merge_output(it):
else:
raise merge_error(3)
else: # both keys changed
cmpCN = cmp(i_com.key, i_new.key)
cmpCN = compare(i_com.key, i_new.key)
if cmpCN == 0: # dueling insert
raise merge_error(4)
if cmpOC > 0: # insert committed
Expand All @@ -511,7 +512,7 @@ def merge_output(it):
raise merge_error(5) # both deleted same key

while i_com.active and i_new.active: # new inserts
cmpCN = cmp(i_com.key, i_new.key)
cmpCN = compare(i_com.key, i_new.key)
if cmpCN == 0:
raise merge_error(6) # dueling insert
if cmpCN > 0: # insert new
Expand All @@ -520,7 +521,7 @@ def merge_output(it):
merge_output(i_com)

while i_old.active and i_com.active: # new deletes rest of original
cmpOC = cmp(i_old.key, i_com.key)
cmpOC = compare(i_old.key, i_com.key)
if cmpOC > 0: # insert committed
merge_output(i_com)
elif cmpOC == 0 and (i_old.value == i_com.value): # del in new
Expand All @@ -531,7 +532,7 @@ def merge_output(it):

while i_old.active and i_new.active:
# committed deletes rest of original
cmpON = cmp(i_old.key, i_new.key)
cmpON = compare(i_old.key, i_new.key)
if cmpON > 0: # insert new
merge_output(i_new)
elif cmpON == 0 and (i_old.value == i_new.value):
Expand Down Expand Up @@ -665,8 +666,8 @@ def merge_output(it):
it.advance()

while i_old.active and i_com.active and i_new.active:
cmpOC = cmp(i_old.key, i_com.key)
cmpON = cmp(i_old.key, i_new.key)
cmpOC = compare(i_old.key, i_com.key)
cmpON = compare(i_old.key, i_new.key)
if cmpOC == 0:
if cmpON == 0: # all match
merge_output(i_old)
Expand Down Expand Up @@ -694,7 +695,7 @@ def merge_output(it):
i_old.advance()
i_new.advance()
else: # both com and new keys changed
cmpCN = cmp(i_com.key, i_new.key)
cmpCN = compare(i_com.key, i_new.key)
if cmpCN == 0: # both inserted same key
raise merge_error(4)
if cmpOC > 0: # insert committed
Expand All @@ -708,7 +709,7 @@ def merge_output(it):
raise merge_error(5)

while i_com.active and i_new.active: # new inserts
cmpCN = cmp(i_com.key, i_new.key)
cmpCN = compare(i_com.key, i_new.key)
if cmpCN == 0: # dueling insert
raise merge_error(6)
if cmpCN > 0: # insert new
Expand All @@ -717,7 +718,7 @@ def merge_output(it):
merge_output(i_com)

while i_old.active and i_com.active: # new deletes rest of original
cmpOC = cmp(i_old.key, i_com.key)
cmpOC = compare(i_old.key, i_com.key)
if cmpOC > 0: # insert committed
merge_output(i_com)
elif cmpOC == 0: # del in new
Expand All @@ -728,7 +729,7 @@ def merge_output(it):

while i_old.active and i_new.active:
# committed deletes rest of original
cmpON = cmp(i_old.key, i_new.key)
cmpON = compare(i_old.key, i_new.key)
if cmpON > 0: # insert new
merge_output(i_new)
elif cmpON == 0: # deleted in committed
Expand Down Expand Up @@ -843,7 +844,7 @@ def _search(self, key):
hi = len(data)
i = hi // 2
while i > lo:
cmp_ = cmp(data[i].key, key)
cmp_ = compare(data[i].key, key)
if cmp_ < 0:
lo = i
elif cmp_ > 0:
Expand Down Expand Up @@ -919,7 +920,7 @@ def maxKey(self, max=_marker):

max = self._to_key(max)
index = self._search(max)
if index and data[index].child.minKey() > max:
if index and compare(data[index].child.minKey(), max) > 0:
index -= 1 #pragma: no cover no idea how to provoke this
return data[index].child.maxKey(max)

Expand Down Expand Up @@ -1014,7 +1015,7 @@ def _del(self, key):
self._p_changed = True

# fix up the node key, but not for the 0'th one.
if index > 0 and child.size and key == data[index].key:
if index > 0 and child.size and compare(key, data[index].key) == 0:
self._p_changed = True
data[index].key = child.minKey()

Expand Down Expand Up @@ -1324,7 +1325,7 @@ def copy(i):
def copy(i):
result._keys.append(i.key)
while i1.active and i2.active:
cmp_ = cmp(i1.key, i2.key)
cmp_ = compare(i1.key, i2.key)
if cmp_ < 0:
copy(i1)
i1.advance()
Expand All @@ -1349,7 +1350,7 @@ def union(set_type, o1, o2):
def copy(i):
result._keys.append(i.key)
while i1.active and i2.active:
cmp_ = cmp(i1.key, i2.key)
cmp_ = compare(i1.key, i2.key)
if cmp_ < 0:
copy(i1)
i1.advance()
Expand Down Expand Up @@ -1379,7 +1380,7 @@ def intersection(set_type, o1, o2):
def copy(i):
result._keys.append(i.key)
while i1.active and i2.active:
cmp_ = cmp(i1.key, i2.key)
cmp_ = compare(i1.key, i2.key)
if cmp_ < 0:
i1.advance()
elif cmp_ == 0:
Expand Down Expand Up @@ -1425,7 +1426,7 @@ def copy(i, w):
result._keys.append(i.key)

while i1.active and i2.active:
cmp_ = cmp(i1.key, i2.key)
cmp_ = compare(i1.key, i2.key)
if cmp_ < 0:
copy(i1, w1)
i1.advance()
Expand Down Expand Up @@ -1466,7 +1467,7 @@ def weightedIntersection(set_type, o1, o2, w1=1, w2=1):
else:
result = o1._set_type()
while i1.active and i2.active:
cmp_ = cmp(i1.key, i2.key)
cmp_ = compare(i1.key, i2.key)
if cmp_ < 0:
i1.advance()
elif cmp_ == 0:
Expand Down
9 changes: 6 additions & 3 deletions BTrees/_compat.h
Expand Up @@ -33,8 +33,9 @@
-1 -> There was an error, which the caller will detect with PyError_Occurred.
*/
#define COMPARE(lhs, rhs) \
PyObject_RichCompareBool((lhs), (rhs), Py_LT) != 0 ? -1 : \
(PyObject_RichCompareBool((lhs), (rhs), Py_EQ) > 0 ? 0 : 1)
(lhs == Py_None ? (rhs == Py_None ? 0 : -1) : (rhs == Py_None ? 1 : \
(PyObject_RichCompareBool((lhs), (rhs), Py_LT) != 0 ? -1 : \
(PyObject_RichCompareBool((lhs), (rhs), Py_EQ) > 0 ? 0 : 1))))

#else

Expand All @@ -45,7 +46,9 @@
#define TEXT_FROM_STRING PyString_FromString
#define TEXT_FORMAT PyString_Format

#define COMPARE(lhs, rhs) PyObject_Compare((lhs), (rhs))
#define COMPARE(lhs, rhs) \
(lhs == Py_None ? (rhs == Py_None ? 0 : -1) : (rhs == Py_None ? 1 : \
PyObject_Compare((lhs), (rhs))))

#endif

Expand Down
23 changes: 20 additions & 3 deletions BTrees/_compat.py
Expand Up @@ -23,7 +23,16 @@

int_types = int, long
xrange = xrange
cmp = cmp
def compare(x, y):
if x is None:
if y is None:
return 0
else:
return -1
elif y is None:
return 1
else:
return cmp(x, y)

_bytes = str
def _ascii(x):
Expand All @@ -43,8 +52,16 @@ def _u(s, encoding='unicode_escape'):
int_types = int,
xrange = range

def cmp(x, y):
return (x > y) - (y > x)
def compare(x, y):
if x is None:
if y is None:
return 0
else:
return -1
elif y is None:
return 1
else:
return (x > y) - (y > x)

_bytes = bytes
def _ascii(x):
Expand Down
10 changes: 7 additions & 3 deletions BTrees/check.py
Expand Up @@ -56,6 +56,8 @@

TYPE_UNKNOWN, TYPE_BTREE, TYPE_BUCKET = range(3)

from ._compat import compare

_type2kind = {}
for kv in ('OO',
'II', 'IO', 'OI', 'IF',
Expand Down Expand Up @@ -346,13 +348,15 @@ def visit_bucket(self, obj, path, parent, is_mapping,
def check_sorted(self, obj, path, keys, lo, hi):
i, n = 0, len(keys)
for x in keys:
if lo is not None and not lo <= x:
# lo or hi are ommitted by supplying None. Thus the not
# None checkes below.
if lo is not None and not compare(lo, x) <= 0:
s = "key %r < lower bound %r at index %d" % (x, lo, i)
self.complain(s, obj, path)
if hi is not None and not x < hi:
if hi is not None and not compare(x, hi) < 0:
s = "key %r >= upper bound %r at index %d" % (x, hi, i)
self.complain(s, obj, path)
if i < n-1 and not x < keys[i+1]:
if i < n-1 and not compare(x, keys[i+1]) < 0:
s = "key %r at index %d >= key %r at index %d" % (
x, i, keys[i+1], i+1)
self.complain(s, obj, path)
Expand Down
3 changes: 3 additions & 0 deletions BTrees/objectkeymacros.h
Expand Up @@ -15,6 +15,9 @@ check_argument_cmp(PyObject *arg)
/* ((PyTypeObject *)object_)->ob_type->tp_richcompare, */
/* arg->ob_type->tp_compare, */
/* ((PyTypeObject *)object_)->ob_type->tp_compare); */
if (arg == Py_None) {
return 1;
}

#ifdef PY3K
if (Py_TYPE(arg)->tp_richcompare == Py_TYPE(object_)->tp_richcompare)
Expand Down
26 changes: 26 additions & 0 deletions BTrees/tests/test_OOBTree.py
Expand Up @@ -161,6 +161,32 @@ class C(object):
self.assertRaises(KeyError, t.__getitem__, C())
self.assertFalse(C() in t)

def test_None_is_smallest(self):
t = self._makeOne()
for i in range(999): # Make sure we multiple buckets
t[i] = i*i
t[None] = -1
for i in range(-99,0): # Make sure we multiple buckets
t[i] = i*i
self.assertEqual(list(t), [None] + list(range(-99, 999)))
self.assertEqual(list(t.values()),
[-1] + [i*i for i in range(-99, 999)])
self.assertEqual(t[2], 4)
self.assertEqual(t[-2], 4)
self.assertEqual(t[None], -1)
t[None] = -2
self.assertEqual(t[None], -2)
t2 = t.__class__(t)
del t[None]
self.assertEqual(list(t), list(range(-99, 999)))

if 'Py' in self.__class__.__name__:
return
from BTrees.OOBTree import difference, union, intersection
self.assertEqual(list(difference(t2, t).items()), [(None, -2)])
self.assertEqual(list(union(t, t2)), list(t2))
self.assertEqual(list(intersection(t, t2)), list(t))

@_skip_under_Py3k
def testDeleteNoneKey(self):
# Check that a None key can be deleted in Python 2.
Expand Down
2 changes: 2 additions & 0 deletions CHANGES.rst
@@ -1,6 +1,8 @@
``BTrees`` Changelog
====================

- Allow None as a special key (sorted smaller than all others).

4.3.2 (2017-01-05)
------------------

Expand Down

0 comments on commit e7991d0

Please sign in to comment.