pyloxi: parse unknown subclasses as the superclass

Previously we would throw an exception if we encountered a (for example)
experimenter action subclass that we didn't know existed. This was fine for
OFTest but causes problems for other tools that use pyloxi.

This change generates a full set of pack/unpack/pretty_print functions for
virtual classes. If a discriminator field contains a subtype we don't know
about, the virtual class is instantiated instead.

This also removes the special cases around unknown hello elements.
diff --git a/py_gen/oftype.py b/py_gen/oftype.py
index 7e28a0c..3b56308 100644
--- a/py_gen/oftype.py
+++ b/py_gen/oftype.py
@@ -150,12 +150,6 @@
         pack='%s.pack()',
         unpack='%s.unpack(%%s)' % pyclass)
 
-# Special case for lists of hello_elem, which must ignore unknown types
-type_data_map['list(of_hello_elem_t)'] = OFTypeData(
-    init='[]',
-    pack='loxi.generic_util.pack_list(%s)',
-    unpack='util.unpack_list_hello_elem(%s)')
-
 ## Public interface
 
 def lookup_type_data(oftype, version):
diff --git a/py_gen/templates/_ofclass.py b/py_gen/templates/_ofclass.py
index f902ae8..dfcd1fc 100644
--- a/py_gen/templates/_ofclass.py
+++ b/py_gen/templates/_ofclass.py
@@ -1,9 +1,19 @@
 :: superclass_pyname = ofclass.superclass.pyname if ofclass.superclass else "loxi.OFObject"
 :: from loxi_ir import *
 :: import py_gen.oftype
+:: import py_gen.util as util
 :: type_members = [m for m in ofclass.members if type(m) == OFTypeMember]
-:: normal_members = [m for m in ofclass.members if type(m) == OFDataMember]
+:: normal_members = [m for m in ofclass.members if type(m) == OFDataMember or
+::                                                 type(m) == OFDiscriminatorMember]
+:: if ofclass.virtual:
+:: discriminator_fmts = { 1: "B", 2: "!H", 4: "!L" }
+:: discriminator_fmt = discriminator_fmts[ofclass.discriminator.length]
+:: #endif
 class ${ofclass.pyname}(${superclass_pyname}):
+:: if ofclass.virtual:
+    subtypes = {}
+
+:: #endif
 :: for m in type_members:
     ${m.name} = ${m.value}
 :: #endfor
@@ -29,6 +39,13 @@
 
     @staticmethod
     def unpack(reader):
+:: if ofclass.virtual:
+        subtype, = reader.peek(${repr(discriminator_fmt)}, ${ofclass.discriminator.offset})
+        subclass = ${ofclass.pyname}.subtypes.get(subtype)
+        if subclass:
+            return subclass.unpack(reader)
+
+:: #endif
         obj = ${ofclass.pyname}()
 :: include("_unpack.py", ofclass=ofclass)
         return obj
diff --git a/py_gen/templates/_unpack.py b/py_gen/templates/_unpack.py
index 81f74c7..cce9de3 100644
--- a/py_gen/templates/_unpack.py
+++ b/py_gen/templates/_unpack.py
@@ -42,7 +42,7 @@
 ::     elif type(m) == OFTypeMember:
         _${m.name} = ${gen_unpack_expr(m.oftype, 'reader', version=version)}
         assert(_${m.name} == ${m.value})
-::     elif type(m) == OFDataMember:
+::     elif type(m) == OFDataMember or type(m) == OFDiscriminatorMember:
 ::         if m.name in field_length_members:
 ::             reader_expr = 'reader.slice(_%s)' % field_length_members[m.name].name
 ::         else:
diff --git a/py_gen/templates/_virtual_ofclass.py b/py_gen/templates/_virtual_ofclass.py
deleted file mode 100644
index 1e5dcc2..0000000
--- a/py_gen/templates/_virtual_ofclass.py
+++ /dev/null
@@ -1,23 +0,0 @@
-:: import py_gen.util as util
-:: superclass_pyname = ofclass.superclass.pyname if ofclass.superclass else "loxi.OFObject"
-:: fmts = { 1: "B", 2: "!H", 4: "!L" }
-:: fmt = fmts[ofclass.discriminator.length]
-:: trail = ' '.join([x.pyname for x in util.ancestors(ofclass)])
-class ${ofclass.pyname}(${superclass_pyname}):
-    subtypes = {}
-
-    @staticmethod
-    def unpack(reader):
-        subtype, = reader.peek(${repr(fmt)}, ${ofclass.discriminator.offset})
-        try:
-            subclass = ${ofclass.pyname}.subtypes[subtype]
-        except KeyError:
-            raise loxi.ProtocolError("unknown ${trail} subtype %#x" % subtype)
-        return subclass.unpack(reader)
-
-:: # Register with our superclass
-:: if ofclass.superclass:
-:: type_field_name = ofclass.superclass.discriminator.name
-:: type_value = ofclass.member_by_name(type_field_name).value
-${superclass_pyname}.subtypes[${type_value}] = ${ofclass.pyname}
-:: #endif
diff --git a/py_gen/templates/module.py b/py_gen/templates/module.py
index 02f0002..dfe23e8 100644
--- a/py_gen/templates/module.py
+++ b/py_gen/templates/module.py
@@ -52,11 +52,7 @@
 import loxi.generic_util
 
 :: for ofclass in ofclasses:
-:: if ofclass.virtual:
-:: include('_virtual_ofclass.py', ofclass=ofclass)
-:: else:
 :: include('_ofclass.py', ofclass=ofclass)
