Reimplement Classes as simplified (not-fully-functional) ordered set
Classes is essentially an ordered set, but we only need very limited
functionality, so keep it simple and provide only the very basics.
Purposely, Classes no longer derives from list so as to not give a false
impression.
Also, switch from nose to unittest.
Signed-off-by: martin f. krafft <madduck@madduck.net>
diff --git a/reclass/datatypes/classes.py b/reclass/datatypes/classes.py
index e750339..dcb6852 100644
--- a/reclass/datatypes/classes.py
+++ b/reclass/datatypes/classes.py
@@ -6,16 +6,59 @@
# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
# Released under the terms of the Artistic Licence 2.0
#
-from reclass.mergers.list import SetExtend
-class Classes(list):
+import types
- def __init__(self, *args, **kwargs):
- super(Classes, self).__init__(*args, **kwargs)
+class Classes(object):
+ '''
+ A very limited ordered set of strings with O(n) uniqueness constraints. It
+ is neither a proper list or a proper set, on purpose, to keep things
+ simple.
+ '''
+ def __init__(self, iterable=None):
+ self._items = []
+ if iterable is not None:
+ self.merge_unique(iterable)
- def merge(self, other):
- merger = SetExtend()
- self[:] = merger.merge(self, other)
+ def __len__(self):
+ return len(self._items)
+
+ def __eq__(self, rhs):
+ if isinstance(rhs, list):
+ return self._items == rhs
+ else:
+ try:
+ return self._items == rhs._items
+ except AttributeError as e:
+ return False
+
+ def __ne__(self, rhs):
+ return not self.__eq__(rhs)
+
+ def as_list(self):
+ return self._items[:]
+
+ def merge_unique(self, iterable):
+ if isinstance(iterable, self.__class__):
+ iterable = iterable.as_list()
+ # Cannot just call list.extend here, as iterable's items might not
+ # be unique by themselves, or in the context of self.
+ for i in iterable:
+ self.append_if_new(i)
+
+ def _assert_is_string(self, item):
+ if not isinstance(item, types.StringTypes):
+ raise TypeError('%s instances can only contain strings, '\
+ 'not %s' % (self.__class__.__name__, type(item)))
+
+ def _append_if_new(self, item):
+ if item not in self._items:
+ self._items.append(item)
+
+ def append_if_new(self, item):
+ self._assert_is_string(item)
+ self._append_if_new(item)
def __repr__(self):
- return '<Classes {0}>'.format(super(Classes, self).__repr__())
+ return '%s(%r)' % (self.__class__.__name__,
+ self._items)
diff --git a/reclass/datatypes/entity.py b/reclass/datatypes/entity.py
index 7135c71..2209460 100644
--- a/reclass/datatypes/entity.py
+++ b/reclass/datatypes/entity.py
@@ -50,7 +50,7 @@
self._parameters = parameters
def merge(self, other):
- self._classes.merge(other._classes)
+ self._classes.merge_unique(other._classes)
self._applications.merge(other._applications)
self._parameters.merge(other._parameters)
self._name = other.name
diff --git a/reclass/datatypes/tests/test_classes.py b/reclass/datatypes/tests/test_classes.py
index b095839..7f55185 100644
--- a/reclass/datatypes/tests/test_classes.py
+++ b/reclass/datatypes/tests/test_classes.py
@@ -7,26 +7,107 @@
# Released under the terms of the Artistic Licence 2.0
#
from reclass.datatypes import Classes
+import unittest
+try:
+ import unittest.mock as mock
+except ImportError:
+ import mock
-class TestClasses:
+TESTLIST1 = ['one', 'two', 'three']
+TESTLIST2 = ['red', 'green', 'blue']
- def test_constructor0(self):
+#TODO: mock out the underlying list
+
+class TestClasses(unittest.TestCase):
+
+ def test_len_empty(self):
+ with mock.patch.object(Classes, 'merge_unique') as m:
+ c = Classes()
+ self.assertEqual(len(c), 0)
+ self.assertFalse(m.called)
+
+ def test_constructor(self):
+ with mock.patch.object(Classes, 'merge_unique') as m:
+ c = Classes(TESTLIST1)
+ m.assert_called_once_with(TESTLIST1)
+
+ def test_equality_list_empty(self):
+ self.assertEqual(Classes(), [])
+
+ def test_equality_list(self):
+ self.assertEqual(Classes(TESTLIST1), TESTLIST1)
+
+ def test_equality_instance_empty(self):
+ self.assertEqual(Classes(), Classes())
+
+ def test_equality_instance(self):
+ self.assertEqual(Classes(TESTLIST1), Classes(TESTLIST1))
+
+ def test_inequality(self):
+ self.assertNotEqual(Classes(TESTLIST1), Classes(TESTLIST2))
+
+ def test_construct_duplicates(self):
+ c = Classes(TESTLIST1 + TESTLIST1)
+ self.assertSequenceEqual(c, TESTLIST1)
+
+ def test_append_if_new(self):
c = Classes()
- assert len(c) == 0
+ c.append_if_new(TESTLIST1[0])
+ self.assertEqual(len(c), 1)
+ self.assertSequenceEqual(c, TESTLIST1[:1])
- def test_constructor1(self):
- DATA = ['one', 'two', 'three', 'four']
- c = Classes(DATA)
- assert len(c) == len(DATA)
- for i in range(0, len(c)):
- assert DATA[i] == c[i]
+ def test_append_if_new_duplicate(self):
+ c = Classes(TESTLIST1)
+ c.append_if_new(TESTLIST1[0])
+ self.assertEqual(len(c), len(TESTLIST1))
+ self.assertSequenceEqual(c, TESTLIST1)
- def test_merge(self):
- DATA0 = ['one', 'two', 'three', 'four']
- DATA1 = ['one', 'three', 'five', 'seven']
- c = Classes(DATA0)
- c.merge(DATA1)
- assert len(c) == 6
- assert c[:4] == DATA0
- assert c[4] == DATA1[2]
- assert c[5] == DATA1[3]
+ def test_append_if_new_nonstring(self):
+ c = Classes()
+ with self.assertRaises(TypeError):
+ c.append_if_new(0)
+
+ def test_merge_unique(self):
+ c = Classes(TESTLIST1)
+ c.merge_unique(TESTLIST2)
+ self.assertSequenceEqual(c, TESTLIST1 + TESTLIST2)
+
+ def test_merge_unique_duplicate1_list(self):
+ c = Classes(TESTLIST1)
+ c.merge_unique(TESTLIST1)
+ self.assertSequenceEqual(c, TESTLIST1)
+
+ def test_merge_unique_duplicate1_instance(self):
+ c = Classes(TESTLIST1)
+ c.merge_unique(Classes(TESTLIST1))
+ self.assertSequenceEqual(c, TESTLIST1)
+
+ def test_merge_unique_duplicate2_list(self):
+ c = Classes(TESTLIST1)
+ c.merge_unique(TESTLIST2 + TESTLIST2)
+ self.assertSequenceEqual(c, TESTLIST1 + TESTLIST2)
+
+ def test_merge_unique_duplicate2_instance(self):
+ c = Classes(TESTLIST1)
+ c.merge_unique(Classes(TESTLIST2 + TESTLIST2))
+ self.assertSequenceEqual(c, TESTLIST1 + TESTLIST2)
+
+ def test_merge_unique_nonstring(self):
+ c = Classes()
+ with self.assertRaises(TypeError):
+ c.merge_unique([0,1,2])
+
+ def test_repr_empty(self):
+ c = Classes()
+ self.assertEqual('%r' % c, '%s(%r)' % (c.__class__.__name__, []))
+
+ def test_repr_contents(self):
+ c = Classes(TESTLIST1)
+ self.assertEqual('%r' % c, '%s(%r)' % (c.__class__.__name__, TESTLIST1))
+
+ def test_as_list(self):
+ c = Classes(TESTLIST1)
+ self.assertListEqual(c.as_list(), TESTLIST1)
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/reclass/storage/__init__.py b/reclass/storage/__init__.py
index ac83c84..edafcae 100644
--- a/reclass/storage/__init__.py
+++ b/reclass/storage/__init__.py
@@ -29,7 +29,7 @@
return {'__reclass__' : {'node': node, 'node_uri': uri,
'timestamp': _get_timestamp()
},
- 'classes': entity.classes,
+ 'classes': entity.classes.as_list(),
'applications': entity.applications,
'parameters': entity.parameters
}
diff --git a/reclass/storage/yaml_fs/__init__.py b/reclass/storage/yaml_fs/__init__.py
index e40c8a0..42eb469 100644
--- a/reclass/storage/yaml_fs/__init__.py
+++ b/reclass/storage/yaml_fs/__init__.py
@@ -27,7 +27,7 @@
seen[name] = True
merge_base = Entity()
- for klass in entity.classes:
+ for klass in entity.classes.as_list():
if klass not in seen:
ret = self._read_nodeinfo(klass, self.classes_uri, seen,
name if nodename is None else nodename)[0]