pyloxi: create inheritance hierarchy

Virtual classes are generated but currently only serve as superclasses.

Somehow this works without topologically sorting the classes.
diff --git a/py_gen/codegen.py b/py_gen/codegen.py
index 4eafa48..f135f18 100644
--- a/py_gen/codegen.py
+++ b/py_gen/codegen.py
@@ -25,7 +25,7 @@
 # EPL for the specific language governing permissions and limitations
 # under the EPL.
 
-from collections import namedtuple
+from collections import defaultdict
 import loxi_globals
 import struct
 import template_utils
@@ -34,74 +34,74 @@
 import oftype
 from loxi_ir import *
 
-ofclasses_by_version = {}
+modules_by_version = {}
 
-# Return the name for the generated Python class
-def generate_pyname(cls):
-    if utils.class_is_action(cls):
-        return cls[10:]
-    elif utils.class_is_oxm(cls):
-        return cls[7:]
-    elif utils.class_is_meter_band(cls):
-        return cls[14:]
-    elif utils.class_is_instruction(cls):
-        return cls[15:]
-    else:
-        return cls[3:]
+# Map from inheritance root to module name
+roots = {
+    'of_header': 'message',
+    'of_action': 'action',
+    'of_oxm': 'oxm',
+    'of_instruction': 'instruction',
+    'of_meter_band': 'meter_band',
+}
+
+# Return the module and class names for the generated Python class
+def generate_pyname(ofclass):
+    for root, module_name in roots.items():
+        if ofclass.name == root:
+            return module_name, module_name
+        elif ofclass.is_instanceof(root):
+            if root == 'of_header':
+                # The input files don't prefix message names
+                return module_name, ofclass.name[3:]
+            else:
+                return module_name, ofclass.name[len(root)+1:]
+    return 'common', ofclass.name[3:]
 
 # Create intermediate representation, extended from the LOXI IR
 def build_ofclasses(version):
-    ofclasses = []
+    modules = defaultdict(list)
     for ofclass in loxi_globals.ir[version].classes:
-        if ofclass.virtual:
-            continue
-
-        ofclass.pyname = generate_pyname(ofclass.name)
-        ofclasses.append(ofclass)
-
-    return ofclasses
+        module_name, ofclass.pyname = generate_pyname(ofclass)
+        modules[module_name].append(ofclass)
+    return modules
 
 def generate_init(out, name, version):
     util.render_template(out, 'init.py', version=version)
 
 def generate_action(out, name, version):
-    ofclasses = [x for x in ofclasses_by_version[version]
-                 if utils.class_is_action(x.name)]
-    util.render_template(out, 'action.py', ofclasses=ofclasses, version=version)
+    util.render_template(out, 'action.py',
+                         ofclasses=modules_by_version[version]['action'],
+                         version=version)
 
 def generate_oxm(out, name, version):
-    ofclasses = [x for x in ofclasses_by_version[version]
-                 if utils.class_is_oxm(x.name)]
-    util.render_template(out, 'oxm.py', ofclasses=ofclasses, version=version)
+    util.render_template(out, 'oxm.py',
+                         ofclasses=modules_by_version[version]['oxm'],
+                         version=version)
 
 def generate_common(out, name, version):
-    ofclasses = [x for x in ofclasses_by_version[version]
-                 if not utils.class_is_message(x.name)
-                    and not utils.class_is_action(x.name)
-                    and not utils.class_is_instruction(x.name)
-                    and not utils.class_is_meter_band(x.name)
-                    and not utils.class_is_oxm(x.name)
-                    and not utils.class_is_list(x.name)]
-    util.render_template(out, 'common.py', ofclasses=ofclasses, version=version)
+    util.render_template(out, 'common.py',
+                         ofclasses=modules_by_version[version]['common'],
+                         version=version)
 
 def generate_const(out, name, version):
     util.render_template(out, 'const.py', version=version,
                          enums=loxi_globals.ir[version].enums)
 
 def generate_instruction(out, name, version):
-    ofclasses = [x for x in ofclasses_by_version[version]
-                 if utils.class_is_instruction(x.name)]
-    util.render_template(out, 'instruction.py', ofclasses=ofclasses, version=version)
+    util.render_template(out, 'instruction.py',
+                         ofclasses=modules_by_version[version]['instruction'],
+                         version=version)
 
 def generate_message(out, name, version):
