Reimplement Applications as a subclass of Classes
Applications are like Classes an ordered set, so we inherit from Classes
and thus reuse the same, limited functionality.
The concept of negation requires to override extend() and append().
Since we would like Applications to keep a memory of negations they
embody, for instance for later merging (extending) into another list,
thereby removing elements, the negations are kept in a separate list.
Also, switch from nose to unittest.
Signed-off-by: martin f. krafft <madduck@madduck.net>
diff --git a/reclass/datatypes/applications.py b/reclass/datatypes/applications.py
index 3074e6b..10edda0 100644
--- a/reclass/datatypes/applications.py
+++ b/reclass/datatypes/applications.py
@@ -6,35 +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 ApplicationsMerger(SetExtend):
+from classes import Classes
- def __init__(self, negater='~'):
- self._negater=negater
+class Applications(Classes):
+ '''
+ Extends Classes with the possibility to let specially formatted items
+ remove earlier occurences of the item. For instance, if the "negater" is
+ '~', then "adding" an element "~foo" to a list causes a previous element
+ "foo" to be removed. If no such element exists, nothing happens, but
+ a reference of the negation is kept, in case the instance is later used to
+ extend another instance, in which case the negations should apply to the
+ instance to be extended.
+ '''
+ DEFAULT_NEGATION_PREFIX = '~'
- def _combine(self, first, second):
- for i in second:
- remove = False
- if i.startswith(self._negater):
- i = i[len(self._negater):]
- remove = True
- if i not in first:
- if not remove:
- first.append(i)
- elif remove:
- first.remove(i)
- return first
+ def __init__(self, iterable=None,
+ negation_prefix=DEFAULT_NEGATION_PREFIX):
+ self._negation_prefix = negation_prefix
+ self._offset = len(negation_prefix)
+ self._negations = []
+ super(Applications, self).__init__(iterable)
+ def _get_negation_prefix(self):
+ return self._negation_prefix
+ negation_prefix = property(_get_negation_prefix)
-class Applications(list):
+ def append_if_new(self, item):
+ self._assert_is_string(item)
+ if item.startswith(self._negation_prefix):
+ item = item[self._offset:]
+ self._negations.append(item)
+ try:
+ self._items.remove(item)
+ except ValueError:
+ pass
+ else:
+ super(Applications, self)._append_if_new(item)
- def __init__(self, *args, **kwargs):
- super(Applications, self).__init__(*args, **kwargs)
-
- def merge(self, other, negater='~'):
- merger = ApplicationsMerger(negater)
- self[:] = merger.merge(self, other)
+ def merge_unique(self, iterable):
+ if isinstance(iterable, self.__class__):
+ # we might be extending ourselves to include negated applications,
+ # in which case we need to remove our own content accordingly:
+ for negation in iterable._negations:
+ try:
+ self._items.remove(negation)
+ except ValueError:
+ pass
+ iterable = iterable.as_list()
+ for i in iterable:
+ self.append_if_new(i)
def __repr__(self):
- return '<Applications {0}>'.format(super(Applications, self).__repr__())
+ contents = self._items + \
+ ['%s%s' % (self._negation_prefix, i) for i in self._negations]
+ return "%s(%r, %r)" % (self.__class__.__name__, contents,
+ self._negation_prefix)
diff --git a/reclass/datatypes/entity.py b/reclass/datatypes/entity.py
index 2209460..f2ed10d 100644
--- a/reclass/datatypes/entity.py
+++ b/reclass/datatypes/entity.py
@@ -51,7 +51,7 @@
def merge(self, other):
self._classes.merge_unique(other._classes)
- self._applications.merge(other._applications)
+ self._applications.merge_unique(other._applications)
self._parameters.merge(other._parameters)
self._name = other.name
diff --git a/reclass/datatypes/tests/test_applications.py b/reclass/datatypes/tests/test_applications.py
index f7a7254..c1d890c 100644
--- a/reclass/datatypes/tests/test_applications.py
+++ b/reclass/datatypes/tests/test_applications.py
@@ -6,52 +6,65 @@
# Copyright © 2007–13 martin f. krafft <madduck@madduck.net>
# Released under the terms of the Artistic Licence 2.0
#
-from reclass.datatypes import Applications
+from reclass.datatypes import Applications, Classes
+import unittest
+try:
+ import unittest.mock as mock
+except ImportError:
+ import mock
-class TestApplications:
+TESTLIST1 = ['one', 'two', 'three']
+TESTLIST2 = ['red', 'green', '~two', '~three']
+GOALLIST = ['one', 'red', 'green']
- def test_constructor0(self):
- c = Applications()
- assert len(c) == 0
+#TODO: mock out the underlying list
- def test_constructor1(self):
- DATA = ['one', 'two', 'three', 'four']
- c = Applications(DATA)
- assert len(c) == len(DATA)
- for i in range(0, len(c)):
- assert DATA[i] == c[i]
+class TestApplications(unittest.TestCase):
- def test_merge(self):
- DATA0 = ['one', 'two', 'three', 'four']
- DATA1 = ['one', 'three', 'five', 'seven']
- c = Applications(DATA0)
- c.merge(DATA1)
- assert len(c) == 6
- assert c[:4] == DATA0
- assert c[4] == DATA1[2]
- assert c[5] == DATA1[3]
+ def test_inheritance(self):
+ a = Applications()
+ self.assertIsInstance(a, Classes)
- def test_merge_negate(self):
+ def test_constructor_negate(self):
+ a = Applications(TESTLIST1 + TESTLIST2)
+ self.assertSequenceEqual(a, GOALLIST)
+
+ def test_merge_unique_negate_list(self):
+ a = Applications(TESTLIST1)
+ a.merge_unique(TESTLIST2)
+ self.assertSequenceEqual(a, GOALLIST)
+
+ def test_merge_unique_negate_instance(self):
+ a = Applications(TESTLIST1)
+ a.merge_unique(Applications(TESTLIST2))
+ self.assertSequenceEqual(a, GOALLIST)
+
+ def test_append_if_new_negate(self):
+ a = Applications(TESTLIST1)
+ a.append_if_new(TESTLIST2[2])
+ self.assertSequenceEqual(a, TESTLIST1[::2])
+
+ def test_repr_empty(self):
+ negater = '%%'
+ a = Applications(negation_prefix=negater)
+ self.assertEqual('%r' % a, "%s(%r, '%s')" % (a.__class__.__name__, [], negater))
+
+ def test_repr_contents(self):
+ negater = '%%'
+ a = Applications(TESTLIST1, negation_prefix=negater)
+ self.assertEqual('%r' % a, "%s(%r, '%s')" % (a.__class__.__name__, TESTLIST1, negater))
+
+ def test_repr_negations(self):
negater = '~'
- DATA0 = ['red', 'green', 'blue']
- DATA1 = [negater + 'red', 'yellow', 'black']
- c = Applications(DATA0)
- c.merge(DATA1, negater)
- assert len(c) == len(DATA0)-1 + len(DATA1)-1
- assert c[0] == DATA0[1]
- assert c[1] == DATA0[2]
- assert c[2] == DATA1[1]
- assert c[3] == DATA1[2]
+ a = Applications(TESTLIST2, negation_prefix=negater)
+ self.assertEqual('%r' % a, "%s(%r, '%s')" % (a.__class__.__name__, TESTLIST2, negater))
- def test_merge_negate_default(self):
- c = Applications(['red'])
- c.merge(['~red'])
- assert len(c) == 0
+ def test_repr_negations_interspersed(self):
+ l = ['a', '~b', 'a', '~d']
+ a = Applications(l)
+ is_negation = lambda x: x.startswith(a.negation_prefix)
+ GOAL = filter(lambda x: not is_negation(x), set(l)) + filter(is_negation, l)
+ self.assertEqual('%r' % a, "%s(%r, '~')" % (a.__class__.__name__, GOAL))
- def test_merge_negate_nonexistent(self):
- c = Applications(['blue'])
- c.merge(['~red'])
- assert len(c) == 1
- assert 'red' not in c
- assert '~red' not in c
- assert 'blue' in c
+if __name__ == '__main__':
+ unittest.main()
diff --git a/reclass/storage/__init__.py b/reclass/storage/__init__.py
index edafcae..7cfb60c 100644
--- a/reclass/storage/__init__.py
+++ b/reclass/storage/__init__.py
@@ -30,7 +30,7 @@
'timestamp': _get_timestamp()
},
'classes': entity.classes.as_list(),
- 'applications': entity.applications,
+ 'applications': entity.applications.as_list(),
'parameters': entity.parameters
}