Extract container converters
authorMichael Hanselmann <hansmi@google.com>
Mon, 4 Feb 2013 13:38:34 +0000 (14:38 +0100)
committerMichael Hanselmann <hansmi@google.com>
Mon, 4 Feb 2013 14:52:12 +0000 (15:52 +0100)
“objects.ConfigObject” contains two useful functions for working with
containers of serialized objects, “_ContainerToDicts” and
“_ContainerFromDicts”. This patch separates those functions and moves
them into “objectutils” as they'll be useful for converting parts of the
QA configuration to objects. Unittests are added to cover the parts not
already tested through other code paths.

Signed-off-by: Michael Hanselmann <hansmi@google.com>
Reviewed-by: Iustin Pop <iustin@google.com>

lib/objects.py
lib/objectutils.py
test/py/ganeti.objectutils_unittest.py

index 915e3b8..fc47c84 100644 (file)
@@ -263,47 +263,6 @@ class ConfigObject(objectutils.ValidatedSlots):
     obj = cls(**val_str) # pylint: disable=W0142
     return obj
 
-  @staticmethod
-  def _ContainerToDicts(container):
-    """Convert the elements of a container to standard python types.
-
-    This method converts a container with elements derived from
-    ConfigData to standard python types. If the container is a dict,
-    we don't touch the keys, only the values.
-
-    """
-    if isinstance(container, dict):
-      ret = dict([(k, v.ToDict()) for k, v in container.iteritems()])
-    elif isinstance(container, (list, tuple, set, frozenset)):
-      ret = [elem.ToDict() for elem in container]
-    else:
-      raise TypeError("Invalid type %s passed to _ContainerToDicts" %
-                      type(container))
-    return ret
-
-  @staticmethod
-  def _ContainerFromDicts(source, c_type, e_type):
-    """Convert a container from standard python types.
-
-    This method converts a container with standard python types to
-    ConfigData objects. If the container is a dict, we don't touch the
-    keys, only the values.
-
-    """
-    if not isinstance(c_type, type):
-      raise TypeError("Container type %s passed to _ContainerFromDicts is"
-                      " not a type" % type(c_type))
-    if source is None:
-      source = c_type()
-    if c_type is dict:
-      ret = dict([(k, e_type.FromDict(v)) for k, v in source.iteritems()])
-    elif c_type in (list, tuple, set, frozenset):
-      ret = c_type([e_type.FromDict(elem) for elem in source])
-    else:
-      raise TypeError("Invalid container type %s passed to"
-                      " _ContainerFromDicts" % c_type)
-    return ret
-
   def Copy(self):
     """Makes a deep copy of the current object and its children.
 
@@ -446,7 +405,7 @@ class ConfigData(ConfigObject):
     mydict = super(ConfigData, self).ToDict()
     mydict["cluster"] = mydict["cluster"].ToDict()
     for key in "nodes", "instances", "nodegroups", "networks":
-      mydict[key] = self._ContainerToDicts(mydict[key])
+      mydict[key] = objectutils.ContainerToDicts(mydict[key])
 
     return mydict
 
@@ -457,10 +416,12 @@ class ConfigData(ConfigObject):
     """
     obj = super(ConfigData, cls).FromDict(val)
     obj.cluster = Cluster.FromDict(obj.cluster)
-    obj.nodes = cls._ContainerFromDicts(obj.nodes, dict, Node)
-    obj.instances = cls._ContainerFromDicts(obj.instances, dict, Instance)
-    obj.nodegroups = cls._ContainerFromDicts(obj.nodegroups, dict, NodeGroup)
-    obj.networks = cls._ContainerFromDicts(obj.networks, dict, Network)
+    obj.nodes = objectutils.ContainerFromDicts(obj.nodes, dict, Node)
+    obj.instances = \
+      objectutils.ContainerFromDicts(obj.instances, dict, Instance)
+    obj.nodegroups = \
+      objectutils.ContainerFromDicts(obj.nodegroups, dict, NodeGroup)
+    obj.networks = objectutils.ContainerFromDicts(obj.networks, dict, Network)
     return obj
 
   def HasAnyDiskOfType(self, dev_type):
@@ -768,7 +729,7 @@ class Disk(ConfigObject):
     for attr in ("children",):
       alist = bo.get(attr, None)
       if alist:
-        bo[attr] = self._ContainerToDicts(alist)
+        bo[attr] = objectutils.ContainerToDicts(alist)
     return bo
 
   @classmethod
@@ -778,7 +739,7 @@ class Disk(ConfigObject):
     """
     obj = super(Disk, cls).FromDict(val)
     if obj.children:
-      obj.children = cls._ContainerFromDicts(obj.children, list, Disk)
+      obj.children = objectutils.ContainerFromDicts(obj.children, list, Disk)
     if obj.logical_id and isinstance(obj.logical_id, list):
       obj.logical_id = tuple(obj.logical_id)
     if obj.physical_id and isinstance(obj.physical_id, list):
@@ -1136,7 +1097,7 @@ class Instance(TaggableObject):
     for attr in "nics", "disks":
       alist = bo.get(attr, None)
       if alist:
-        nlist = self._ContainerToDicts(alist)
+        nlist = objectutils.ContainerToDicts(alist)
       else:
         nlist = []
       bo[attr] = nlist
@@ -1155,8 +1116,8 @@ class Instance(TaggableObject):
     if "admin_up" in val:
       del val["admin_up"]
     obj = super(Instance, cls).FromDict(val)
-    obj.nics = cls._ContainerFromDicts(obj.nics, list, NIC)
-    obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
+    obj.nics = objectutils.ContainerFromDicts(obj.nics, list, NIC)
+    obj.disks = objectutils.ContainerFromDicts(obj.disks, list, Disk)
     return obj
 
   def UpgradeConfig(self):
@@ -1344,12 +1305,12 @@ class Node(TaggableObject):
 
     hv_state = data.get("hv_state", None)
     if hv_state is not None:
-      data["hv_state"] = self._ContainerToDicts(hv_state)
+      data["hv_state"] = objectutils.ContainerToDicts(hv_state)
 
     disk_state = data.get("disk_state", None)
     if disk_state is not None:
       data["disk_state"] = \
-        dict((key, self._ContainerToDicts(value))
+        dict((key, objectutils.ContainerToDicts(value))
              for (key, value) in disk_state.items())
 
     return data
@@ -1362,11 +1323,12 @@ class Node(TaggableObject):
     obj = super(Node, cls).FromDict(val)
 
     if obj.hv_state is not None:
-      obj.hv_state = cls._ContainerFromDicts(obj.hv_state, dict, NodeHvState)
+      obj.hv_state = \
+        objectutils.ContainerFromDicts(obj.hv_state, dict, NodeHvState)
 
     if obj.disk_state is not None:
       obj.disk_state = \
-        dict((key, cls._ContainerFromDicts(value, dict, NodeDiskState))
+        dict((key, objectutils.ContainerFromDicts(value, dict, NodeDiskState))
              for (key, value) in obj.disk_state.items())
 
     return obj
@@ -1935,7 +1897,7 @@ class _QueryResponseBase(ConfigObject):
 
     """
     mydict = super(_QueryResponseBase, self).ToDict()
-    mydict["fields"] = self._ContainerToDicts(mydict["fields"])
+    mydict["fields"] = objectutils.ContainerToDicts(mydict["fields"])
     return mydict
 
   @classmethod
@@ -1944,7 +1906,8 @@ class _QueryResponseBase(ConfigObject):
 
     """
     obj = super(_QueryResponseBase, cls).FromDict(val)
-    obj.fields = cls._ContainerFromDicts(obj.fields, list, QueryFieldDefinition)
+    obj.fields = \
+      objectutils.ContainerFromDicts(obj.fields, list, QueryFieldDefinition)
     return obj
 
 
index e2742b1..f0f6558 100644 (file)
 """Module for object related utils."""
 
 
+#: Supported container types for serialization/de-serialization (must be a
+#: tuple as it's used as a parameter for C{isinstance})
+_SEQUENCE_TYPES = (list, tuple, set, frozenset)
+
+
 class AutoSlots(type):
   """Meta base class for __slots__ definitions.
 
@@ -91,3 +96,55 @@ class ValidatedSlots(object):
 
     """
     raise NotImplementedError
+
+
+def ContainerToDicts(container):
+  """Convert the elements of a container to standard Python types.
+
+  This method converts a container with elements to standard Python types. If
+  the input container is of the type C{dict}, only its values are touched.
+  Those values, as well as all elements of input sequences, must support a
+  C{ToDict} method returning a serialized version.
+
+  @type container: dict or sequence (see L{_SEQUENCE_TYPES})
+
+  """
+  if isinstance(container, dict):
+    ret = dict([(k, v.ToDict()) for k, v in container.items()])
+  elif isinstance(container, _SEQUENCE_TYPES):
+    ret = [elem.ToDict() for elem in container]
+  else:
+    raise TypeError("Unknown container type '%s'" % type(container))
+
+  return ret
+
+
+def ContainerFromDicts(source, c_type, e_type):
+  """Convert a container from standard python types.
+
+  This method converts a container with standard Python types to objects. If
+  the container is a dict, we don't touch the keys, only the values.
+
+  @type source: None, dict or sequence (see L{_SEQUENCE_TYPES})
+  @param source: Input data
+  @type c_type: type class
+  @param c_type: Desired type for returned container
+  @type e_type: element type class
+  @param e_type: Item type for elements in returned container (must have a
+    C{FromDict} class method)
+
+  """
+  if not isinstance(c_type, type):
+    raise TypeError("Container type '%s' is not a type" % type(c_type))
+
+  if source is None:
+    source = c_type()
+
+  if c_type is dict:
+    ret = dict([(k, e_type.FromDict(v)) for k, v in source.items()])
+  elif c_type in _SEQUENCE_TYPES:
+    ret = c_type(map(e_type.FromDict, source))
+  else:
+    raise TypeError("Unknown container type '%s'" % c_type)
+
+  return ret
index aa3a205..aec95f7 100755 (executable)
@@ -46,5 +46,62 @@ class TestAutoSlot(unittest.TestCase):
     slotted = AutoSlotted()
     self.assertEqual(slotted.__slots__, AutoSlotted.SLOTS)
 
+
+class TestContainerToDicts(unittest.TestCase):
+  def testUnknownType(self):
+    for value in [None, 19410, "xyz"]:
+      try:
+        objectutils.ContainerToDicts(value)
+      except TypeError, err:
+        self.assertTrue(str(err).startswith("Unknown container type"))
+      else:
+        self.fail("Exception was not raised")
+
+  def testEmptyDict(self):
+    value = {}
+    self.assertFalse(type(value) in objectutils._SEQUENCE_TYPES)
+    self.assertEqual(objectutils.ContainerToDicts(value), {})
+
+  def testEmptySequences(self):
+    for cls in [list, tuple, set, frozenset]:
+      self.assertEqual(objectutils.ContainerToDicts(cls()), [])
+
+
+class _FakeWithFromDict:
+  def FromDict(self, _):
+    raise NotImplemented
+
+
+class TestContainerFromDicts(unittest.TestCase):
+  def testUnknownType(self):
+    for cls in [str, int, bool]:
+      try:
+        objectutils.ContainerFromDicts(None, cls, NotImplemented)
+      except TypeError, err:
+        self.assertTrue(str(err).startswith("Unknown container type"))
+      else:
+        self.fail("Exception was not raised")
+
+      try:
+        objectutils.ContainerFromDicts(None, cls(), NotImplemented)
+      except TypeError, err:
+        self.assertTrue(str(err).endswith("is not a type"))
+      else:
+        self.fail("Exception was not raised")
+
+  def testEmptyDict(self):
+    value = {}
+    self.assertFalse(type(value) in objectutils._SEQUENCE_TYPES)
+    self.assertEqual(objectutils.ContainerFromDicts(value, dict,
+                                                    NotImplemented),
+                     {})
+
+  def testEmptySequences(self):
+    for cls in [list, tuple, set, frozenset]:
+      self.assertEqual(objectutils.ContainerFromDicts([], cls,
+                                                      _FakeWithFromDict),
+                       cls())
+
+
 if __name__ == "__main__":
   testutils.GanetiTestProgram()