Improve Entity class

Among a bit of cleanup, this mainly adds type-safety and a better
representation of instances.

Also, switch from nose to unittest.

Signed-off-by: martin f. krafft <madduck@madduck.net>
diff --git a/reclass/datatypes/entity.py b/reclass/datatypes/entity.py
index a643f3f..7135c71 100644
--- a/reclass/datatypes/entity.py
+++ b/reclass/datatypes/entity.py
@@ -7,45 +7,64 @@
 # Released under the terms of the Artistic Licence 2.0
 #
 from classes import Classes
-from parameters import Parameters
 from applications import Applications
+from parameters import Parameters
 
 class Entity(object):
-
+    '''
+    A collection of Classes, Parameters, and Applications, mainly as a wrapper
+    for merging. The name of an Entity will be updated to the name of the
+    Entity that is being merged.
+    '''
     def __init__(self, classes=None, applications=None, parameters=None,
                  name=None):
-        if applications is None: applications = Applications()
-        self._applications = applications
         if classes is None: classes = Classes()
-        self._classes = classes
+        self._set_classes(classes)
+        if applications is None: applications = Applications()
+        self._set_applications(applications)
         if parameters is None: parameters = Parameters()
-        self._parameters = parameters
-        self._name = name
+        self._set_parameters(parameters)
+        self._name = name or ''
 
-    applications = property(lambda self: self._applications)
-    classes = property(lambda self: self._classes)
-    parameters = property(lambda self: self._parameters)
-    name = property(lambda self: self._name)
+    name = property(lambda s: s._name)
+    classes = property(lambda s: s._classes)
+    applications = property(lambda s: s._applications)
+    parameters = property(lambda s: s._parameters)
+
+    def _set_classes(self, classes):
+        if not isinstance(classes, Classes):
+            raise TypeError('Entity.classes cannot be set to '\
+                            'instance of type %s' % type(classes))
+        self._classes = classes
+
+    def _set_applications(self, applications):
+        if not isinstance(applications, Applications):
+            raise TypeError('Entity.applications cannot be set to '\
+                            'instance of type %s' % type(applications))
+        self._applications = applications
+
+    def _set_parameters(self, parameters):
+        if not isinstance(parameters, Parameters):
+            raise TypeError('Entity.parameters cannot be set to '\
+                            'instance of type %s' % type(parameters))
+        self._parameters = parameters
 
     def merge(self, other):
-        self.classes.merge(other.classes)
-        self.applications.merge(other.applications)
-        self.parameters.merge(other.parameters)
+        self._classes.merge(other._classes)
+        self._applications.merge(other._applications)
+        self._parameters.merge(other._parameters)
         self._name = other.name
 
     def __eq__(self, other):
-        return self.applications == other.applications \
-                and self.classes == other.classes \
-                and self.parameters == other.parameters \
-                and self.name == other.name
+        return self._applications == other._applications \
+                and self._classes == other._classes \
+                and self._parameters == other._parameters \
+                and self._name == other._name
 
     def __ne__(self, other):
         return not self.__eq__(other)
 
     def __repr__(self):
-        if self.name:
-            name = " '%s'" % self.name
-        else:
-            name = ''
-        return '<Entity{0} classes:{1} applications:{2}, parameters:{3}>'.format(
-            name, len(self.classes), len(self.applications), len(self.parameters))
+        return "%s(%r, %r, %r, %r)" % (self.__class__.__name__,
+                                         self.classes, self.applications,
+                                         self.parameters, self.name)
diff --git a/reclass/datatypes/tests/test_entity.py b/reclass/datatypes/tests/test_entity.py
index 79a5274..d4f85b3 100644
--- a/reclass/datatypes/tests/test_entity.py
+++ b/reclass/datatypes/tests/test_entity.py
@@ -7,34 +7,95 @@
 # Released under the terms of the Artistic Licence 2.0
 #
 from reclass.datatypes import Entity, Classes, Parameters, Applications
+import unittest
+try:
+    import unittest.mock as mock
+except ImportError:
+    import mock
 
-class TestEntity:
+@mock.patch.multiple('reclass.datatypes', autospec=True, Classes=mock.DEFAULT,
+                     Applications=mock.DEFAULT,
+                     Parameters=mock.DEFAULT)
+class TestEntity(unittest.TestCase):
 
-    def test_constructor0(self):
+    def _make_instances(self, Classes, Applications, Parameters):
+        return Classes(), Applications(), Parameters()
+
+    def test_constructor_default(self, **mocks):
+        # Actually test the real objects by calling the default constructor,
+        # all other tests shall pass instances to the constructor
         e = Entity()
