Commit e7991d01 authored by Jim Fulton's avatar Jim Fulton Committed by GitHub

Merge pull request #58 from zopefoundation/allow_None

Allow None as a special key (sorted smaller than all others).
parents eb8a9f1f f7991e61
......@@ -22,7 +22,7 @@ from persistent import Persistent
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
......@@ -113,7 +113,8 @@ class _BucketBase(_Base):
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
......@@ -256,7 +257,7 @@ _object_lt = getattr(object, '__lt__', _marker)
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)
......@@ -458,8 +459,8 @@ class Bucket(_BucketBase):
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:
......@@ -497,7 +498,7 @@ class Bucket(_BucketBase):
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
......@@ -511,7 +512,7 @@ class Bucket(_BucketBase):
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
......@@ -520,7 +521,7 @@ class Bucket(_BucketBase):
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
......@@ -531,7 +532,7 @@ class Bucket(_BucketBase):
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):
......@@ -665,8 +666,8 @@ class Set(_BucketBase):
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)
......@@ -694,7 +695,7 @@ class Set(_BucketBase):
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
......@@ -708,7 +709,7 @@ class Set(_BucketBase):
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
......@@ -717,7 +718,7 @@ class Set(_BucketBase):
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
......@@ -728,7 +729,7 @@ class Set(_BucketBase):
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
......@@ -843,7 +844,7 @@ class _Tree(_Base):
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:
......@@ -919,7 +920,7 @@ class _Tree(_Base):
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)
......@@ -1014,7 +1015,7 @@ class _Tree(_Base):
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()
......@@ -1324,7 +1325,7 @@ def difference(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()
......@@ -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()
......@@ -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:
......@@ -1425,7 +1426,7 @@ def weightedUnion(set_type, o1, o2, w1=1, w2=1):
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()
......@@ -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:
......
......@@ -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
......@@ -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
......
......@@ -23,7 +23,16 @@ if sys.version_info[0] < 3: #pragma NO COVER Python2
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):
......@@ -43,8 +52,16 @@ else: #pragma NO COVER Python3
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):
......
......@@ -56,6 +56,8 @@ from BTrees.utils import oid_repr
TYPE_UNKNOWN, TYPE_BTREE, TYPE_BUCKET = range(3)
from ._compat import compare
_type2kind = {}
for kv in ('OO',
'II', 'IO', 'OI', 'IF',
......@@ -346,13 +348,15 @@ class Checker(Walker):
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)
......
......@@ -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)
......
......@@ -161,6 +161,32 @@ class OOBTreeTest(BTreeTests, unittest.TestCase):
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.
......
``BTrees`` Changelog
====================
- Allow None as a special key (sorted smaller than all others).
4.3.2 (2017-01-05)
------------------
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment