New ways to instantate cstruct objects.

"default" constructor zero-initializes the whole struct. (It's a common
pattern that should be easier to read after this.

keyword constructor so that instantiating a struct with many fields is
easier to read.

Change-Id: Ide18aa41c7289d63df7d0297673518fcafdf0174
Test: coming soon
diff --git a/net/test/cstruct.py b/net/test/cstruct.py
index 43c47a2..d31979e 100644
--- a/net/test/cstruct.py
+++ b/net/test/cstruct.py
@@ -25,7 +25,8 @@
 >>> NLMsgHdr = cstruct.Struct("NLMsgHdr", "=LHHLL", "length type flags seq pid")
 >>>
 >>>
->>> # Create instances from tuples or raw bytes. Data past the end is ignored.
+>>> # Create instances from a tuple of values, raw bytes, zero-initialized, or
+>>> # using keywords.
 ... n1 = NLMsgHdr((44, 32, 0x2, 0, 491))
 >>> print n1
 NLMsgHdr(length=44, type=32, flags=2, seq=0, pid=491)
@@ -35,6 +36,14 @@
 >>> print n2
 NLMsgHdr(length=44, type=33, flags=2, seq=0, pid=510)
 >>>
+>>> n3 = netlink.NLMsgHdr() # Zero-initialized
+>>> print n3
+NLMsgHdr(length=0, type=0, flags=0, seq=0, pid=0)
+>>>
+>>> n4 = netlink.NLMsgHdr(length=44, type=33) # Other fields zero-initialized
+>>> print n4
+NLMsgHdr(length=44, type=33, flags=0, seq=0, pid=0)
+>>>
 >>> # Serialize to raw bytes.
 ... print n1.Pack().encode("hex")
 2c0000002000020000000000eb010000
@@ -123,12 +132,14 @@
         _asciiz.add(index)
         _format += "s"
       else:
-         # Standard struct format character.
+        # Standard struct format character.
         _format += fmt[i]
 
     _length = CalcSize(_format)
 
     def _SetValues(self, values):
+      # Replace self._values with the given list. We can't do direct assignment
+      # because of the __setattr__ overload on this class.
       super(CStruct, self).__setattr__("_values", list(values))
 
     def _Parse(self, data):
@@ -139,19 +150,37 @@
           values[index] = self._nested[index](value)
       self._SetValues(values)
 
-    def __init__(self, values):
-      # Initializing from a string.
-      if isinstance(values, str):
-        if len(values) < self._length:
+    def __init__(self, tuple_or_bytes=None, **kwargs):
+      """Construct an instance of this Struct.
+
+      1. With no args, the whole struct is zero-initialized.
+      2. With keyword args, the matching fields are populated; rest are zeroed.
+      3. With one tuple as the arg, the fields are assigned based on position.
+      4. With one string arg, the Struct is parsed from bytes.
+      """
+      if tuple_or_bytes and kwargs:
+        raise TypeError(
+            "%s: cannot specify both a tuple and keyword args" % self._name)
+
+      if tuple_or_bytes is None:
+        # Default construct from null bytes.
+        self._Parse("\x00" * len(self))
+        # If any keywords were supplied, set those fields.
+        for k, v in kwargs.iteritems():
+          setattr(self, k, v)
+      elif isinstance(tuple_or_bytes, str):
+        # Initializing from a string.
+        if len(tuple_or_bytes) < self._length:
           raise TypeError("%s requires string of length %d, got %d" %
-                          (self._name, self._length, len(values)))
-        self._Parse(values)
+                          (self._name, self._length, len(tuple_or_bytes)))
+        self._Parse(tuple_or_bytes)
       else:
         # Initializing from a tuple.
-        if len(values) != len(self._fieldnames):
+        if len(tuple_or_bytes) != len(self._fieldnames):
           raise TypeError("%s has exactly %d fieldnames (%d given)" %
-                          (self._name, len(self._fieldnames), len(values)))
-        self._SetValues(values)
+                          (self._name, len(self._fieldnames),
+                           len(tuple_or_bytes)))
+        self._SetValues(tuple_or_bytes)
 
     def _FieldIndex(self, attr):
       try:
@@ -164,6 +193,8 @@
       return self._values[self._FieldIndex(name)]
 
     def __setattr__(self, name, value):
+      # TODO: check value type against self._format and throw here, or else
+      # callers get an unhelpful exception when they call Pack().
       self._values[self._FieldIndex(name)] = value
 
     @classmethod
diff --git a/net/test/cstruct_test.py b/net/test/cstruct_test.py
index fdcbd55..b69aeb7 100755
--- a/net/test/cstruct_test.py
+++ b/net/test/cstruct_test.py
@@ -110,6 +110,33 @@
                 " int3=12345, ascii4=hello\x00visible123, word5=33210)")
     self.assertEquals(expected, str(t))
 
+  def testZeroInitialization(self):
+    TestStruct = cstruct.Struct("TestStruct", "B16si16AH",
+                                "byte1 string2 int3 ascii4 word5")
+    t = TestStruct()
+    self.assertEquals(0, t.byte1)
+    self.assertEquals("\x00" * 16, t.string2)
+    self.assertEquals(0, t.int3)
+    self.assertEquals("\x00" * 16, t.ascii4)
+    self.assertEquals(0, t.word5)
+    self.assertEquals("\x00" * len(TestStruct), t.Pack())
+
+  def testKeywordInitialization(self):
+    TestStruct = cstruct.Struct("TestStruct", "=B16sIH",
+                                "byte1 string2 int3 word4")
+    text = "hello world! ^_^"
+    text_bytes = text.encode("hex")
+
+    # Populate all fields
+    t1 = TestStruct(byte1=1, string2=text, int3=0xFEDCBA98, word4=0x1234)
+    expected = ("01" + text_bytes + "98BADCFE" "3412").decode("hex")
+    self.assertEquals(expected, t1.Pack())
+
+    # Partially populated
+    t1 = TestStruct(string2=text, word4=0x1234)
+    expected = ("00" + text_bytes + "00000000" "3412").decode("hex")
+    self.assertEquals(expected, t1.Pack())
+
 
 if __name__ == "__main__":
   unittest.main()