pyloxi: generic list deserialization

Since objects now handle their own type and length fields they can all use the
same generic list code. The oftype functions now generate list pack, unpack,
and init code on demand so nothing needs to be changed in pyloxi to use a new
list type.
diff --git a/py_gen/oftype.py b/py_gen/oftype.py
index a8e1f3a..9718a9f 100644
--- a/py_gen/oftype.py
+++ b/py_gen/oftype.py
@@ -28,6 +28,8 @@
 from collections import namedtuple
 
 import loxi_utils.loxi_utils as loxi_utils
+import py_gen.codegen
+import loxi_globals
 
 OFTypeData = namedtuple("OFTypeData", ["init", "pack", "unpack"])
 
@@ -109,24 +111,6 @@
         init='None',
         pack='%s.pack()',
         unpack='oxm.oxm.unpack(%s)'),
-
-    # TODO implement unpack
-    'list(of_table_features_t)': OFTypeData(
-        init='[]',
-        pack='util.pack_list(%s)',
-        unpack=None),
-
-    # TODO implement unpack
-    'list(of_action_id_t)': OFTypeData(
-        init='[]',
-        pack='util.pack_list(%s)',
-        unpack=None),
-
-    # TODO implement unpack
-    'list(of_table_feature_prop_t)': OFTypeData(
-        init='[]',
-        pack='util.pack_list(%s)',
-        unpack=None),
 }
 
 ## Fixed length strings
@@ -161,51 +145,11 @@
         pack='%s.pack()',
         unpack='%s.unpack(%%s)' % pyclass)
 
-## Variable element length lists
-
-# Map from list class name to list deserializer
-variable_elem_len_lists = {
-    'list(of_action_t)': 'util.unpack_list_action',
-    'list(of_bucket_t)': 'util.unpack_list_bucket',
-    'list(of_flow_stats_entry_t)': 'util.unpack_list_flow_stats_entry',
-    'list(of_group_desc_stats_entry_t)': 'util.unpack_list_group_desc_stats_entry',
-    'list(of_group_stats_entry_t)': 'util.unpack_list_group_stats_entry',
-    'list(of_hello_elem_t)': 'util.unpack_list_hello_elem',
-    'list(of_instruction_t)': 'util.unpack_list_instruction',
-    'list(of_meter_band_t)': 'util.unpack_list_meter_band',
-    'list(of_meter_stats_t)': 'util.unpack_list_meter_stats',
-    'list(of_packet_queue_t)': 'util.unpack_list_packet_queue',
-    'list(of_queue_prop_t)': 'util.unpack_list_queue_prop',
-    'list(of_oxm_t)': 'util.unpack_list_oxm',
-}
-
-for (cls, deserializer) in variable_elem_len_lists.items():
-    type_data_map[cls] = OFTypeData(
-        init='[]',
-        pack='util.pack_list(%s)',
-        unpack='%s(%%s)' % deserializer)
-
-## Fixed element length lists
-
-# Map from list class name to list element deserializer
-fixed_elem_len_lists = {
-    'list(of_bsn_interface_t)': 'common.bsn_interface.unpack',
-    'list(of_bucket_counter_t)': 'common.bucket_counter.unpack',
-    'list(of_meter_band_stats_t)': 'common.meter_band_stats.unpack',
-    'list(of_port_desc_t)': 'common.port_desc.unpack',
-    'list(of_port_stats_entry_t)': 'common.port_stats_entry.unpack',
-    'list(of_queue_stats_entry_t)': 'common.queue_stats_entry.unpack',
-    'list(of_table_stats_entry_t)': 'common.table_stats_entry.unpack',
-    'list(of_uint32_t)': 'common.uint32.unpack',
-    'list(of_uint8_t)': 'common.uint8.unpack',
-    'list(of_bsn_lacp_stats_entry_t)': 'common.bsn_lacp_stats_entry.unpack',
-}
-
-for (cls, element_deserializer) in fixed_elem_len_lists.items():
-    type_data_map[cls] = OFTypeData(
-        init='[]',
-        pack='util.pack_list(%s)',
-        unpack='loxi.generic_util.unpack_list(%%s, %s)' % element_deserializer)
+# Special case for lists of hello_elem, which must ignore unknown types
+type_data_map['list(of_hello_elem_t)'] = OFTypeData(
+    init='[]',
+    pack='util.pack_list(%s)',
+    unpack='util.unpack_list_hello_elem(%s)')
 
 ## Public interface
 
@@ -217,6 +161,8 @@
     type_data = lookup_type_data(oftype, version)
     if type_data and type_data.init:
         return type_data.init
+    elif oftype_is_list(oftype):
+        return "[]"
     else:
         return "loxi.unimplemented('init %s')" % oftype
 
@@ -228,6 +174,8 @@
     type_data = lookup_type_data(oftype, version)
     if type_data and type_data.pack:
         return type_data.pack % value_expr