-        assert isinstance(e.classes, Classes)
-        assert len(e.classes) == 0
-        assert isinstance(e.parameters, Parameters)
-        assert len(e.parameters) == 0
-        assert isinstance(e.applications, Applications)
-        assert len(e.applications) == 0
+        self.assertEqual(e.name, '')
+        self.assertIsInstance(e.classes, Classes)
+        self.assertIsInstance(e.applications, Applications)
+        self.assertIsInstance(e.parameters, Parameters)
 
-    def test_constructor1(self):
-        c = Classes(['one', 'two'])
-        p = Parameters({'blue':'white', 'black':'yellow'})
-        a = Applications(['three', 'four'])
-        e = Entity(c, a, p)
-        assert e.classes == c
-        assert e.parameters == p
-        assert e.applications == a
+    def test_constructor_empty(self, **types):
+        instances = self._make_instances(**types)
+        e = Entity(*instances)
+        self.assertEqual(e.name, '')
+        cl, al, pl = [getattr(i, '__len__') for i in instances]
+        self.assertEqual(len(e.classes), cl.return_value)
+        cl.assert_called_once_with()
+        self.assertEqual(len(e.applications), al.return_value)
+        al.assert_called_once_with()
+        self.assertEqual(len(e.parameters), pl.return_value)
+        pl.assert_called_once_with()
 
-    def test_merge_to_empty(self):
-        e1 = Entity()
-        c = Classes(['one', 'two'])
-        p = Parameters({'blue':'white', 'black':'yellow'})
-        a = Applications(['three', 'four'])
-        e2 = Entity(c, a, p)
+    def test_constructor_empty_named(self, **types):
+        name = 'empty'
+        e = Entity(*self._make_instances(**types), name=name)
+        self.assertEqual(e.name, name)
+
+    def test_equal_empty(self, **types):
+        instances = self._make_instances(**types)
+        self.assertEqual(Entity(*instances), Entity(*instances))
+        for i in instances:
+            i.__eq__.assert_called_once_with(i)
+
+    def test_equal_empty_named(self, **types):
+        instances = self._make_instances(**types)
+        self.assertEqual(Entity(*instances), Entity(*instances))
+        name = 'empty'
+        self.assertEqual(Entity(*instances, name=name),
+                         Entity(*instances, name=name))
+
+    def test_unequal_empty_named(self, **types):
+        instances = self._make_instances(**types)
+        name = 'empty'
+        self.assertNotEqual(Entity(*instances, name='empty'),
+                            Entity(*instances, name='ytpme'))
+        for i in instances:
+            i.__eq__.assert_called_once_with(i)
+
+    def _test_constructor_wrong_types(self, which_replace, **types):
+        instances = self._make_instances(**types)
+        instances[which_replace] = 'Invalid type'
+        e = Entity(*instances)
+
+    def test_constructor_wrong_type_classes(self, **types):
+        self.assertRaises(TypeError, self._test_constructor_wrong_types, 0)
+
+    def test_constructor_wrong_type_applications(self, **types):
+        self.assertRaises(TypeError, self._test_constructor_wrong_types, 1)
+
+    def test_constructor_wrong_type_parameters(self, **types):
+        self.assertRaises(TypeError, self._test_constructor_wrong_types, 2)
+
+    def test_merge(self, **types):
+        instances = self._make_instances(**types)
+        e = Entity(*instances)
+        e.merge(e)
+        for i, fn in zip(instances, ('merge_unique', 'merge_unique', 'merge')):
+            getattr(i, fn).assert_called_once_with(i)
+
+    def test_merge_newname(self, **types):
+        instances = self._make_instances(**types)
+        newname = 'newname'
+        e1 = Entity(*instances, name='oldname')
+        e2 = Entity(*instances, name=newname)
         e1.merge(e2)
-        assert e1.classes == e2.classes
-        assert e1.applications == e2.applications
-        assert e1.parameters == e2.parameters
+        self.assertEqual(e1.name, newname)
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/reclass/storage/__init__.py b/reclass/storage/__init__.py
index 76c2a67..ac83c84 100644
--- a/reclass/storage/__init__.py
+++ b/reclass/storage/__init__.py
@@ -29,9 +29,9 @@
         return {'__reclass__' : {'node': node, 'node_uri': uri,
                                  'timestamp': _get_timestamp()
                                 },
-                'classes': list(entity.classes),
-                'applications': list(entity.applications),
-                'parameters': dict(entity.parameters)
+                'classes': entity.classes,
+                'applications': entity.applications,
+                'parameters': entity.parameters
                }
 
     def _list_inventory(self):