-    ofclasses = [x for x in ofclasses_by_version[version]
-                 if utils.class_is_message(x.name)]
-    util.render_template(out, 'message.py', ofclasses=ofclasses, version=version)
+    util.render_template(out, 'message.py',
+                         ofclasses=modules_by_version[version]['message'],
+                         version=version)
 
 def generate_meter_band(out, name, version):
-    ofclasses = [x for x in ofclasses_by_version[version]
-                 if utils.class_is_meter_band(x.name)]
-    util.render_template(out, 'meter_band.py', ofclasses=ofclasses, version=version)
+    util.render_template(out, 'meter_band.py',
+                         ofclasses=modules_by_version[version]['meter_band'],
+                         version=version)
 
 def generate_pp(out, name, version):
     util.render_template(out, 'pp.py')
@@ -111,4 +111,4 @@
 
 def init():
     for version in loxi_globals.OFVersions.target_versions:
-        ofclasses_by_version[version] = build_ofclasses(version)
+        modules_by_version[version] = build_ofclasses(version)
diff --git a/py_gen/templates/_ofclass.py b/py_gen/templates/_ofclass.py
index 53d6d7a..610e5a8 100644
--- a/py_gen/templates/_ofclass.py
+++ b/py_gen/templates/_ofclass.py
@@ -1,8 +1,9 @@
+:: superclass_pyname = ofclass.superclass.pyname if ofclass.superclass else "loxi.OFObject"
 :: from loxi_ir import *
 :: import py_gen.oftype
 :: type_members = [m for m in ofclass.members if type(m) == OFTypeMember]
 :: normal_members = [m for m in ofclass.members if type(m) == OFDataMember]
-class ${ofclass.pyname}(${superclass}):
+class ${ofclass.pyname}(${superclass_pyname}):
 :: for m in type_members:
     ${m.name} = ${m.value}
 :: #endfor
@@ -39,12 +40,5 @@
 :: #endfor
         return True
 
-    def __ne__(self, other):
-        return not self.__eq__(other)
-
-    def show(self):
-        import loxi.pp
-        return loxi.pp.pp(self)
-
     def pretty_print(self, q):
 :: include('_pretty_print.py', ofclass=ofclass)
diff --git a/py_gen/templates/_virtual_ofclass.py b/py_gen/templates/_virtual_ofclass.py
new file mode 100644
index 0000000..29f01f7
--- /dev/null
+++ b/py_gen/templates/_virtual_ofclass.py
@@ -0,0 +1,3 @@
+:: superclass_pyname = ofclass.superclass.pyname if ofclass.superclass else "loxi.OFObject"
+class ${ofclass.pyname}(${superclass_pyname}):
+    pass
diff --git a/py_gen/templates/action.py b/py_gen/templates/action.py
index 77a1d9f..2f1d020 100644
--- a/py_gen/templates/action.py
+++ b/py_gen/templates/action.py
@@ -48,12 +48,12 @@
         return parser(reader)
     return loxi.generic_util.unpack_list_tlv16(reader, deserializer)
 
-class Action(object):
-    type = None # override in subclass
-    pass
-
 :: for ofclass in ofclasses:
-:: include('_ofclass.py', ofclass=ofclass, superclass="Action")
+:: if ofclass.virtual:
+:: include('_virtual_ofclass.py', ofclass=ofclass)
+:: else:
+:: include('_ofclass.py', ofclass=ofclass)
+:: #endif
 
 :: #endfor
 
@@ -72,8 +72,9 @@
         raise loxi.ProtocolError("unexpected BSN experimenter subtype %#x" % subtype)
 
 parsers = {
+:: concrete_ofclasses = [x for x in ofclasses if not x.virtual]
 :: sort_key = lambda x: x.member_by_name('type').value
-:: msgtype_groups = itertools.groupby(sorted(ofclasses, key=sort_key), sort_key)
+:: msgtype_groups = itertools.groupby(sorted(concrete_ofclasses, key=sort_key), sort_key)
 :: for (k, v) in msgtype_groups:
 :: k = util.constant_for_value(version, "ofp_action_type", k)
 :: v = list(v)
@@ -85,7 +86,7 @@
 :: #endfor
 }
 
-:: experimenter_ofclasses = [x for x in ofclasses if x.member_by_name('type').value == 0xffff]
+:: experimenter_ofclasses = [x for x in concrete_ofclasses if x.member_by_name('type').value == 0xffff]
 :: sort_key = lambda x: x.member_by_name('experimenter').value
 :: experimenter_ofclasses.sort(key=sort_key)
 :: grouped = itertools.groupby(experimenter_ofclasses, sort_key)
diff --git a/py_gen/templates/common.py b/py_gen/templates/common.py
index 76ae631..ddffdf7 100644
--- a/py_gen/templates/common.py
+++ b/py_gen/templates/common.py
@@ -90,7 +90,11 @@
     return loxi.generic_util.unpack_list(reader, wrapper)
 
 :: for ofclass in ofclasses:
-:: include('_ofclass.py', ofclass=ofclass, superclass="object")
+:: if ofclass.virtual:
+:: include('_virtual_ofclass.py', ofclass=ofclass)
+:: else:
+:: include('_ofclass.py', ofclass=ofclass)
+:: #endif
 
 :: #endfor
 
diff --git a/py_gen/templates/instruction.py b/py_gen/templates/instruction.py
index 5dfafab..21ab84a 100644
--- a/py_gen/templates/instruction.py
+++ b/py_gen/templates/instruction.py
@@ -45,12 +45,12 @@
         return parser(reader)
     return loxi.generic_util.unpack_list_tlv16(reader, deserializer)
 
-class Instruction(object):
-    type = None # override in subclass
-    pass
-
 :: for ofclass in ofclasses:
-:: include('_ofclass.py', ofclass=ofclass, superclass="Instruction")
+:: if ofclass.virtual:
+:: include('_virtual_ofclass.py', ofclass=ofclass)
+:: else:
+:: include('_ofclass.py', ofclass=ofclass)
+:: #endif
 
 :: #endfor
 
@@ -67,8 +67,9 @@
         raise loxi.ProtocolError("unexpected experimenter id %#x subtype %#x" % (experimenter, subtype))
 
 parsers = {
+:: concrete_ofclasses = [x for x in ofclasses if not x.virtual]
 :: sort_key = lambda x: x.member_by_name('type').value
-:: msgtype_groups = itertools.groupby(sorted(ofclasses, key=sort_key), sort_key)
+:: msgtype_groups = itertools.groupby(sorted(concrete_ofclasses, key=sort_key), sort_key)
 :: for (k, v) in msgtype_groups:
 :: k = util.constant_for_value(version, "ofp_instruction_type", k)
 :: v = list(v)
@@ -80,7 +81,7 @@
 :: #endfor
 }
 
-:: experimenter_ofclasses = [x for x in ofclasses if x.member_by_name('type').value == 0xffff]
+:: experimenter_ofclasses = [x for x in concrete_ofclasses if x.member_by_name('type').value == 0xffff]
 :: sort_key = lambda x: x.member_by_name('experimenter').value
 :: experimenter_ofclasses.sort(key=sort_key)
 :: grouped = itertools.groupby(experimenter_ofclasses, sort_key)
diff --git a/py_gen/templates/message.py b/py_gen/templates/message.py
index 2106ce7..9307676 100644
--- a/py_gen/templates/message.py
+++ b/py_gen/templates/message.py
@@ -48,13 +48,12 @@
 import util
 import loxi.generic_util
 
-class Message(object):
-    version = const.OFP_VERSION
-    type = None # override in subclass
-    xid = None
-
 :: for ofclass in ofclasses:
-:: include('_ofclass.py', ofclass=ofclass, superclass="Message")
+:: if ofclass.virtual:
+:: include('_virtual_ofclass.py', ofclass=ofclass)
+:: else:
+:: include('_ofclass.py', ofclass=ofclass)
+:: #endif
 
 :: #endfor
 
@@ -170,8 +169,9 @@
         raise loxi.ProtocolError("unexpected experimenter %#x subtype %#x" % (experimenter, subtype))
 
 parsers = {
+:: concrete_ofclasses = [x for x in ofclasses if not x.virtual]
 :: sort_key = lambda x: x.member_by_name('type').value
-:: msgtype_groups = itertools.groupby(sorted(ofclasses, key=sort_key), sort_key)
+:: msgtype_groups = itertools.groupby(sorted(concrete_ofclasses, key=sort_key), sort_key)
 :: for (k, v) in msgtype_groups:
 :: k = util.constant_for_value(version, "ofp_type", k)
 :: v = list(v)
@@ -271,7 +271,7 @@
 :: #endif
 }
 