+    elif oftype_is_list(oftype):
+        return "util.pack_list(%s)" % value_expr
     else:
         return "loxi.unimplemented('pack %s')" % oftype
 
@@ -239,5 +187,19 @@
     type_data = lookup_type_data(oftype, version)
     if type_data and type_data.unpack:
         return type_data.unpack % reader_expr
+    elif oftype_is_list(oftype):
+        ofproto = loxi_globals.ir[version]
+        ofclass = ofproto.class_by_name(oftype_list_elem(oftype))
+        module_name, class_name = py_gen.codegen.generate_pyname(ofclass)
+        return 'loxi.generic_util.unpack_list(%s, %s.%s.unpack)' % \
+            (reader_expr, module_name, class_name)
     else:
         return "loxi.unimplemented('unpack %s')" % oftype
+
+def oftype_is_list(oftype):
+    return (oftype.find("list(") == 0)
+
+# Converts "list(of_flow_stats_entry_t)" to "of_flow_stats_entry"
+def oftype_list_elem(oftype):
+    assert oftype.find("list(") == 0
+    return oftype[5:-3]
diff --git a/py_gen/templates/generic_util.py b/py_gen/templates/generic_util.py
index 89b158d..99840ec 100644
--- a/py_gen/templates/generic_util.py
+++ b/py_gen/templates/generic_util.py
@@ -44,25 +44,6 @@
         entries.append(deserializer(reader))
     return entries
 