-:: #endif
 
 :: #endfor
 
diff --git a/py_gen/templates/util.py b/py_gen/templates/util.py
index d690939..85181dc 100644
--- a/py_gen/templates/util.py
+++ b/py_gen/templates/util.py
@@ -173,16 +173,6 @@
         x >>= 1
     return value
 
-def unpack_list_hello_elem(reader):
-    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(reader, deserializer) if x != None]
-
 def pack_checksum_128(value):
     return struct.pack("!QQ", (value >> 64) & MASK64, value & MASK64)
 
diff --git a/py_gen/tests/of10.py b/py_gen/tests/of10.py
index 250000f..7e10335 100644
--- a/py_gen/tests/of10.py
+++ b/py_gen/tests/of10.py
@@ -112,10 +112,10 @@
         with self.assertRaisesRegexp(ofp.ProtocolError, 'Buffer too short'):
             loxi.generic_util.unpack_list(OFReader(buf), ofp.action.action.unpack)
 
-    def test_invalid_action_type(self):
+    def test_unknown_action_type(self):
         buf = '\xff\xfe\x00\x08\x00\x00\x00\x00'
-        with self.assertRaisesRegexp(ofp.ProtocolError, 'unknown action subtype'):
-            loxi.generic_util.unpack_list(OFReader(buf), ofp.action.action.unpack)
+        result = loxi.generic_util.unpack_list(OFReader(buf), ofp.action.action.unpack)
+        self.assertEquals(result, [ofp.action.action(type=0xfffe)])
 
 class TestConstants(unittest.TestCase):
     def test_ports(self):
@@ -200,11 +200,19 @@
         test_klasses = [x for x in ofp.message.__dict__.values()
                         if type(x) == type
                            and issubclass(x, ofp.message.message)
-                           and hasattr(x, 'pack')]
+                           and not hasattr(x, 'subtypes')]
 
         for klass in test_klasses:
             self.assertIsInstance(ofp.message.parse_message(klass(xid=1).pack()), klass)
 
+    def test_parse_unknown_message(self):
+        import loxi
+        import loxi.of10 as ofp
+
+        buf = "\x01\xfe\x00\x08\x12\x34\x56\x78"
+        msg = ofp.message.parse_message(buf)
+        self.assertIsInstance(msg, ofp.message.message)
+
 class TestUtils(unittest.TestCase):
     def test_pretty_wildcards(self):
         self.assertEquals("OFPFW_ALL", ofp.util.pretty_wildcards(ofp.OFPFW_ALL))
@@ -231,7 +239,7 @@
                               for klass in mod.__dict__.values()
                               if isinstance(klass, type) and
                                  issubclass(klass, loxi.OFObject) and
-                                 hasattr(klass, 'pack')]
+                                 not hasattr(klass, 'subtypes')]
         self.klasses.sort(key=lambda x: str(x))
 
     def test_serialization(self):
diff --git a/py_gen/tests/of11.py b/py_gen/tests/of11.py
index d620509..7ae6d8a 100644
--- a/py_gen/tests/of11.py
+++ b/py_gen/tests/of11.py
@@ -69,7 +69,7 @@
                               for klass in mod.__dict__.values()
                               if isinstance(klass, type) and
                                  issubclass(klass, loxi.OFObject) and
-                                 hasattr(klass, 'pack')]
+                                 not hasattr(klass, 'subtypes')]
         self.klasses.sort(key=lambda x: str(x))
 
     def test_serialization(self):
diff --git a/py_gen/tests/of12.py b/py_gen/tests/of12.py
index 4774672..c463c50 100644
--- a/py_gen/tests/of12.py
+++ b/py_gen/tests/of12.py
@@ -78,7 +78,7 @@
                               for klass in mod.__dict__.values()
                               if isinstance(klass, type) and
                                  issubclass(klass, loxi.OFObject) and
-                                 hasattr(klass, 'pack')]
+                                 not hasattr(klass, 'subtypes')]
         self.klasses.sort(key=lambda x: str(x))
 
     def test_serialization(self):
diff --git a/py_gen/tests/of13.py b/py_gen/tests/of13.py
index 8e258df..c5a16b2 100644
--- a/py_gen/tests/of13.py
+++ b/py_gen/tests/of13.py
@@ -60,18 +60,6 @@
         self.assertTrue(hasattr(loxi.of13, "message"))
         self.assertTrue(hasattr(loxi.of13, "oxm"))
 
-class TestCommon(unittest.TestCase):
-    def test_list_hello_elem_unpack(self):
-        buf = ''.join([
-            '\x00\x01\x00\x04', # versionbitmap
-            '\x00\x00\x00\x04', # unknown type
-            '\x00\x01\x00\x04', # versionbitmap
-        ])
-        l = ofp.util.unpack_list_hello_elem(OFReader(buf))
-        self.assertEquals(len(l), 2)
-        self.assertTrue(isinstance(l[0], ofp.hello_elem_versionbitmap))
-        self.assertTrue(isinstance(l[1], ofp.hello_elem_versionbitmap))
-
 # The majority of the serialization tests are created here using the files in
 # the test_data directory.
 class TestDataFiles(unittest.TestCase):
@@ -91,7 +79,7 @@
                               for klass in mod.__dict__.values()
                               if isinstance(klass, type) and
                                  issubclass(klass, loxi.OFObject) and
-                                 hasattr(klass, 'pack')]
+                                 not hasattr(klass, 'subtypes')]
         self.klasses.sort(key=lambda x: str(x))
 
     def test_serialization(self):