-:: experimenter_ofclasses = [x for x in ofclasses if x.member_by_name('type').value == 4]
+:: experimenter_ofclasses = [x for x in concrete_ofclasses if x.member_by_name('type').value == 4]
 :: sort_key = lambda x: x.member_by_name('experimenter').value
 :: experimenter_ofclasses.sort(key=sort_key)
 :: grouped = itertools.groupby(experimenter_ofclasses, sort_key)
diff --git a/py_gen/templates/meter_band.py b/py_gen/templates/meter_band.py
index 3273314..ef73978 100644
--- a/py_gen/templates/meter_band.py
+++ b/py_gen/templates/meter_band.py
@@ -44,18 +44,19 @@
         return parser(reader)
     return loxi.generic_util.unpack_list_tlv16(reader, deserializer)
 
-class MeterBand(object):
-    type = None # override in subclass
-    pass
-
 :: for ofclass in ofclasses:
-:: include('_ofclass.py', ofclass=ofclass, superclass="MeterBand")
+:: if ofclass.virtual:
+:: include('_virtual_ofclass.py', ofclass=ofclass)
+:: else:
+:: include('_ofclass.py', ofclass=ofclass)
+:: #endif
 
 :: #endfor
 
 parsers = {
+:: concrete_ofclasses = [x for x in ofclasses if not x.virtual]
 :: sort_key = lambda x: x.member_by_name('type').value
-:: msgtype_groups = itertools.groupby(sorted(ofclasses, key=sort_key), sort_key)
+:: msgtype_groups = itertools.groupby(sorted(concrete_ofclasses, key=sort_key), sort_key)
 :: for (k, v) in msgtype_groups:
 :: k = util.constant_for_value(version, "ofp_meter_band_type", k)
 :: v = list(v)
diff --git a/py_gen/templates/oxm.py b/py_gen/templates/oxm.py
index 176a74c..4adfab0 100644
--- a/py_gen/templates/oxm.py
+++ b/py_gen/templates/oxm.py
@@ -48,18 +48,19 @@
 def unpack_list(reader):
     return loxi.generic_util.unpack_list(reader, unpack)
 
-class OXM(object):
-    type_len = None # override in subclass
-    pass
-
 :: for ofclass in ofclasses:
-:: include('_ofclass.py', ofclass=ofclass, superclass="OXM")
+:: if ofclass.virtual:
+:: include('_virtual_ofclass.py', ofclass=ofclass)
+:: else:
+:: include('_ofclass.py', ofclass=ofclass)
+:: #endif
 
 :: #endfor
 
 parsers = {
+:: concrete_ofclasses = [x for x in ofclasses if not x.virtual]
 :: key = lambda x: x.member_by_name('type_len').value
-:: for ofclass in sorted(ofclasses, key=key):
+:: for ofclass in sorted(concrete_ofclasses, key=key):
     ${key(ofclass)} : ${ofclass.pyname}.unpack,
 :: #endfor
 }
diff --git a/py_gen/templates/toplevel_init.py b/py_gen/templates/toplevel_init.py
index b170bd8..e5493a5 100644
--- a/py_gen/templates/toplevel_init.py
+++ b/py_gen/templates/toplevel_init.py
@@ -68,3 +68,17 @@
 
 def unimplemented(msg):
     raise Unimplemented(msg)
+
+class OFObject(object):
+    """
+    Superclass of all OpenFlow classes
+    """
+    def __init__(self, *args):
+        raise NotImplementedError("cannot instantiate abstract class")
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def show(self):
+        import loxi.pp
+        return loxi.pp.pp(self)
diff --git a/py_gen/tests/of10.py b/py_gen/tests/of10.py
index c4725e3..818f123 100644
--- a/py_gen/tests/of10.py
+++ b/py_gen/tests/of10.py
@@ -30,6 +30,7 @@
 from testutil import add_datafiles_tests
 
 try:
+    import loxi
     import loxi.of10 as ofp
     from loxi.generic_util import OFReader
 except ImportError:
@@ -195,11 +196,11 @@
         msg = ofp.message.parse_message(buf)
         assert(msg.xid == 0x12345678)
 
-        # Get a list of all message classes
+        # Get a list of all concrete message classes
         test_klasses = [x for x in ofp.message.__dict__.values()
                         if type(x) == type
-                           and issubclass(x, ofp.message.Message)
-                           and x != ofp.message.Message]
+                           and issubclass(x, ofp.message.message)
+                           and hasattr(x, 'pack')]
 
         for klass in test_klasses:
             self.assertIsInstance(ofp.message.parse_message(klass(xid=1).pack()), klass)