-def unpack_list_lv16(reader, deserializer):
-    """
-    The deserializer function should take an OFReader and return the new object.
-    """
-    def wrapper(reader):
-        length, = reader.peek('!H')
-        return deserializer(reader.slice(length))
-    return unpack_list(reader, wrapper)
-
-def unpack_list_tlv16(reader, deserializer):
-    """
-    The deserializer function should take an OFReader and an integer type
-    and return the new object.
-    """
-    def wrapper(reader):
-        typ, length, = reader.peek('!HH')
-        return deserializer(reader.slice(length), typ)
-    return unpack_list(reader, wrapper)
-
 def pad_to(alignment, length):
     """
     Return a string of zero bytes that will pad a string of length 'length' to
diff --git a/py_gen/templates/util.py b/py_gen/templates/util.py
index 73da0ea..9c9ccc1 100644
--- a/py_gen/templates/util.py
+++ b/py_gen/templates/util.py
@@ -174,57 +174,12 @@
         x >>= 1
     return value
 
-def unpack_list_flow_stats_entry(reader):
-    return loxi.generic_util.unpack_list_lv16(reader, common.flow_stats_entry.unpack)
-
-def unpack_list_queue_prop(reader):
-    def deserializer(reader, typ):
-        return common.queue_prop.unpack(reader)
-    return loxi.generic_util.unpack_list_tlv16(reader, deserializer)
-
-def unpack_list_packet_queue(reader):
-    def wrapper(reader):
-        length, = reader.peek('!4xH')
-        return common.packet_queue.unpack(reader.slice(length))
-    return loxi.generic_util.unpack_list(reader, wrapper)
-
 def unpack_list_hello_elem(reader):
-    def deserializer(reader, typ):
+    def deserializer(reader):
+        typ, length, = reader.peek('!HH')
+        reader = reader.slice(length)
         try:
             return common.hello_elem.unpack(reader)
         except loxi.ProtocolError:
             return None
-    return [x for x in loxi.generic_util.unpack_list_tlv16(reader, deserializer) if x != None]
-
-def unpack_list_bucket(reader):
-    return loxi.generic_util.unpack_list_lv16(reader, common.bucket.unpack)
-
-def unpack_list_group_desc_stats_entry(reader):
-    return loxi.generic_util.unpack_list_lv16(reader, common.group_desc_stats_entry.unpack)
-
-def unpack_list_group_stats_entry(reader):
-    return loxi.generic_util.unpack_list_lv16(reader, common.group_stats_entry.unpack)
-
-def unpack_list_meter_stats(reader):
-    def wrapper(reader):
-        length, = reader.peek('!4xH')
-        return common.meter_stats.unpack(reader.slice(length))
-    return loxi.generic_util.unpack_list(reader, wrapper)
-
-def unpack_list_action(reader):
-    def deserializer(reader, typ):
-        return action.action.unpack(reader)
-    return loxi.generic_util.unpack_list_tlv16(reader, deserializer)
-
-def unpack_list_instruction(reader):
-    def deserializer(reader, typ):
-        return instruction.instruction.unpack(reader)
-    return loxi.generic_util.unpack_list_tlv16(reader, deserializer)
-
-def unpack_list_meter_band(reader):
-    def deserializer(reader, typ):
-        return meter_band.meter_band.unpack(reader)
-    return loxi.generic_util.unpack_list_tlv16(reader, deserializer)
-
-def unpack_list_oxm(reader):
-    return loxi.generic_util.unpack_list(reader, oxm.oxm.unpack)
+    return [x for x in loxi.generic_util.unpack_list(reader, deserializer) if x != None]
diff --git a/py_gen/tests/generic_util.py b/py_gen/tests/generic_util.py
index a3b18c6..8b2f59f 100644
--- a/py_gen/tests/generic_util.py
+++ b/py_gen/tests/generic_util.py
@@ -43,15 +43,6 @@
         a = loxi.generic_util.unpack_list(reader, deserializer)
         self.assertEquals(['\x04abc', '\x03de', '\x02f', '\x01'], a)
 
-class TestUnpackListLV16(unittest.TestCase):
-    def test_simple(self):
-        def deserializer(reader):
-            reader.skip(2)
-            return reader.read_all()
-        reader = loxi.generic_util.OFReader("\x00\x05abc\x00\x04de\x00\x03f\x00\x02")
-        a = loxi.generic_util.unpack_list_lv16(reader, deserializer)
-        self.assertEquals(['abc', 'de', 'f', ''], a)
-
 class TestOFReader(unittest.TestCase):
     def test_empty(self):
         reader = OFReader("")
diff --git a/py_gen/tests/of10.py b/py_gen/tests/of10.py
index e08a2b3..250000f 100644
--- a/py_gen/tests/of10.py
+++ b/py_gen/tests/of10.py
@@ -88,34 +88,34 @@
         add(ofp.action.bsn_set_tunnel_dst(dst=0x12345678))
         add(ofp.action.nicira_dec_ttl())
 
-        actions = ofp.util.unpack_list_action(OFReader(''.join(bufs)))
+        actions = loxi.generic_util.unpack_list(OFReader(''.join(bufs)), ofp.action.action.unpack)
         self.assertEquals(actions, expected)
 
     def test_empty_list(self):
-        self.assertEquals(ofp.util.unpack_list_action(OFReader('')), [])
+        self.assertEquals(loxi.generic_util.unpack_list(OFReader(''), ofp.action.action.unpack), [])
 
     def test_invalid_list_length(self):
         buf = '\x00' * 9
         with self.assertRaisesRegexp(ofp.ProtocolError, 'Buffer too short'):
-            ofp.util.unpack_list_action(OFReader(buf))
+            loxi.generic_util.unpack_list(OFReader(buf), ofp.action.action.unpack)
 
     def test_invalid_action_length(self):
         buf = '\x00' * 8
         with self.assertRaisesRegexp(ofp.ProtocolError, 'Buffer too short'):
-            ofp.util.unpack_list_action(OFReader(buf))
+            loxi.generic_util.unpack_list(OFReader(buf), ofp.action.action.unpack)
 
         buf = '\x00\x00\x00\x04'
         with self.assertRaisesRegexp(ofp.ProtocolError, 'Buffer too short'):
-            ofp.util.unpack_list_action(OFReader(buf))
+            loxi.generic_util.unpack_list(OFReader(buf), ofp.action.action.unpack)
 
         buf = '\x00\x00\x00\x10\x00\x00\x00\x00'
         with self.assertRaisesRegexp(ofp.ProtocolError, 'Buffer too short'):
-            ofp.util.unpack_list_action(OFReader(buf))
+            loxi.generic_util.unpack_list(OFReader(buf), ofp.action.action.unpack)
 
     def test_invalid_action_type(self):
         buf = '\xff\xfe\x00\x08\x00\x00\x00\x00'
         with self.assertRaisesRegexp(ofp.ProtocolError, 'unknown action subtype'):
-            ofp.util.unpack_list_action(OFReader(buf))
+            loxi.generic_util.unpack_list(OFReader(buf), ofp.action.action.unpack)
 
 class TestConstants(unittest.TestCase):
     def test_ports(self):
diff --git a/py_gen/tests/of13.py b/py_gen/tests/of13.py
index 54a0629..8e258df 100644
--- a/py_gen/tests/of13.py
+++ b/py_gen/tests/of13.py
@@ -97,13 +97,6 @@
     def test_serialization(self):
         expected_failures = [
             ofp.action.set_field, # field defaults to None
-            ofp.common.table_feature_prop_apply_actions,
-            ofp.common.table_feature_prop_apply_actions_miss,
-            ofp.common.table_feature_prop_write_actions,
-            ofp.common.table_feature_prop_write_actions_miss,
-            ofp.common.table_features,
-            ofp.message.table_features_stats_reply,
-            ofp.message.table_features_stats_request,
         ]
         for klass in self.klasses:
             def fn():
@@ -119,8 +112,6 @@
 
     def test_parse_message(self):
         expected_failures = [
-            ofp.message.table_features_stats_reply,
-            ofp.message.table_features_stats_request,
         ]
         for klass in self.klasses:
             if not issubclass(klass, ofp.message.message):