@@ -228,7 +229,9 @@
         mods = [ofp.action,ofp.message,ofp.common]
         self.klasses = [klass for mod in mods
                               for klass in mod.__dict__.values()
-                              if hasattr(klass, 'show')]
+                              if isinstance(klass, type) and
+                                 issubclass(klass, loxi.OFObject) and
+                                 hasattr(klass, 'pack')]
         self.klasses.sort(key=lambda x: str(x))
 
     def test_serialization(self):
@@ -248,7 +251,7 @@
     def test_parse_message(self):
         expected_failures = []
         for klass in self.klasses:
-            if not issubclass(klass, ofp.message.Message):
+            if not issubclass(klass, ofp.message.message):
                 continue
             def fn():
                 obj = klass(xid=42)
diff --git a/py_gen/tests/of11.py b/py_gen/tests/of11.py
index 07a5437..b30a40c 100644
--- a/py_gen/tests/of11.py
+++ b/py_gen/tests/of11.py
@@ -28,6 +28,7 @@
 import unittest
 
 try:
+    import loxi
     import loxi.of11 as ofp
 except ImportError:
     exit("loxi package not found. Try setting PYTHONPATH.")
@@ -65,7 +66,9 @@
         mods = [ofp.action,ofp.message,ofp.common]
         self.klasses = [klass for mod in mods
                               for klass in mod.__dict__.values()
-                              if hasattr(klass, 'show')]
+                              if isinstance(klass, type) and
+                                 issubclass(klass, loxi.OFObject) and
+                                 hasattr(klass, 'pack')]
         self.klasses.sort(key=lambda x: str(x))
 
     def test_serialization(self):
@@ -85,7 +88,7 @@
     def test_parse_message(self):
         expected_failures = []
         for klass in self.klasses:
-            if not issubclass(klass, ofp.message.Message):
+            if not issubclass(klass, ofp.message.message):
                 continue
             def fn():
                 obj = klass(xid=42)
diff --git a/py_gen/tests/of12.py b/py_gen/tests/of12.py
index 98d999b..fcdb0cf 100644
--- a/py_gen/tests/of12.py
+++ b/py_gen/tests/of12.py
@@ -29,6 +29,7 @@
 from testutil import add_datafiles_tests
 
 try:
+    import loxi
     import loxi.of12 as ofp
 except ImportError:
     exit("loxi package not found. Try setting PYTHONPATH.")
@@ -74,7 +75,9 @@
         mods = [ofp.action,ofp.message,ofp.common,ofp.oxm]
         self.klasses = [klass for mod in mods
                               for klass in mod.__dict__.values()
-                              if hasattr(klass, 'show')]
+                              if isinstance(klass, type) and
+                                 issubclass(klass, loxi.OFObject) and
+                                 hasattr(klass, 'pack')]
         self.klasses.sort(key=lambda x: str(x))
 
     def test_serialization(self):
@@ -96,7 +99,7 @@
     def test_parse_message(self):
         expected_failures = []
         for klass in self.klasses:
-            if not issubclass(klass, ofp.message.Message):
+            if not issubclass(klass, ofp.message.message):
                 continue
             def fn():
                 obj = klass(xid=42)
diff --git a/py_gen/tests/of13.py b/py_gen/tests/of13.py
index 8c18f41..07b0ef0 100644
--- a/py_gen/tests/of13.py
+++ b/py_gen/tests/of13.py
@@ -30,6 +30,7 @@
 from testutil import add_datafiles_tests
 
 try:
+    import loxi
     import loxi.of13 as ofp
     from loxi.generic_util import OFReader
 except ImportError:
@@ -88,7 +89,9 @@
         mods = [ofp.action,ofp.message,ofp.common,ofp.oxm]
         self.klasses = [klass for mod in mods
                               for klass in mod.__dict__.values()
-                              if hasattr(klass, 'show')]
+                              if isinstance(klass, type) and
+                                 issubclass(klass, loxi.OFObject) and
+                                 hasattr(klass, 'pack')]
         self.klasses.sort(key=lambda x: str(x))
 
     def test_serialization(self):
@@ -120,7 +123,7 @@
             ofp.message.table_features_stats_request,
         ]
         for klass in self.klasses:
-            if not issubclass(klass, ofp.message.Message):
+            if not issubclass(klass, ofp.message.message):
                 continue
             def fn():
                 obj = klass(xid=42)