Initial import

LoxiGen is the work of several developers, not just myself.
diff --git a/py_gen/__init__.py b/py_gen/__init__.py
new file mode 100644
index 0000000..5e4e379
--- /dev/null
+++ b/py_gen/__init__.py
@@ -0,0 +1,26 @@
+# Copyright 2013, Big Switch Networks, Inc.
+#
+# LoxiGen is licensed under the Eclipse Public License, version 1.0 (EPL), with
+# the following special exception:
+#
+# LOXI Exception
+#
+# As a special exception to the terms of the EPL, you may distribute libraries
+# generated by LoxiGen (LoxiGen Libraries) under the terms of your choice, provided
+# that copyright and licensing notices generated by LoxiGen are not altered or removed
+# from the LoxiGen Libraries and the notice provided below is (i) included in
+# the LoxiGen Libraries, if distributed in source code form and (ii) included in any
+# documentation for the LoxiGen Libraries, if distributed in binary form.
+#
+# Notice: "Copyright 2013, Big Switch Networks, Inc. This library was generated by the LoxiGen Compiler."
+#
+# You may not use this file except in compliance with the EPL or LOXI Exception. You may obtain
+# a copy of the EPL at:
+#
+# http://www.eclipse.org/legal/epl-v10.html
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# EPL for the specific language governing permissions and limitations
+# under the EPL.
diff --git a/py_gen/codegen.py b/py_gen/codegen.py
new file mode 100644
index 0000000..211c71e
--- /dev/null
+++ b/py_gen/codegen.py
@@ -0,0 +1,164 @@
+# Copyright 2013, Big Switch Networks, Inc.
+#
+# LoxiGen is licensed under the Eclipse Public License, version 1.0 (EPL), with
+# the following special exception:
+#
+# LOXI Exception
+#
+# As a special exception to the terms of the EPL, you may distribute libraries
+# generated by LoxiGen (LoxiGen Libraries) under the terms of your choice, provided
+# that copyright and licensing notices generated by LoxiGen are not altered or removed
+# from the LoxiGen Libraries and the notice provided below is (i) included in
+# the LoxiGen Libraries, if distributed in source code form and (ii) included in any
+# documentation for the LoxiGen Libraries, if distributed in binary form.
+#
+# Notice: "Copyright 2013, Big Switch Networks, Inc. This library was generated by the LoxiGen Compiler."
+#
+# You may not use this file except in compliance with the EPL or LOXI Exception. You may obtain
+# a copy of the EPL at:
+#
+# http://www.eclipse.org/legal/epl-v10.html
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# EPL for the specific language governing permissions and limitations
+# under the EPL.
+
+from collections import namedtuple
+import of_g
+import loxi_front_end.type_maps as type_maps
+import loxi_utils.loxi_utils as utils
+import util
+import oftype
+
+OFClass = namedtuple('OFClass', ['name', 'pyname',
+                                 'members', 'length_member', 'type_members',
+                                 'min_length', 'is_fixed_length'])
+Member = namedtuple('Member', ['name', 'oftype', 'offset', 'skip'])
+LengthMember = namedtuple('LengthMember', ['name', 'oftype', 'offset'])
+TypeMember = namedtuple('TypeMember', ['name', 'oftype', 'offset', 'value'])
+
+def get_type_values(cls, version):
+    """
+    Returns a map from the name of the type member to its value.
+    """
+    type_values = {}
+
+    # Primary wire type
+    if utils.class_is_message(cls):
+        type_values['version'] = 'const.OFP_VERSION'
+        type_values['type'] = util.constant_for_value(version, "ofp_type", util.primary_wire_type(cls, version))
+        if cls in type_maps.flow_mod_list:
+            type_values['_command'] = util.constant_for_value(version, "ofp_flow_mod_command",
+                                                              type_maps.flow_mod_types[version][cls[8:]])
+        if cls in type_maps.stats_request_list:
+            type_values['stats_type'] = util.constant_for_value(version, "ofp_stats_types",
+                                                                type_maps.stats_types[version][cls[3:-14]])
+        if cls in type_maps.stats_reply_list:
+            type_values['stats_type'] = util.constant_for_value(version, "ofp_stats_types",
+                                                                type_maps.stats_types[version][cls[3:-12]])
+        if type_maps.message_is_extension(cls, version):
+            type_values['experimenter'] = '%#x' % type_maps.extension_to_experimenter_id(cls)
+            type_values['subtype'] = type_maps.extension_message_to_subtype(cls, version)
+    elif utils.class_is_action(cls):
+        type_values['type'] = util.constant_for_value(version, "ofp_action_type", util.primary_wire_type(cls, version))
+        if type_maps.action_is_extension(cls, version):
+            type_values['experimenter'] = '%#x' % type_maps.extension_to_experimenter_id(cls)
+            type_values['subtype'] = type_maps.extension_action_to_subtype(cls, version)
+    elif utils.class_is_queue_prop(cls):
+        type_values['type'] = util.constant_for_value(version, "ofp_queue_properties", util.primary_wire_type(cls, version))
+
+    return type_values
+
+# Create intermediate representation
+def build_ofclasses(version):
+    blacklist = ["of_action", "of_action_header", "of_header", "of_queue_prop",
+                 "of_queue_prop_header", "of_experimenter", "of_action_experimenter"]
+    ofclasses = []
+    for cls in of_g.standard_class_order:
+        if version not in of_g.unified[cls] or cls in blacklist:
+            continue
+        unified_class = util.lookup_unified_class(cls, version)
+
+        # Name for the generated Python class
+        if utils.class_is_action(cls):
+            pyname = cls[10:]
+        else:
+            pyname = cls[3:]
+
+        type_values = get_type_values(cls, version)
+        members = []
+
+        length_member = None
+        type_members = []
+
+        for member in unified_class['members']:
+            if member['name'] in ['length', 'len']:
+                length_member = LengthMember(name=member['name'],
+                                             offset=member['offset'],
+                                             oftype=oftype.OFType(member['m_type'], version))
+            elif member['name'] in type_values:
+                type_members.append(TypeMember(name=member['name'],
+                                               offset=member['offset'],
+                                               oftype=oftype.OFType(member['m_type'], version),
+                                               value=type_values[member['name']]))
+            else:
+                # HACK ensure member names are unique
+                if member['name'] == "pad" and \
+                [x for x in members if x.name == 'pad']:
+                    m_name = "pad2"
+                else:
+                    m_name = member['name']
+                members.append(Member(name=m_name,
+                                      oftype=oftype.OFType(member['m_type'], version),
+                                      offset=member['offset'],
+                                      skip=member['name'] in of_g.skip_members))
+
+        ofclasses.append(
+            OFClass(name=cls,
+                    pyname=pyname,
+                    members=members,
+                    length_member=length_member,
+                    type_members=type_members,
+                    min_length=of_g.base_length[(cls, version)],
+                    is_fixed_length=(cls, version) in of_g.is_fixed_length))
+    return ofclasses
+
+def generate_init(out, name, version):
+    util.render_template(out, 'init.py')
+
+def generate_action(out, name, version):
+    ofclasses = [x for x in build_ofclasses(version)
+                 if utils.class_is_action(x.name)]
+    util.render_template(out, 'action.py', ofclasses=ofclasses)
+
+def generate_common(out, name, version):
+    ofclasses = [x for x in build_ofclasses(version)
+                 if not utils.class_is_message(x.name)
+                    and not utils.class_is_action(x.name)
+                    and not utils.class_is_list(x.name)]
+    util.render_template(out, 'common.py', ofclasses=ofclasses)
+
+def generate_const(out, name, version):
+    groups = {}
+    for (group, idents) in of_g.identifiers_by_group.items():
+        items = []
+        for ident in idents:
+            info = of_g.identifiers[ident]
+            if version in info["values_by_version"]:
+                items.append((info["ofp_name"], info["values_by_version"][version]))
+        if items:
+            groups[group] = items
+    util.render_template(out, 'const.py', version=version, groups=groups)
+
+def generate_message(out, name, version):
+    ofclasses = [x for x in build_ofclasses(version)
+                 if utils.class_is_message(x.name)]
+    util.render_template(out, 'message.py', ofclasses=ofclasses, version=version)
+
+def generate_pp(out, name, version):
+    util.render_template(out, 'pp.py')
+
+def generate_util(out, name, version):
+    util.render_template(out, 'util.py')
diff --git a/py_gen/oftype.py b/py_gen/oftype.py
new file mode 100644
index 0000000..ee5040c
--- /dev/null
+++ b/py_gen/oftype.py
@@ -0,0 +1,180 @@
+# Copyright 2013, Big Switch Networks, Inc.
+#
+# LoxiGen is licensed under the Eclipse Public License, version 1.0 (EPL), with
+# the following special exception:
+#
+# LOXI Exception
+#
+# As a special exception to the terms of the EPL, you may distribute libraries
+# generated by LoxiGen (LoxiGen Libraries) under the terms of your choice, provided
+# that copyright and licensing notices generated by LoxiGen are not altered or removed
+# from the LoxiGen Libraries and the notice provided below is (i) included in
+# the LoxiGen Libraries, if distributed in source code form and (ii) included in any
+# documentation for the LoxiGen Libraries, if distributed in binary form.
+#
+# Notice: "Copyright 2013, Big Switch Networks, Inc. This library was generated by the LoxiGen Compiler."
+#
+# You may not use this file except in compliance with the EPL or LOXI Exception. You may obtain
+# a copy of the EPL at:
+#
+# http://www.eclipse.org/legal/epl-v10.html
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# EPL for the specific language governing permissions and limitations
+# under the EPL.
+
+import of_g
+import loxi_utils.loxi_utils as utils
+import unittest
+
+class OFType(object):
+    """
+    Encapsulates knowledge about the OpenFlow type system.
+    """
+
+    version = None
+    base = None
+    is_array = False
+    array_length = None
+
+    def __init__(self, string, version):
+        self.version = version
+        self.array_length, self.base = utils.type_dec_to_count_base(string)
+        self.is_array = self.array_length != 1
+
+    def gen_init_expr(self):
+        if utils.class_is_list(self.base):
+            v = "[]"
+        elif self.base.find("uint") == 0 or self.base in ["char", "of_port_no_t"]:
+            v = "0"
+        elif self.base == 'of_mac_addr_t':
+            v = '[0,0,0,0,0,0]'
+        elif self.base == 'of_wc_bmap_t':
+            v = 'const.OFPFW_ALL'
+        elif self.base in ['of_octets_t', 'of_port_name_t', 'of_table_name_t',
+                           'of_desc_str_t', 'of_serial_num_t']:
+            v = '""'
+        elif self.base == 'of_match_t':
+            v = 'common.match()'
+        elif self.base == 'of_port_desc_t':
+            v = 'common.port_desc()'
+        else:
+            v = "None"
+
+        if self.is_array:
+            return "[" + ','.join([v] * self.array_length) + "]"
+        else:
+            return v
+
+    def gen_pack_expr(self, expr_expr):
+        pack_fmt = self._pack_fmt()
+        if pack_fmt and not self.is_array:
+            return 'struct.pack("!%s", %s)' % (pack_fmt, expr_expr)
+        elif pack_fmt and self.is_array:
+            return 'struct.pack("!%s%s", *%s)' % (self.array_length, pack_fmt, expr_expr)
+        elif self.base == 'of_octets_t':
+            return expr_expr
+        elif utils.class_is_list(self.base):
+            return '"".join([x.pack() for x in %s])' % expr_expr
+        elif self.base == 'of_mac_addr_t':
+            return 'struct.pack("!6B", *%s)' % expr_expr
+        elif self.base in ['of_match_t', 'of_port_desc_t']:
+            return '%s.pack()' % expr_expr
+        elif self.base == 'of_port_name_t':
+            return self._gen_string_pack_expr(16, expr_expr)
+        elif self.base == 'of_table_name_t' or self.base == 'of_serial_num_t':
+            return self._gen_string_pack_expr(32, expr_expr)
+        elif self.base == 'of_desc_str_t':
+            return self._gen_string_pack_expr(256, expr_expr)
+        else:
+            return "'TODO pack %s'" % self.base
+
+    def _gen_string_pack_expr(self, length, expr_expr):
+        return 'struct.pack("!%ds", %s)' % (length, expr_expr)
+
+    def gen_unpack_expr(self, buf_expr, offset_expr):
+        pack_fmt = self._pack_fmt()
+        if pack_fmt and not self.is_array:
+            return "struct.unpack_from('!%s', %s, %s)[0]" % (pack_fmt, buf_expr, offset_expr)
+        elif pack_fmt and self.is_array:
+            return "list(struct.unpack_from('!%d%s', %s, %s))" % (self.array_length, pack_fmt, buf_expr, offset_expr)
+        elif self.base == 'of_octets_t':
+            return "%s[%s:]" % (buf_expr, offset_expr)
+        elif self.base == 'of_mac_addr_t':
+            return "list(struct.unpack_from('!6B', %s, %s))" % (buf_expr, offset_expr)
+        elif self.base == 'of_match_t':
+            return 'common.match.unpack(buffer(%s, %s))' % (buf_expr, offset_expr)
+        elif self.base == 'of_port_desc_t':
+            return 'common.port_desc.unpack(buffer(%s, %s))' % (buf_expr, offset_expr)
+        elif self.base == 'of_list_action_t':
+            return 'action.unpack_list(buffer(%s, %s))' % (buf_expr, offset_expr)
+        elif self.base == 'of_list_flow_stats_entry_t':
+            return 'common.unpack_list_flow_stats_entry(buffer(%s, %s))' % (buf_expr, offset_expr)
+        elif self.base == 'of_list_queue_prop_t':
+            return 'common.unpack_list_queue_prop(buffer(%s, %s))' % (buf_expr, offset_expr)
+        elif self.base == 'of_list_packet_queue_t':
+            return 'common.unpack_list_packet_queue(buffer(%s, %s))' % (buf_expr, offset_expr)
+        elif self.base == 'of_port_name_t':
+            return self._gen_string_unpack_expr(16, buf_expr, offset_expr)
+        elif self.base == 'of_table_name_t' or self.base == 'of_serial_num_t':
+            return self._gen_string_unpack_expr(32, buf_expr, offset_expr)
+        elif self.base == 'of_desc_str_t':
+            return self._gen_string_unpack_expr(256, buf_expr, offset_expr)
+        elif utils.class_is_list(self.base):
+            element_cls = utils.list_to_entry_type(self.base)[:-2]
+            if ((element_cls, self.version) in of_g.is_fixed_length):
+                klass_name = self.base[8:-2]
+                element_size, = of_g.base_length[(element_cls, self.version)],
+                return 'util.unpack_array(common.%s.unpack, %d, buffer(%s, %s))' % (klass_name, element_size, buf_expr, offset_expr)
+            else:
+                return "None # TODO unpack list %s" % self.base
+        else:
+            return "None # TODO unpack %s" % self.base
+
+    def _gen_string_unpack_expr(self, length, buf_expr, offset_expr):
+        return 'str(buffer(%s, %s, %d)).rstrip("\\x00")' % (buf_expr, offset_expr, length)
+
+    def _pack_fmt(self):
+        if self.base == "char":
+            return "B"
+        if self.base == "uint8_t":
+            return "B"
+        if self.base == "uint16_t":
+            return "H"
+        if self.base == "uint32_t":
+            return "L"
+        if self.base == "uint64_t":
+            return "Q"
+        if self.base == "of_port_no_t":
+            if self.version == of_g.VERSION_1_0:
+                return "H"
+            else:
+                return "L"
+        if self.base == "of_fm_cmd_t":
+            if self.version == of_g.VERSION_1_0:
+                return "H"
+            else:
+                return "B"
+        if self.base in ["of_wc_bmap_t", "of_match_bmap_t"]:
+            if self.version in [of_g.VERSION_1_0, of_g.VERSION_1_1]:
+                return "L"
+            else:
+                return "Q"
+        return None
+
+class TestOFType(unittest.TestCase):
+    def test_init(self):
+        from oftype import OFType
+        self.assertEquals("None", OFType("of_list_action_t", 1).gen_init_expr())
+        self.assertEquals("[0,0,0]", OFType("uint32_t[3]", 1).gen_init_expr())
+
+    def test_pack(self):
+        self.assertEquals('struct.pack("!16s", "foo")', OFType("of_port_name_t", 1).gen_pack_expr('"foo"'))
+
+    def test_unpack(self):
+        self.assertEquals('str(buffer(buf, 8, 16)).rstrip("\\x00")', OFType("of_port_name_t", 1).gen_unpack_expr('buf', 8))
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/py_gen/templates/.gitignore b/py_gen/templates/.gitignore
new file mode 100644
index 0000000..81fc9b5
--- /dev/null
+++ b/py_gen/templates/.gitignore
@@ -0,0 +1,2 @@
+# Tenjin cache files
+/*.cache
diff --git a/py_gen/templates/_autogen.py b/py_gen/templates/_autogen.py
new file mode 100644
index 0000000..520ad9e
--- /dev/null
+++ b/py_gen/templates/_autogen.py
@@ -0,0 +1,30 @@
+:: # Copyright 2013, Big Switch Networks, Inc.
+:: #
+:: # LoxiGen is licensed under the Eclipse Public License, version 1.0 (EPL), with
+:: # the following special exception:
+:: #
+:: # LOXI Exception
+:: #
+:: # As a special exception to the terms of the EPL, you may distribute libraries
+:: # generated by LoxiGen (LoxiGen Libraries) under the terms of your choice, provided
+:: # that copyright and licensing notices generated by LoxiGen are not altered or removed
+:: # from the LoxiGen Libraries and the notice provided below is (i) included in
+:: # the LoxiGen Libraries, if distributed in source code form and (ii) included in any
+:: # documentation for the LoxiGen Libraries, if distributed in binary form.
+:: #
+:: # Notice: "Copyright 2013, Big Switch Networks, Inc. This library was generated by the LoxiGen Compiler."
+:: #
+:: # You may not use this file except in compliance with the EPL or LOXI Exception. You may obtain
+:: # a copy of the EPL at:
+:: #
+:: # http://www.eclipse.org/legal/epl-v10.html
+:: #
+:: # Unless required by applicable law or agreed to in writing, software
+:: # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+:: # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+:: # EPL for the specific language governing permissions and limitations
+:: # under the EPL.
+::
+:: import inspect, os
+# Automatically generated by LOXI from template #{os.path.basename(inspect.stack()[3][1])}
+# Do not modify
diff --git a/py_gen/templates/_copyright.py b/py_gen/templates/_copyright.py
new file mode 100644
index 0000000..24dc1b6
--- /dev/null
+++ b/py_gen/templates/_copyright.py
@@ -0,0 +1,30 @@
+:: # Copyright 2013, Big Switch Networks, Inc.
+:: #
+:: # LoxiGen is licensed under the Eclipse Public License, version 1.0 (EPL), with
+:: # the following special exception:
+:: #
+:: # LOXI Exception
+:: #
+:: # As a special exception to the terms of the EPL, you may distribute libraries
+:: # generated by LoxiGen (LoxiGen Libraries) under the terms of your choice, provided
+:: # that copyright and licensing notices generated by LoxiGen are not altered or removed
+:: # from the LoxiGen Libraries and the notice provided below is (i) included in
+:: # the LoxiGen Libraries, if distributed in source code form and (ii) included in any
+:: # documentation for the LoxiGen Libraries, if distributed in binary form.
+:: #
+:: # Notice: "Copyright 2013, Big Switch Networks, Inc. This library was generated by the LoxiGen Compiler."
+:: #
+:: # You may not use this file except in compliance with the EPL or LOXI Exception. You may obtain
+:: # a copy of the EPL at:
+:: #
+:: # http://www.eclipse.org/legal/epl-v10.html
+:: #
+:: # Unless required by applicable law or agreed to in writing, software
+:: # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+:: # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+:: # EPL for the specific language governing permissions and limitations
+:: # under the EPL.
+::
+# Copyright (c) 2008 The Board of Trustees of The Leland Stanford Junior University
+# Copyright (c) 2011, 2012 Open Networking Foundation
+# Copyright (c) 2012, 2013 Big Switch Networks, Inc.
diff --git a/py_gen/templates/_pack.py b/py_gen/templates/_pack.py
new file mode 100644
index 0000000..0d50a38
--- /dev/null
+++ b/py_gen/templates/_pack.py
@@ -0,0 +1,47 @@
+:: # Copyright 2013, Big Switch Networks, Inc.
+:: #
+:: # LoxiGen is licensed under the Eclipse Public License, version 1.0 (EPL), with
+:: # the following special exception:
+:: #
+:: # LOXI Exception
+:: #
+:: # As a special exception to the terms of the EPL, you may distribute libraries
+:: # generated by LoxiGen (LoxiGen Libraries) under the terms of your choice, provided
+:: # that copyright and licensing notices generated by LoxiGen are not altered or removed
+:: # from the LoxiGen Libraries and the notice provided below is (i) included in
+:: # the LoxiGen Libraries, if distributed in source code form and (ii) included in any
+:: # documentation for the LoxiGen Libraries, if distributed in binary form.
+:: #
+:: # Notice: "Copyright 2013, Big Switch Networks, Inc. This library was generated by the LoxiGen Compiler."
+:: #
+:: # You may not use this file except in compliance with the EPL or LOXI Exception. You may obtain
+:: # a copy of the EPL at:
+:: #
+:: # http://www.eclipse.org/legal/epl-v10.html
+:: #
+:: # Unless required by applicable law or agreed to in writing, software
+:: # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+:: # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+:: # EPL for the specific language governing permissions and limitations
+:: # under the EPL.
+::
+:: # TODO coalesce format strings
+:: all_members = ofclass.members[:]
+:: if ofclass.length_member: all_members.append(ofclass.length_member)
+:: all_members.extend(ofclass.type_members)
+:: all_members.sort(key=lambda x: x.offset)
+:: length_member_index = None
+:: index = 0
+:: for m in all_members:
+::     if m == ofclass.length_member:
+::         length_member_index = index
+        packed.append(${m.oftype.gen_pack_expr('0')}) # placeholder for ${m.name} at index ${length_member_index}
+::     else:
+        packed.append(${m.oftype.gen_pack_expr('self.' + m.name)})
+::     #endif
+::     index += 1
+:: #endfor
+:: if length_member_index != None:
+        length = sum([len(x) for x in packed])
+        packed[${length_member_index}] = ${ofclass.length_member.oftype.gen_pack_expr('length')}
+:: #endif
diff --git a/py_gen/templates/_pack_packet_out.py b/py_gen/templates/_pack_packet_out.py
new file mode 100644
index 0000000..ad8b827
--- /dev/null
+++ b/py_gen/templates/_pack_packet_out.py
@@ -0,0 +1,39 @@
+:: # Copyright 2013, Big Switch Networks, Inc.
+:: #
+:: # LoxiGen is licensed under the Eclipse Public License, version 1.0 (EPL), with
+:: # the following special exception:
+:: #
+:: # LOXI Exception
+:: #
+:: # As a special exception to the terms of the EPL, you may distribute libraries
+:: # generated by LoxiGen (LoxiGen Libraries) under the terms of your choice, provided
+:: # that copyright and licensing notices generated by LoxiGen are not altered or removed
+:: # from the LoxiGen Libraries and the notice provided below is (i) included in
+:: # the LoxiGen Libraries, if distributed in source code form and (ii) included in any
+:: # documentation for the LoxiGen Libraries, if distributed in binary form.
+:: #
+:: # Notice: "Copyright 2013, Big Switch Networks, Inc. This library was generated by the LoxiGen Compiler."
+:: #
+:: # You may not use this file except in compliance with the EPL or LOXI Exception. You may obtain
+:: # a copy of the EPL at:
+:: #
+:: # http://www.eclipse.org/legal/epl-v10.html
+:: #
+:: # Unless required by applicable law or agreed to in writing, software
+:: # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+:: # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+:: # EPL for the specific language governing permissions and limitations
+:: # under the EPL.
+::
+        packed.append(struct.pack("!B", self.version))
+        packed.append(struct.pack("!B", self.type))
+        packed.append(struct.pack("!H", 0)) # placeholder for length at index 3
+        packed.append(struct.pack("!L", self.xid))
+        packed.append(struct.pack("!L", self.buffer_id))
+        packed.append(struct.pack("!H", self.in_port))
+        packed_actions = "".join([x.pack() for x in self.actions])
+        packed.append(struct.pack("!H", len(packed_actions)))
+        packed.append(packed_actions)
+        packed.append(self.data)
+        length = sum([len(x) for x in packed])
+        packed[2] = struct.pack("!H", length)
diff --git a/py_gen/templates/_pretty_print.py b/py_gen/templates/_pretty_print.py
new file mode 100644
index 0000000..604cd94
--- /dev/null
+++ b/py_gen/templates/_pretty_print.py
@@ -0,0 +1,61 @@
+:: # Copyright 2013, Big Switch Networks, Inc.
+:: #
+:: # LoxiGen is licensed under the Eclipse Public License, version 1.0 (EPL), with
+:: # the following special exception:
+:: #
+:: # LOXI Exception
+:: #
+:: # As a special exception to the terms of the EPL, you may distribute libraries
+:: # generated by LoxiGen (LoxiGen Libraries) under the terms of your choice, provided
+:: # that copyright and licensing notices generated by LoxiGen are not altered or removed
+:: # from the LoxiGen Libraries and the notice provided below is (i) included in
+:: # the LoxiGen Libraries, if distributed in source code form and (ii) included in any
+:: # documentation for the LoxiGen Libraries, if distributed in binary form.
+:: #
+:: # Notice: "Copyright 2013, Big Switch Networks, Inc. This library was generated by the LoxiGen Compiler."
+:: #
+:: # You may not use this file except in compliance with the EPL or LOXI Exception. You may obtain
+:: # a copy of the EPL at:
+:: #
+:: # http://www.eclipse.org/legal/epl-v10.html
+:: #
+:: # Unless required by applicable law or agreed to in writing, software
+:: # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+:: # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+:: # EPL for the specific language governing permissions and limitations
+:: # under the EPL.
+::
+        q.text("${ofclass.pyname} {")
+        with q.group():
+            with q.indent(2):
+                q.breakable()
+:: first = True
+:: for m in ofclass.members:
+:: if m.name == 'actions_len': continue
+:: if not first:
+                q.text(","); q.breakable()
+:: else:
+:: first = False
+:: #endif
+                q.text("${m.name} = ");
+:: if m.name == "xid":
+                if self.${m.name} != None:
+                    q.text("%#x" % self.${m.name})
+                else:
+                    q.text('None')
+:: elif m.oftype.base == 'of_mac_addr_t':
+                q.text(util.pretty_mac(self.${m.name}))
+:: elif m.oftype.base == 'uint32_t' and m.name.startswith("ipv4"):
+                q.text(util.pretty_ipv4(self.${m.name}))
+:: elif m.oftype.base == 'of_wc_bmap_t':
+                q.text(util.pretty_wildcards(self.${m.name}))
+:: elif m.oftype.base == 'of_port_no_t':
+                q.text(util.pretty_port(self.${m.name}))
+:: elif m.oftype.base.startswith("uint") and not m.oftype.is_array:
+                q.text("%#x" % self.${m.name})
+:: else:
+                q.pp(self.${m.name})
+:: #endif
+:: #endfor
+            q.breakable()
+        q.text('}')
diff --git a/py_gen/templates/_unpack.py b/py_gen/templates/_unpack.py
new file mode 100644
index 0000000..173ebb5
--- /dev/null
+++ b/py_gen/templates/_unpack.py
@@ -0,0 +1,49 @@
+:: # Copyright 2013, Big Switch Networks, Inc.
+:: #
+:: # LoxiGen is licensed under the Eclipse Public License, version 1.0 (EPL), with
+:: # the following special exception:
+:: #
+:: # LOXI Exception
+:: #
+:: # As a special exception to the terms of the EPL, you may distribute libraries
+:: # generated by LoxiGen (LoxiGen Libraries) under the terms of your choice, provided
+:: # that copyright and licensing notices generated by LoxiGen are not altered or removed
+:: # from the LoxiGen Libraries and the notice provided below is (i) included in
+:: # the LoxiGen Libraries, if distributed in source code form and (ii) included in any
+:: # documentation for the LoxiGen Libraries, if distributed in binary form.
+:: #
+:: # Notice: "Copyright 2013, Big Switch Networks, Inc. This library was generated by the LoxiGen Compiler."
+:: #
+:: # You may not use this file except in compliance with the EPL or LOXI Exception. You may obtain
+:: # a copy of the EPL at:
+:: #
+:: # http://www.eclipse.org/legal/epl-v10.html
+:: #
+:: # Unless required by applicable law or agreed to in writing, software
+:: # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+:: # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+:: # EPL for the specific language governing permissions and limitations
+:: # under the EPL.
+::
+:: # TODO coalesce format strings
+:: all_members = ofclass.members[:]
+:: if ofclass.length_member: all_members.append(ofclass.length_member)
+:: all_members.extend(ofclass.type_members)
+:: all_members.sort(key=lambda x: x.offset)
+:: for m in all_members:
+::     unpack_expr = m.oftype.gen_unpack_expr('buf', m.offset)
+::     if m == ofclass.length_member:
+        _length = ${unpack_expr}
+        assert(_length == len(buf))
+:: if ofclass.is_fixed_length:
+        if _length != ${ofclass.min_length}: raise loxi.ProtocolError("${ofclass.pyname} length is %d, should be ${ofclass.min_length}" % _length)
+:: else:
+        if _length < ${ofclass.min_length}: raise loxi.ProtocolError("${ofclass.pyname} length is %d, should be at least ${ofclass.min_length}" % _length)
+:: #endif
+::     elif m in ofclass.type_members:
+        ${m.name} = ${unpack_expr}
+        assert(${m.name} == ${m.value})
+::     else:
+        obj.${m.name} = ${unpack_expr}
+::     #endif
+:: #endfor
diff --git a/py_gen/templates/_unpack_packet_out.py b/py_gen/templates/_unpack_packet_out.py
new file mode 100644
index 0000000..b97e829
--- /dev/null
+++ b/py_gen/templates/_unpack_packet_out.py
@@ -0,0 +1,40 @@
+:: # Copyright 2013, Big Switch Networks, Inc.
+:: #
+:: # LoxiGen is licensed under the Eclipse Public License, version 1.0 (EPL), with
+:: # the following special exception:
+:: #
+:: # LOXI Exception
+:: #
+:: # As a special exception to the terms of the EPL, you may distribute libraries
+:: # generated by LoxiGen (LoxiGen Libraries) under the terms of your choice, provided
+:: # that copyright and licensing notices generated by LoxiGen are not altered or removed
+:: # from the LoxiGen Libraries and the notice provided below is (i) included in
+:: # the LoxiGen Libraries, if distributed in source code form and (ii) included in any
+:: # documentation for the LoxiGen Libraries, if distributed in binary form.
+:: #
+:: # Notice: "Copyright 2013, Big Switch Networks, Inc. This library was generated by the LoxiGen Compiler."
+:: #
+:: # You may not use this file except in compliance with the EPL or LOXI Exception. You may obtain
+:: # a copy of the EPL at:
+:: #
+:: # http://www.eclipse.org/legal/epl-v10.html
+:: #
+:: # Unless required by applicable law or agreed to in writing, software
+:: # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+:: # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+:: # EPL for the specific language governing permissions and limitations
+:: # under the EPL.
+::
+        version = struct.unpack_from('!B', buf, 0)[0]
+        assert(version == const.OFP_VERSION)
+        type = struct.unpack_from('!B', buf, 1)[0]
+        assert(type == const.OFPT_PACKET_OUT)
+        _length = struct.unpack_from('!H', buf, 2)[0]
+        assert(_length == len(buf))
+        if _length < 16: raise loxi.ProtocolError("packet_out length is %d, should be at least 16" % _length)
+        obj.xid = struct.unpack_from('!L', buf, 4)[0]
+        obj.buffer_id = struct.unpack_from('!L', buf, 8)[0]
+        obj.in_port = struct.unpack_from('!H', buf, 12)[0]
+        actions_len = struct.unpack_from('!H', buf, 14)[0]
+        obj.actions = action.unpack_list(buffer(buf, 16, actions_len))
+        obj.data = str(buffer(buf, 16+actions_len))
diff --git a/py_gen/templates/action.py b/py_gen/templates/action.py
new file mode 100644
index 0000000..439e56f
--- /dev/null
+++ b/py_gen/templates/action.py
@@ -0,0 +1,145 @@
+:: # Copyright 2013, Big Switch Networks, Inc.
+:: #
+:: # LoxiGen is licensed under the Eclipse Public License, version 1.0 (EPL), with
+:: # the following special exception:
+:: #
+:: # LOXI Exception
+:: #
+:: # As a special exception to the terms of the EPL, you may distribute libraries
+:: # generated by LoxiGen (LoxiGen Libraries) under the terms of your choice, provided
+:: # that copyright and licensing notices generated by LoxiGen are not altered or removed
+:: # from the LoxiGen Libraries and the notice provided below is (i) included in
+:: # the LoxiGen Libraries, if distributed in source code form and (ii) included in any
+:: # documentation for the LoxiGen Libraries, if distributed in binary form.
+:: #
+:: # Notice: "Copyright 2013, Big Switch Networks, Inc. This library was generated by the LoxiGen Compiler."
+:: #
+:: # You may not use this file except in compliance with the EPL or LOXI Exception. You may obtain
+:: # a copy of the EPL at:
+:: #
+:: # http://www.eclipse.org/legal/epl-v10.html
+:: #
+:: # Unless required by applicable law or agreed to in writing, software
+:: # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+:: # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+:: # EPL for the specific language governing permissions and limitations
+:: # under the EPL.
+::
+:: import itertools
+:: include('_copyright.py')
+
+:: include('_autogen.py')
+
+import struct
+import const
+import util
+import loxi
+
+def unpack_list(buf):
+    if len(buf) % 8 != 0: raise loxi.ProtocolError("action list length not a multiple of 8")
+    actions = []
+    offset = 0
+    while offset < len(buf):
+        type, length = struct.unpack_from("!HH", buf, offset)
+        if length == 0: raise loxi.ProtocolError("action length is 0")
+        if length % 8 != 0: raise loxi.ProtocolError("action length not a multiple of 8")
+        if offset + length > len(buf): raise loxi.ProtocolError("action length overruns list length")
+        parser = parsers.get(type)
+        if not parser: raise loxi.ProtocolError("unknown action type %d" % type)
+        actions.append(parser(buffer(buf, offset, length)))
+        offset += length
+    return actions
+
+class Action(object):
+    type = None # override in subclass
+    pass
+
+:: for ofclass in ofclasses:
+:: nonskip_members = [m for m in ofclass.members if not m.skip]
+class ${ofclass.pyname}(Action):
+:: for m in ofclass.type_members:
+    ${m.name} = ${m.value}
+:: #endfor
+
+    def __init__(self, ${', '.join(["%s=None" % m.name for m in nonskip_members])}):
+:: for m in nonskip_members:
+        if ${m.name} != None:
+            self.${m.name} = ${m.name}
+        else:
+            self.${m.name} = ${m.oftype.gen_init_expr()}
+:: #endfor
+
+    def pack(self):
+        packed = []
+:: include("_pack.py", ofclass=ofclass)
+        return ''.join(packed)
+
+    @staticmethod
+    def unpack(buf):
+        obj = ${ofclass.pyname}()
+:: include("_unpack.py", ofclass=ofclass)
+        return obj
+
+    def __eq__(self, other):
+        if type(self) != type(other): return False
+        if self.type != other.type: return False
+:: for m in nonskip_members:
+        if self.${m.name} != other.${m.name}: return False
+:: #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)
+
+:: #endfor
+
+def parse_vendor(buf):
+    if len(buf) < 16:
+        raise loxi.ProtocolError("experimenter action too short")
+
+    experimenter, = struct.unpack_from("!L", buf, 4)
+    if experimenter == 0x005c16c7: # Big Switch Networks
+        subtype, = struct.unpack_from("!L", buf, 8)
+    elif experimenter == 0x00002320: # Nicira
+        subtype, = struct.unpack_from("!H", buf, 8)
+    else:
+        raise loxi.ProtocolError("unexpected experimenter id %#x" % experimenter)
+
+    if subtype in experimenter_parsers[experimenter]:
+        return experimenter_parsers[experimenter][subtype](buf)
+    else:
+        raise loxi.ProtocolError("unexpected BSN experimenter subtype %#x" % subtype)
+
+parsers = {
+:: sort_key = lambda x: x.type_members[0].value
+:: msgtype_groups = itertools.groupby(sorted(ofclasses, key=sort_key), sort_key)
+:: for (k, v) in msgtype_groups:
+:: v = list(v)
+:: if len(v) == 1:
+    ${k} : ${v[0].pyname}.unpack,
+:: else:
+    ${k} : parse_${k[12:].lower()},
+:: #endif
+:: #endfor
+}
+
+:: experimenter_ofclasses = [x for x in ofclasses if x.type_members[0].value == 'const.OFPAT_VENDOR']
+:: sort_key = lambda x: x.type_members[1].value
+:: experimenter_ofclasses.sort(key=sort_key)
+:: grouped = itertools.groupby(experimenter_ofclasses, sort_key)
+experimenter_parsers = {
+:: for (experimenter, v) in grouped:
+    ${experimenter} : {
+:: for ofclass in v:
+        ${ofclass.type_members[2].value}: ${ofclass.pyname}.unpack,
+:: #endfor
+    },
+:: #endfor
+}
diff --git a/py_gen/templates/common.py b/py_gen/templates/common.py
new file mode 100644
index 0000000..08fb65d
--- /dev/null
+++ b/py_gen/templates/common.py
@@ -0,0 +1,125 @@
+:: # Copyright 2013, Big Switch Networks, Inc.
+:: #
+:: # LoxiGen is licensed under the Eclipse Public License, version 1.0 (EPL), with
+:: # the following special exception:
+:: #
+:: # LOXI Exception
+:: #
+:: # As a special exception to the terms of the EPL, you may distribute libraries
+:: # generated by LoxiGen (LoxiGen Libraries) under the terms of your choice, provided
+:: # that copyright and licensing notices generated by LoxiGen are not altered or removed
+:: # from the LoxiGen Libraries and the notice provided below is (i) included in
+:: # the LoxiGen Libraries, if distributed in source code form and (ii) included in any
+:: # documentation for the LoxiGen Libraries, if distributed in binary form.
+:: #
+:: # Notice: "Copyright 2013, Big Switch Networks, Inc. This library was generated by the LoxiGen Compiler."
+:: #
+:: # You may not use this file except in compliance with the EPL or LOXI Exception. You may obtain
+:: # a copy of the EPL at:
+:: #
+:: # http://www.eclipse.org/legal/epl-v10.html
+:: #
+:: # Unless required by applicable law or agreed to in writing, software
+:: # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+:: # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+:: # EPL for the specific language governing permissions and limitations
+:: # under the EPL.
+::
+:: include('_copyright.py')
+
+:: include('_autogen.py')
+
+import sys
+import struct
+import action
+import const
+import util
+
+# HACK make this module visible as 'common' to simplify code generation
+common = sys.modules[__name__]
+
+def unpack_list_flow_stats_entry(buf):
+    entries = []
+    offset = 0
+    while offset < len(buf):
+        length, = struct.unpack_from("!H", buf, offset)
+        if length == 0: raise loxi.ProtocolError("entry length is 0")
+        if offset + length > len(buf): raise loxi.ProtocolError("entry length overruns list length")
+        entries.append(flow_stats_entry.unpack(buffer(buf, offset, length)))
+        offset += length
+    return entries
+
+def unpack_list_queue_prop(buf):
+    entries = []
+    offset = 0
+    while offset < len(buf):
+        type, length, = struct.unpack_from("!HH", buf, offset)
+        if length == 0: raise loxi.ProtocolError("entry length is 0")
+        if offset + length > len(buf): raise loxi.ProtocolError("entry length overruns list length")
+        if type == const.OFPQT_MIN_RATE:
+            entry = queue_prop_min_rate.unpack(buffer(buf, offset, length))
+        else:
+            raise loxi.ProtocolError("unknown queue prop %d" % type)
+        entries.append(entry)
+        offset += length
+    return entries
+
+def unpack_list_packet_queue(buf):
+    entries = []
+    offset = 0
+    while offset < len(buf):
+        _, length, = struct.unpack_from("!LH", buf, offset)
+        if length == 0: raise loxi.ProtocolError("entry length is 0")
+        if offset + length > len(buf): raise loxi.ProtocolError("entry length overruns list length")
+        entries.append(packet_queue.unpack(buffer(buf, offset, length)))
+        offset += length
+    return entries
+
+:: for ofclass in ofclasses:
+class ${ofclass.pyname}(object):
+:: for m in ofclass.type_members:
+    ${m.name} = ${m.value}
+:: #endfor
+
+    def __init__(self, ${', '.join(["%s=None" % m.name for m in ofclass.members])}):
+:: for m in ofclass.members:
+        if ${m.name} != None:
+            self.${m.name} = ${m.name}
+        else:
+            self.${m.name} = ${m.oftype.gen_init_expr()}
+:: #endfor
+
+    def pack(self):
+        packed = []
+:: include("_pack.py", ofclass=ofclass)
+        return ''.join(packed)
+
+    @staticmethod
+    def unpack(buf):
+        assert(len(buf) >= ${ofclass.min_length}) # Should be verified by caller
+        obj = ${ofclass.pyname}()
+:: include("_unpack.py", ofclass=ofclass)
+        return obj
+
+    def __eq__(self, other):
+        if type(self) != type(other): return False
+:: for m in ofclass.members:
+        if self.${m.name} != other.${m.name}: return False
+:: #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)
+
+:: if ofclass.name.startswith("of_match_v"):
+match = ${ofclass.pyname}
+
+:: #endif
+:: #endfor
diff --git a/py_gen/templates/const.py b/py_gen/templates/const.py
new file mode 100644
index 0000000..c5a0e93
--- /dev/null
+++ b/py_gen/templates/const.py
@@ -0,0 +1,66 @@
+:: # Copyright 2013, Big Switch Networks, Inc.
+:: #
+:: # LoxiGen is licensed under the Eclipse Public License, version 1.0 (EPL), with
+:: # the following special exception:
+:: #
+:: # LOXI Exception
+:: #
+:: # As a special exception to the terms of the EPL, you may distribute libraries
+:: # generated by LoxiGen (LoxiGen Libraries) under the terms of your choice, provided
+:: # that copyright and licensing notices generated by LoxiGen are not altered or removed
+:: # from the LoxiGen Libraries and the notice provided below is (i) included in
+:: # the LoxiGen Libraries, if distributed in source code form and (ii) included in any
+:: # documentation for the LoxiGen Libraries, if distributed in binary form.
+:: #
+:: # Notice: "Copyright 2013, Big Switch Networks, Inc. This library was generated by the LoxiGen Compiler."
+:: #
+:: # You may not use this file except in compliance with the EPL or LOXI Exception. You may obtain
+:: # a copy of the EPL at:
+:: #
+:: # http://www.eclipse.org/legal/epl-v10.html
+:: #
+:: # Unless required by applicable law or agreed to in writing, software
+:: # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+:: # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+:: # EPL for the specific language governing permissions and limitations
+:: # under the EPL.
+::
+:: blacklisted_map_groups = ['macro_definitions']
+:: blacklisted_map_idents = ['OFPFW_NW_DST_BITS', 'OFPFW_NW_SRC_BITS',
+::     'OFPFW_NW_SRC_SHIFT', 'OFPFW_NW_DST_SHIFT', 'OFPFW_NW_SRC_ALL',
+::     'OFPFW_NW_SRC_MASK', 'OFPFW_NW_DST_ALL', 'OFPFW_NW_DST_MASK',
+::     'OFPFW_ALL']
+:: include('_copyright.py')
+
+:: include('_autogen.py')
+
+OFP_VERSION = ${version}
+
+:: for (group, idents) in sorted(groups.items()):
+::    idents.sort(key=lambda (ident, value): eval(value))
+# Identifiers from group ${group}
+::    for (ident, value) in idents:
+::        if version == 1 and ident.startswith('OFPP_'):
+::        # HACK loxi converts these to 32-bit
+${ident} = ${"%#x" % (int(value, 16) & 0xffff)}
+::        else:
+${ident} = ${value}
+::        #endif
+::    #endfor
+
+::    if group not in blacklisted_map_groups:
+${group}_map = {
+::        for (ident, value) in idents:
+::            if ident in blacklisted_map_idents:
+::                pass
+::            elif version == 1 and ident.startswith('OFPP_'):
+::                # HACK loxi converts these to 32-bit
+    ${"%#x" % (int(value, 16) & 0xffff)}: ${repr(ident)},
+::        else:
+    ${value}: ${repr(ident)},
+::            #endif
+::        #endfor
+}
+
+::     #endif
+:: #endfor
diff --git a/py_gen/templates/init.py b/py_gen/templates/init.py
new file mode 100644
index 0000000..abc8d70
--- /dev/null
+++ b/py_gen/templates/init.py
@@ -0,0 +1,35 @@
+:: # Copyright 2013, Big Switch Networks, Inc.
+:: #
+:: # LoxiGen is licensed under the Eclipse Public License, version 1.0 (EPL), with
+:: # the following special exception:
+:: #
+:: # LOXI Exception
+:: #
+:: # As a special exception to the terms of the EPL, you may distribute libraries
+:: # generated by LoxiGen (LoxiGen Libraries) under the terms of your choice, provided
+:: # that copyright and licensing notices generated by LoxiGen are not altered or removed
+:: # from the LoxiGen Libraries and the notice provided below is (i) included in
+:: # the LoxiGen Libraries, if distributed in source code form and (ii) included in any
+:: # documentation for the LoxiGen Libraries, if distributed in binary form.
+:: #
+:: # Notice: "Copyright 2013, Big Switch Networks, Inc. This library was generated by the LoxiGen Compiler."
+:: #
+:: # You may not use this file except in compliance with the EPL or LOXI Exception. You may obtain
+:: # a copy of the EPL at:
+:: #
+:: # http://www.eclipse.org/legal/epl-v10.html
+:: #
+:: # Unless required by applicable law or agreed to in writing, software
+:: # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+:: # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+:: # EPL for the specific language governing permissions and limitations
+:: # under the EPL.
+::
+:: include('_copyright.py')
+
+:: include('_autogen.py')
+
+import action, common, const, message
+from const import *
+from common import *
+from loxi import ProtocolError
diff --git a/py_gen/templates/message.py b/py_gen/templates/message.py
new file mode 100644
index 0000000..71a2871
--- /dev/null
+++ b/py_gen/templates/message.py
@@ -0,0 +1,219 @@
+:: # Copyright 2013, Big Switch Networks, Inc.
+:: #
+:: # LoxiGen is licensed under the Eclipse Public License, version 1.0 (EPL), with
+:: # the following special exception:
+:: #
+:: # LOXI Exception
+:: #
+:: # As a special exception to the terms of the EPL, you may distribute libraries
+:: # generated by LoxiGen (LoxiGen Libraries) under the terms of your choice, provided
+:: # that copyright and licensing notices generated by LoxiGen are not altered or removed
+:: # from the LoxiGen Libraries and the notice provided below is (i) included in
+:: # the LoxiGen Libraries, if distributed in source code form and (ii) included in any
+:: # documentation for the LoxiGen Libraries, if distributed in binary form.
+:: #
+:: # Notice: "Copyright 2013, Big Switch Networks, Inc. This library was generated by the LoxiGen Compiler."
+:: #
+:: # You may not use this file except in compliance with the EPL or LOXI Exception. You may obtain
+:: # a copy of the EPL at:
+:: #
+:: # http::: #www.eclipse.org/legal/epl-v10.html
+:: #
+:: # Unless required by applicable law or agreed to in writing, software
+:: # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+:: # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+:: # EPL for the specific language governing permissions and limitations
+:: # under the EPL.
+::
+:: import itertools
+:: include('_copyright.py')
+
+:: include('_autogen.py')
+
+import struct
+import loxi
+import const
+import common
+import action # for unpack_list
+import util
+
+class Message(object):
+    version = const.OFP_VERSION
+    type = None # override in subclass
+    xid = None
+
+:: for ofclass in ofclasses:
+:: nonskip_members = [m for m in ofclass.members if not m.skip]
+class ${ofclass.pyname}(Message):
+:: for m in ofclass.type_members:
+    ${m.name} = ${m.value}
+:: #endfor
+
+    def __init__(self, ${', '.join(["%s=None" % m.name for m in nonskip_members])}):
+        self.xid = xid
+:: for m in [x for x in nonskip_members if x.name != 'xid']:
+        if ${m.name} != None:
+            self.${m.name} = ${m.name}
+        else:
+            self.${m.name} = ${m.oftype.gen_init_expr()}
+:: #endfor
+
+    def pack(self):
+        packed = []
+:: if ofclass.name == 'of_packet_out':
+:: include('_pack_packet_out.py', ofclass=ofclass)
+:: else:
+:: include('_pack.py', ofclass=ofclass)
+:: #endif
+        return ''.join(packed)
+
+    @staticmethod
+    def unpack(buf):
+        if len(buf) < 8: raise loxi.ProtocolError("buffer too short to contain an OpenFlow message")
+        obj = ${ofclass.pyname}()
+:: if ofclass.name == 'of_packet_out':
+:: include('_unpack_packet_out.py', ofclass=ofclass)
+:: else:
+:: include('_unpack.py', ofclass=ofclass)
+:: #endif
+        return obj
+
+    def __eq__(self, other):
+        if type(self) != type(other): return False
+        if self.version != other.version: return False
+        if self.type != other.type: return False
+:: for m in nonskip_members:
+        if self.${m.name} != other.${m.name}: return False
+:: #endfor
+        return True
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __str__(self):
+        return self.show()
+
+    def show(self):
+        import loxi.pp
+        return loxi.pp.pp(self)
+
+    def pretty_print(self, q):
+:: include('_pretty_print.py', ofclass=ofclass)
+
+:: #endfor
+
+def parse_header(buf):
+    if len(buf) < 8:
+        raise loxi.ProtocolError("too short to be an OpenFlow message")
+    return struct.unpack_from("!BBHL", buf)
+
+def parse_message(buf):
+    msg_ver, msg_type, msg_len, msg_xid = parse_header(buf)
+    if msg_ver != const.OFP_VERSION and msg_type != ofp.OFPT_HELLO:
+        raise loxi.ProtocolError("wrong OpenFlow version")
+    if len(buf) != msg_len:
+        raise loxi.ProtocolError("incorrect message size")
+    if msg_type in parsers:
+        return parsers[msg_type](buf)
+    else:
+        raise loxi.ProtocolError("unexpected message type")
+
+:: # TODO fix for OF 1.1+
+def parse_flow_mod(buf):
+    if len(buf) < 56 + 2:
+        raise loxi.ProtocolError("message too short")
+    cmd, = struct.unpack_from("!H", buf, 56)
+    if cmd in flow_mod_parsers:
+        return flow_mod_parsers[cmd](buf)
+    else:
+        raise loxi.ProtocolError("unexpected flow mod cmd %u" % cmd)
+
+def parse_stats_reply(buf):
+    if len(buf) < 8 + 2:
+        raise loxi.ProtocolError("message too short")
+    stats_type, = struct.unpack_from("!H", buf, 8)
+    if stats_type in stats_reply_parsers:
+        return stats_reply_parsers[stats_type](buf)
+    else:
+        raise loxi.ProtocolError("unexpected stats type %u" % stats_type)
+
+def parse_stats_request(buf):
+    if len(buf) < 8 + 2:
+        raise loxi.ProtocolError("message too short")
+    stats_type, = struct.unpack_from("!H", buf, 8)
+    if stats_type in stats_request_parsers:
+        return stats_request_parsers[stats_type](buf)
+    else:
+        raise loxi.ProtocolError("unexpected stats type %u" % stats_type)
+
+def parse_vendor(buf):
+    if len(buf) < 16:
+        raise loxi.ProtocolError("experimenter message too short")
+
+    experimenter, = struct.unpack_from("!L", buf, 8)
+    if experimenter == 0x005c16c7: # Big Switch Networks
+        subtype, = struct.unpack_from("!L", buf, 12)
+    elif experimenter == 0x00002320: # Nicira
+        subtype, = struct.unpack_from("!L", buf, 12)
+    else:
+        raise loxi.ProtocolError("unexpected experimenter id %#x" % experimenter)
+
+    if subtype in experimenter_parsers[experimenter]:
+        return experimenter_parsers[experimenter][subtype](buf)
+    else:
+        raise loxi.ProtocolError("unexpected experimenter %#x subtype %#x" % (experimenter, subtype))
+
+parsers = {
+:: sort_key = lambda x: x.type_members[1].value
+:: msgtype_groups = itertools.groupby(sorted(ofclasses, key=sort_key), sort_key)
+:: for (k, v) in msgtype_groups:
+:: v = list(v)
+:: if len(v) == 1:
+    ${k} : ${v[0].pyname}.unpack,
+:: else:
+    ${k} : parse_${k[11:].lower()},
+:: #endif
+:: #endfor
+}
+
+flow_mod_parsers = {
+    const.OFPFC_ADD : flow_add.unpack,
+    const.OFPFC_MODIFY : flow_modify.unpack,
+    const.OFPFC_MODIFY_STRICT : flow_modify_strict.unpack,
+    const.OFPFC_DELETE : flow_delete.unpack,
+    const.OFPFC_DELETE_STRICT : flow_delete_strict.unpack,
+}
+
+stats_reply_parsers = {
+    const.OFPST_DESC : desc_stats_reply.unpack,
+    const.OFPST_FLOW : flow_stats_reply.unpack,
+    const.OFPST_AGGREGATE : aggregate_stats_reply.unpack,
+    const.OFPST_TABLE : table_stats_reply.unpack,
+    const.OFPST_PORT : port_stats_reply.unpack,
+    const.OFPST_QUEUE : queue_stats_reply.unpack,
+    const.OFPST_VENDOR : experimenter_stats_reply.unpack,
+}
+
+stats_request_parsers = {
+    const.OFPST_DESC : desc_stats_request.unpack,
+    const.OFPST_FLOW : flow_stats_request.unpack,
+    const.OFPST_AGGREGATE : aggregate_stats_request.unpack,
+    const.OFPST_TABLE : table_stats_request.unpack,
+    const.OFPST_PORT : port_stats_request.unpack,
+    const.OFPST_QUEUE : queue_stats_request.unpack,
+    const.OFPST_VENDOR : experimenter_stats_request.unpack,
+}
+
+:: experimenter_ofclasses = [x for x in ofclasses if x.type_members[1].value == 'const.OFPT_VENDOR']
+:: sort_key = lambda x: x.type_members[2].value
+:: experimenter_ofclasses.sort(key=sort_key)
+:: grouped = itertools.groupby(experimenter_ofclasses, sort_key)
+experimenter_parsers = {
+:: for (experimenter, v) in grouped:
+    ${experimenter} : {
+:: for ofclass in v:
+        ${ofclass.type_members[3].value}: ${ofclass.pyname}.unpack,
+:: #endfor
+    },
+:: #endfor
+}
diff --git a/py_gen/templates/pp.py b/py_gen/templates/pp.py
new file mode 100644
index 0000000..0021a28
--- /dev/null
+++ b/py_gen/templates/pp.py
@@ -0,0 +1,277 @@
+:: # Copyright 2013, Big Switch Networks, Inc.
+:: #
+:: # LoxiGen is licensed under the Eclipse Public License, version 1.0 (EPL), with
+:: # the following special exception:
+:: #
+:: # LOXI Exception
+:: #
+:: # As a special exception to the terms of the EPL, you may distribute libraries
+:: # generated by LoxiGen (LoxiGen Libraries) under the terms of your choice, provided
+:: # that copyright and licensing notices generated by LoxiGen are not altered or removed
+:: # from the LoxiGen Libraries and the notice provided below is (i) included in
+:: # the LoxiGen Libraries, if distributed in source code form and (ii) included in any
+:: # documentation for the LoxiGen Libraries, if distributed in binary form.
+:: #
+:: # Notice: "Copyright 2013, Big Switch Networks, Inc. This library was generated by the LoxiGen Compiler."
+:: #
+:: # You may not use this file except in compliance with the EPL or LOXI Exception. You may obtain
+:: # a copy of the EPL at:
+:: #
+:: # http://www.eclipse.org/legal/epl-v10.html
+:: #
+:: # Unless required by applicable law or agreed to in writing, software
+:: # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+:: # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+:: # EPL for the specific language governing permissions and limitations
+:: # under the EPL.
+::
+# Copyright 2013, Big Switch Networks, Inc.
+
+"""
+pp - port of Ruby's PP library
+Also based on Lindig, C., & GbR, G. D. (2000). Strictly Pretty.
+
+Example usage:
+>>> import pp.pp as pp
+>>> print pp([[1, 2], [3, 4]], maxwidth=15)
+[
+  [ 1, 2 ],
+  [ 3, 4 ]
+]
+"""
+import unittest
+from contextlib import contextmanager
+
+def pp(obj, maxwidth=79):
+    """
+    Pretty-print the given object.
+    """
+    ctx = PrettyPrinter(maxwidth=maxwidth)
+    ctx.pp(obj)
+    return str(ctx)
+
+
+## Pretty-printers for builtin classes
+
+def pretty_print_list(pp, obj):
+    with pp.group():
+        pp.text('[')
+        with pp.indent(2):
+            for v in obj:
+                if not pp.first(): pp.text(',')
+                pp.breakable()
+                pp.pp(v)
+        pp.breakable()
+        pp.text(']')
+
+def pretty_print_dict(pp, obj):
+    with pp.group():
+        pp.text('{')
+        with pp.indent(2):
+            for (k, v) in sorted(obj.items()):
+                if not pp.first(): pp.text(',')
+                pp.breakable()
+                pp.pp(k)
+                pp.text(': ')
+                pp.pp(v)
+        pp.breakable()
+        pp.text('}')
+
+pretty_printers = {
+    list: pretty_print_list,
+    dict: pretty_print_dict,
+}
+
+
+## Implementation
+
+class PrettyPrinter(object):
+    def __init__(self, maxwidth):
+        self.maxwidth = maxwidth
+        self.cur_indent = 0
+        self.root_group = Group()
+        self.group_stack = [self.root_group]
+
+    def current_group(self):
+        return self.group_stack[-1]
+
+    def text(self, s):
+        self.current_group().append(str(s))
+
+    def breakable(self, sep=' '):
+        self.current_group().append(Breakable(sep, self.cur_indent))
+
+    def first(self):
+        return self.current_group().first()
+
+    @contextmanager
+    def indent(self, n):
+        self.cur_indent += n
+        yield
+        self.cur_indent -= n
+
+    @contextmanager
+    def group(self):
+        self.group_stack.append(Group())
+        yield
+        new_group = self.group_stack.pop()
+        self.current_group().append(new_group)
+
+    def pp(self, obj):
+        if hasattr(obj, "pretty_print"):
+            obj.pretty_print(self)
+        elif type(obj) in pretty_printers:
+            pretty_printers[type(obj)](self, obj)
+        else:
+            self.text(repr(obj))
+
+    def __str__(self):
+        return self.root_group.render(0, self.maxwidth)
+
+class Group(object):
+    __slots__ = ["fragments", "length", "_first"]
+
+    def __init__(self):
+        self.fragments = []
+        self.length = 0
+        self._first = True
+
+    def append(self, x):
+        self.fragments.append(x)
+        self.length += len(x)
+
+    def first(self):
+        if self._first:
+            self._first = False
+            return True
+        return False
+
+    def __len__(self):
+        return self.length
+
+    def render(self, curwidth, maxwidth):
+        dobreak = len(self) > (maxwidth - curwidth)
+
+        a = []
+        for x in self.fragments:
+            if isinstance(x, Breakable):
+                if dobreak:
+                    a.append('\n')
+                    a.append(' ' * x.indent)
+                    curwidth = 0
+                else:
+                    a.append(x.sep)
+            elif isinstance(x, Group):
+                a.append(x.render(curwidth, maxwidth))
+            else:
+                a.append(x)
+            curwidth += len(a[-1])
+        return ''.join(a)
+
+class Breakable(object):
+    __slots__ = ["sep", "indent"]
+
+    def __init__(self, sep, indent):
+        self.sep = sep
+        self.indent = indent
+
+    def __len__(self):
+        return len(self.sep)
+
+
+## Tests
+
+class TestPP(unittest.TestCase):
+    def test_scalars(self):
+        self.assertEquals(pp(1), "1")
+        self.assertEquals(pp("foo"), "'foo'")
+
+    def test_hash(self):
+        expected = """{ 1: 'a', 'b': 2 }"""
+        self.assertEquals(pp(eval(expected)), expected)
+        expected = """\
+{
+  1: 'a',
+  'b': 2
+}"""
+        self.assertEquals(pp(eval(expected), maxwidth=0), expected)
+
+    def test_array(self):
+        expected = """[ 1, 'a', 2 ]"""
+        self.assertEquals(pp(eval(expected)), expected)
+        expected = """\
+[
+  1,
+  'a',
+  2
+]"""
+        self.assertEquals(pp(eval(expected), maxwidth=0), expected)
+
+    def test_nested(self):
+        expected = """[ [ 1, 2 ], [ 3, 4 ] ]"""
+        self.assertEquals(pp(eval(expected)), expected)
+        expected = """\
+[
+  [
+    1,
+    2
+  ],
+  [
+    3,
+    4
+  ]
+]"""
+        self.assertEquals(pp(eval(expected), maxwidth=0), expected)
+
+    def test_breaking(self):
+        expected = """\
+[
+  [ 1, 2 ],
+  'abcdefghijklmnopqrstuvwxyz'
+]"""
+        self.assertEquals(pp(eval(expected), maxwidth=24), expected)
+        expected = """\
+[
+  [ 'abcd', 2 ],
+  [ '0123456789' ],
+  [
+    '0123456789',
+    'abcdefghij'
+  ],
+  [ 'abcdefghijklmnop' ],
+  [
+    'abcdefghijklmnopq'
+  ],
+  { 'k': 'v' },
+  {
+    1: [ 2, [ 3, 4 ] ],
+    'foo': 'abcdefghijklmnop'
+  }
+]"""
+        self.assertEquals(pp(eval(expected), maxwidth=24), expected)
+        expected = """\
+[
+  [ 1, 2 ],
+  [ 3, 4 ]
+]"""
+        self.assertEquals(pp(eval(expected), maxwidth=15), expected)
+
+    # This is an edge case where our simpler algorithm breaks down.
+    @unittest.expectedFailure
+    def test_greedy_breaking(self):
+        expected = """\
+abc def
+ghijklmnopqrstuvwxyz\
+"""
+        pp = PrettyPrinter(maxwidth=8)
+        pp.text("abc")
+        with pp.group():
+            pp.breakable()
+        pp.text("def")
+        with pp.group():
+            pp.breakable()
+        pp.text("ghijklmnopqrstuvwxyz")
+        self.assertEquals(str(pp), expected)
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/py_gen/templates/toplevel_init.py b/py_gen/templates/toplevel_init.py
new file mode 100644
index 0000000..c990aa3
--- /dev/null
+++ b/py_gen/templates/toplevel_init.py
@@ -0,0 +1,46 @@
+:: # Copyright 2013, Big Switch Networks, Inc.
+:: #
+:: # LoxiGen is licensed under the Eclipse Public License, version 1.0 (EPL), with
+:: # the following special exception:
+:: #
+:: # LOXI Exception
+:: #
+:: # As a special exception to the terms of the EPL, you may distribute libraries
+:: # generated by LoxiGen (LoxiGen Libraries) under the terms of your choice, provided
+:: # that copyright and licensing notices generated by LoxiGen are not altered or removed
+:: # from the LoxiGen Libraries and the notice provided below is (i) included in
+:: # the LoxiGen Libraries, if distributed in source code form and (ii) included in any
+:: # documentation for the LoxiGen Libraries, if distributed in binary form.
+:: #
+:: # Notice: "Copyright 2013, Big Switch Networks, Inc. This library was generated by the LoxiGen Compiler."
+:: #
+:: # You may not use this file except in compliance with the EPL or LOXI Exception. You may obtain
+:: # a copy of the EPL at:
+:: #
+:: # http://www.eclipse.org/legal/epl-v10.html
+:: #
+:: # Unless required by applicable law or agreed to in writing, software
+:: # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+:: # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+:: # EPL for the specific language governing permissions and limitations
+:: # under the EPL.
+::
+:: include('_copyright.py')
+
+:: include('_autogen.py')
+
+def protocol(ver):
+    """
+    Import and return the protocol module for the given wire version.
+    """
+    if ver == 1:
+        import of10
+        return of10
+    else:
+        raise ValueError
+
+class ProtocolError(Exception):
+    """
+    Raised when failing to deserialize an invalid OpenFlow message.
+    """
+    pass
diff --git a/py_gen/templates/util.py b/py_gen/templates/util.py
new file mode 100644
index 0000000..5933e14
--- /dev/null
+++ b/py_gen/templates/util.py
@@ -0,0 +1,77 @@
+:: # Copyright 2013, Big Switch Networks, Inc.
+:: #
+:: # LoxiGen is licensed under the Eclipse Public License, version 1.0 (EPL), with
+:: # the following special exception:
+:: #
+:: # LOXI Exception
+:: #
+:: # As a special exception to the terms of the EPL, you may distribute libraries
+:: # generated by LoxiGen (LoxiGen Libraries) under the terms of your choice, provided
+:: # that copyright and licensing notices generated by LoxiGen are not altered or removed
+:: # from the LoxiGen Libraries and the notice provided below is (i) included in
+:: # the LoxiGen Libraries, if distributed in source code form and (ii) included in any
+:: # documentation for the LoxiGen Libraries, if distributed in binary form.
+:: #
+:: # Notice: "Copyright 2013, Big Switch Networks, Inc. This library was generated by the LoxiGen Compiler."
+:: #
+:: # You may not use this file except in compliance with the EPL or LOXI Exception. You may obtain
+:: # a copy of the EPL at:
+:: #
+:: # http://www.eclipse.org/legal/epl-v10.html
+:: #
+:: # Unless required by applicable law or agreed to in writing, software
+:: # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+:: # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+:: # EPL for the specific language governing permissions and limitations
+:: # under the EPL.
+::
+:: include('_copyright.py')
+
+:: include('_autogen.py')
+
+import loxi
+import const
+
+def unpack_array(deserializer, element_size, buf):
+    """
+    Deserialize an array of fixed length elements.
+    The deserializer function should take a buffer and return the new object.
+    """
+    if len(buf) % element_size != 0: raise loxi.ProtocolError("invalid array length")
+    n = len(buf) / element_size
+    return [deserializer(buffer(buf, i*element_size, element_size)) for i in range(n)]
+
+def pretty_mac(mac):
+    return ':'.join(["%02x" % x for x in mac])
+
+def pretty_ipv4(v):
+    return "%d.%d.%d.%d" % ((v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF)
+
+def pretty_flags(v, flag_names):
+    set_flags = []
+    for flag_name in flag_names:
+        flag_value = getattr(const, flag_name)
+        if v & flag_value == flag_value:
+            set_flags.append(flag_name)
+        elif v & flag_value:
+            set_flags.append('%s&%#x' % (flag_name, v & flag_value))
+        v &= ~flag_value
+    if v:
+        set_flags.append("%#x" % v)
+    return '|'.join(set_flags) or '0'
+
+def pretty_wildcards(v):
+    if v == const.OFPFW_ALL:
+        return 'OFPFW_ALL'
+    flag_names = ['OFPFW_IN_PORT', 'OFPFW_DL_VLAN', 'OFPFW_DL_SRC', 'OFPFW_DL_DST',
+                  'OFPFW_DL_TYPE', 'OFPFW_NW_PROTO', 'OFPFW_TP_SRC', 'OFPFW_TP_DST',
+                  'OFPFW_NW_SRC_MASK', 'OFPFW_NW_DST_MASK', 'OFPFW_DL_VLAN_PCP',
+                  'OFPFW_NW_TOS']
+    return pretty_flags(v, flag_names)
+
+def pretty_port(v):
+    named_ports = [(k,v2) for (k,v2) in const.__dict__.iteritems() if k.startswith('OFPP_')]
+    for (k, v2) in named_ports:
+        if v == v2:
+            return k
+    return v
diff --git a/py_gen/tests.py b/py_gen/tests.py
new file mode 100644
index 0000000..23d40b8
--- /dev/null
+++ b/py_gen/tests.py
@@ -0,0 +1,1002 @@
+#!/usr/bin/env python
+# Copyright 2013, Big Switch Networks, Inc.
+#
+# LoxiGen is licensed under the Eclipse Public License, version 1.0 (EPL), with
+# the following special exception:
+#
+# LOXI Exception
+#
+# As a special exception to the terms of the EPL, you may distribute libraries
+# generated by LoxiGen (LoxiGen Libraries) under the terms of your choice, provided
+# that copyright and licensing notices generated by LoxiGen are not altered or removed
+# from the LoxiGen Libraries and the notice provided below is (i) included in
+# the LoxiGen Libraries, if distributed in source code form and (ii) included in any
+# documentation for the LoxiGen Libraries, if distributed in binary form.
+#
+# Notice: "Copyright 2013, Big Switch Networks, Inc. This library was generated by the LoxiGen Compiler."
+#
+# You may not use this file except in compliance with the EPL or LOXI Exception. You may obtain
+# a copy of the EPL at:
+#
+# http://www.eclipse.org/legal/epl-v10.html
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# EPL for the specific language governing permissions and limitations
+# under the EPL.
+import unittest
+
+try:
+    import loxi
+    del loxi
+except ImportError:
+    exit("loxi package not found. Try setting PYTHONPATH.")
+
+class TestImports(unittest.TestCase):
+    def test_toplevel(self):
+        import loxi
+        self.assertTrue(hasattr(loxi, "ProtocolError"))
+        ofp = loxi.protocol(1)
+        self.assertEquals(ofp.OFP_VERSION, 1)
+        self.assertTrue(hasattr(ofp, "action"))
+        self.assertTrue(hasattr(ofp, "common"))
+        self.assertTrue(hasattr(ofp, "const"))
+        self.assertTrue(hasattr(ofp, "message"))
+
+    def test_version(self):
+        import loxi.of10
+        self.assertTrue(hasattr(loxi.of10, "ProtocolError"))
+        self.assertTrue(hasattr(loxi.of10, "OFP_VERSION"))
+        self.assertEquals(loxi.of10.OFP_VERSION, 1)
+        self.assertTrue(hasattr(loxi.of10, "action"))
+        self.assertTrue(hasattr(loxi.of10, "common"))
+        self.assertTrue(hasattr(loxi.of10, "const"))
+        self.assertTrue(hasattr(loxi.of10, "message"))
+
+class TestActions(unittest.TestCase):
+    def test_output_pack(self):
+        import loxi.of10 as ofp
+        expected = "\x00\x00\x00\x08\xff\xf8\xff\xff"
+        action = ofp.action.output(port=ofp.OFPP_IN_PORT, max_len=0xffff)
+        self.assertEquals(expected, action.pack())
+
+    def test_output_unpack(self):
+        import loxi.of10 as ofp
+
+        # Normal case
+        buf = "\x00\x00\x00\x08\xff\xf8\xff\xff"
+        action = ofp.action.output.unpack(buf)
+        self.assertEqual(action.port, ofp.OFPP_IN_PORT)
+        self.assertEqual(action.max_len, 0xffff)
+
+        # Invalid length
+        buf = "\x00\x00\x00\x09\xff\xf8\xff\xff\x00"
+        with self.assertRaises(ofp.ProtocolError):
+            ofp.action.output.unpack(buf)
+
+    def test_output_equality(self):
+        import loxi.of10 as ofp
+        action = ofp.action.output(port=1, max_len=0x1234)
+        action2 = ofp.action.output(port=1, max_len=0x1234)
+        self.assertEquals(action, action2)
+
+        action2.port = 2
+        self.assertNotEquals(action, action2)
+        action2.port = 1
+
+        action2.max_len = 0xffff
+        self.assertNotEquals(action, action2)
+        action2.max_len = 0x1234
+
+    def test_output_show(self):
+        import loxi.of10 as ofp
+        action = ofp.action.output(port=1, max_len=0x1234)
+        expected = "output { port = 1, max_len = 0x1234 }"
+        self.assertEquals(expected, action.show())
+
+    def test_bsn_set_tunnel_dst_pack(self):
+        import loxi.of10 as ofp
+        expected = ''.join([
+            "\xff\xff", "\x00\x10", # type/length
+            "\x00\x5c\x16\xc7", # experimenter
+            "\x00\x00\x00\x02", # subtype
+            "\x12\x34\x56\x78" # dst
+        ])
+        action = ofp.action.bsn_set_tunnel_dst(dst=0x12345678)
+        self.assertEquals(expected, action.pack())
+
+    def test_bsn_set_tunnel_dst_unpack(self):
+        import loxi.of10 as ofp
+        buf = ''.join([
+            "\xff\xff", "\x00\x10", # type/length
+            "\x00\x5c\x16\xc7", # experimenter
+            "\x00\x00\x00\x02", # subtype
+            "\x12\x34\x56\x78" # dst
+        ])
+        action = ofp.action.bsn_set_tunnel_dst.unpack(buf)
+        self.assertEqual(action.subtype, 2)
+        self.assertEqual(action.dst, 0x12345678)
+
+# Assumes action serialization/deserialization works
+class TestActionList(unittest.TestCase):
+    def test_normal(self):
+        import loxi.of10 as ofp
+
+        expected = []
+        bufs = []
+
+        def add(action):
+            expected.append(action)
+            bufs.append(action.pack())
+
+        add(ofp.action.output(port=1, max_len=0xffff))
+        add(ofp.action.output(port=2, max_len=0xffff))
+        add(ofp.action.output(port=ofp.OFPP_IN_PORT, max_len=0xffff))
+        add(ofp.action.bsn_set_tunnel_dst(dst=0x12345678))
+        add(ofp.action.nicira_dec_ttl())
+
+        actions = ofp.action.unpack_list(''.join(bufs))
+        self.assertEquals(actions, expected)
+
+    def test_empty_list(self):
+        import loxi.of10 as ofp
+        self.assertEquals(ofp.action.unpack_list(''), [])
+
+    def test_invalid_list_length(self):
+        import loxi.of10 as ofp
+        buf = '\x00' * 9
+        with self.assertRaisesRegexp(ofp.ProtocolError, 'not a multiple of 8'):
+            ofp.action.unpack_list(buf)
+
+    def test_invalid_action_length(self):
+        import loxi.of10 as ofp
+
+        buf = '\x00' * 8
+        with self.assertRaisesRegexp(ofp.ProtocolError, 'is 0'):
+            ofp.action.unpack_list(buf)
+
+        buf = '\x00\x00\x00\x04'
+        with self.assertRaisesRegexp(ofp.ProtocolError, 'not a multiple of 8'):
+            ofp.action.unpack_list(buf)
+
+        buf = '\x00\x00\x00\x10\x00\x00\x00\x00'
+        with self.assertRaisesRegexp(ofp.ProtocolError, 'overrun'):
+            ofp.action.unpack_list(buf)
+
+    def test_invalid_action_type(self):
+        import loxi.of10 as ofp
+        buf = '\xff\xfe\x00\x08\x00\x00\x00\x00'
+        with self.assertRaisesRegexp(ofp.ProtocolError, 'unknown action type'):
+            ofp.action.unpack_list(buf)
+
+class TestConstants(unittest.TestCase):
+    def test_ports(self):
+        import loxi.of10 as ofp
+        self.assertEquals(0xffff, ofp.OFPP_NONE)
+
+    def test_wildcards(self):
+        import loxi.of10 as ofp
+        self.assertEquals(0xfc000, ofp.OFPFW_NW_DST_MASK)
+
+class TestCommon(unittest.TestCase):
+    def test_port_desc_pack(self):
+        import loxi.of10 as ofp
+        obj = ofp.port_desc(port_no=ofp.OFPP_CONTROLLER,
+                            hw_addr=[1,2,3,4,5,6],
+                            name="foo",
+                            config=ofp.OFPPC_NO_FLOOD,
+                            state=ofp.OFPPS_STP_FORWARD,
+                            curr=ofp.OFPPF_10MB_HD,
+                            advertised=ofp.OFPPF_1GB_FD,
+                            supported=ofp.OFPPF_AUTONEG,
+                            peer=ofp.OFPPF_PAUSE_ASYM)
+        expected = ''.join([
+            '\xff\xfd', # port_no
+            '\x01\x02\x03\x04\x05\x06', # hw_addr
+            'foo'.ljust(16, '\x00'), # name
+            '\x00\x00\x00\x10', # config
+            '\x00\x00\x02\x00', # state
+            '\x00\x00\x00\x01', # curr
+            '\x00\x00\x00\x20', # advertised
+            '\x00\x00\x02\x00', # supported
+            '\x00\x00\x08\x00', # peer
+        ])
+        self.assertEquals(expected, obj.pack())
+
+    def test_port_desc_unpack(self):
+        import loxi.of10 as ofp
+        buf = ''.join([
+            '\xff\xfd', # port_no
+            '\x01\x02\x03\x04\x05\x06', # hw_addr
+            'foo'.ljust(16, '\x00'), # name
+            '\x00\x00\x00\x10', # config
+            '\x00\x00\x02\x00', # state
+            '\x00\x00\x00\x01', # curr
+            '\x00\x00\x00\x20', # advertised
+            '\x00\x00\x02\x00', # supported
+            '\x00\x00\x08\x00', # peer
+        ])
+        obj = ofp.port_desc.unpack(buf)
+        self.assertEquals(ofp.OFPP_CONTROLLER, obj.port_no)
+        self.assertEquals('foo', obj.name)
+        self.assertEquals(ofp.OFPPF_PAUSE_ASYM, obj.peer)
+
+    def test_table_stats_entry_pack(self):
+        import loxi.of10 as ofp
+        obj = ofp.table_stats_entry(table_id=3,
+                                    name="foo",
+                                    wildcards=ofp.OFPFW_ALL,
+                                    max_entries=5,
+                                    active_count=2,
+                                    lookup_count=1099511627775,
+                                    matched_count=9300233470495232273L)
+        expected = ''.join([
+            '\x03', # table_id
+            '\x00\x00\x00', # pad
+            'foo'.ljust(32, '\x00'), # name
+            '\x00\x3f\xFF\xFF', # wildcards
+            '\x00\x00\x00\x05', # max_entries
+            '\x00\x00\x00\x02', # active_count
+            '\x00\x00\x00\xff\xff\xff\xff\xff', # lookup_count
+            '\x81\x11\x11\x11\x11\x11\x11\x11', # matched_count
+        ])
+        self.assertEquals(expected, obj.pack())
+
+    def test_table_stats_entry_unpack(self):
+        import loxi.of10 as ofp
+        buf = ''.join([
+            '\x03', # table_id
+            '\x00\x00\x00', # pad
+            'foo'.ljust(32, '\x00'), # name
+            '\x00\x3f\xFF\xFF', # wildcards
+            '\x00\x00\x00\x05', # max_entries
+            '\x00\x00\x00\x02', # active_count
+            '\x00\x00\x00\xff\xff\xff\xff\xff', # lookup_count
+            '\x81\x11\x11\x11\x11\x11\x11\x11', # matched_count
+        ])
+        obj = ofp.table_stats_entry.unpack(buf)
+        self.assertEquals(3, obj.table_id)
+        self.assertEquals('foo', obj.name)
+        self.assertEquals(9300233470495232273L, obj.matched_count)
+
+    def test_flow_stats_entry_pack(self):
+        import loxi.of10 as ofp
+        obj = ofp.flow_stats_entry(table_id=3,
+                                   match=ofp.match(),
+                                   duration_sec=1,
+                                   duration_nsec=2,
+                                   priority=100,
+                                   idle_timeout=5,
+                                   hard_timeout=10,
+                                   cookie=0x0123456789abcdef,
+                                   packet_count=10,
+                                   byte_count=1000,
+                                   actions=[ofp.action.output(port=1),
+                                            ofp.action.output(port=2)])
+        expected = ''.join([
+            '\x00\x68', # length
+            '\x03', # table_id
+            '\x00', # pad
+            '\x00\x3f\xff\xff', # match.wildcards
+            '\x00' * 36, # remaining match fields
+            '\x00\x00\x00\x01', # duration_sec
+            '\x00\x00\x00\x02', # duration_nsec
+            '\x00\x64', # priority
+            '\x00\x05', # idle_timeout
+            '\x00\x0a', # hard_timeout
+            '\x00' * 6, # pad2
+            '\x01\x23\x45\x67\x89\xab\xcd\xef', # cookie
+            '\x00\x00\x00\x00\x00\x00\x00\x0a', # packet_count
+            '\x00\x00\x00\x00\x00\x00\x03\xe8', # byte_count
+            '\x00\x00', # actions[0].type
+            '\x00\x08', # actions[0].len
+            '\x00\x01', # actions[0].port
+            '\x00\x00', # actions[0].max_len
+            '\x00\x00', # actions[1].type
+            '\x00\x08', # actions[1].len
+            '\x00\x02', # actions[1].port
+            '\x00\x00', # actions[1].max_len
+        ])
+        self.assertEquals(expected, obj.pack())
+
+    def test_flow_stats_entry_unpack(self):
+        import loxi.of10 as ofp
+        buf = ''.join([
+            '\x00\x68', # length
+            '\x03', # table_id
+            '\x00', # pad
+            '\x00\x3f\xff\xff', # match.wildcards
+            '\x00' * 36, # remaining match fields
+            '\x00\x00\x00\x01', # duration_sec
+            '\x00\x00\x00\x02', # duration_nsec
+            '\x00\x64', # priority
+            '\x00\x05', # idle_timeout
+            '\x00\x0a', # hard_timeout
+            '\x00' * 6, # pad2
+            '\x01\x23\x45\x67\x89\xab\xcd\xef', # cookie
+            '\x00\x00\x00\x00\x00\x00\x00\x0a', # packet_count
+            '\x00\x00\x00\x00\x00\x00\x03\xe8', # byte_count
+            '\x00\x00', # actions[0].type
+            '\x00\x08', # actions[0].len
+            '\x00\x01', # actions[0].port
+            '\x00\x00', # actions[0].max_len
+            '\x00\x00', # actions[1].type
+            '\x00\x08', # actions[1].len
+            '\x00\x02', # actions[1].port
+            '\x00\x00', # actions[1].max_len
+        ])
+        obj = ofp.flow_stats_entry.unpack(buf)
+        self.assertEquals(3, obj.table_id)
+        self.assertEquals(ofp.OFPFW_ALL, obj.match.wildcards)
+        self.assertEquals(2, len(obj.actions))
+        self.assertEquals(1, obj.actions[0].port)
+        self.assertEquals(2, obj.actions[1].port)
+
+    def test_match(self):
+        import loxi.of10 as ofp
+        match = ofp.match()
+        self.assertEquals(match.wildcards, ofp.OFPFW_ALL)
+        self.assertEquals(match.tcp_src, 0)
+        buf = match.pack()
+        match2 = ofp.match.unpack(buf)
+        self.assertEquals(match, match2)
+
+class TestMessages(unittest.TestCase):
+    def test_hello_construction(self):
+        import loxi.of10 as ofp
+
+        msg = ofp.message.hello()
+        self.assertEquals(msg.version, ofp.OFP_VERSION)
+        self.assertEquals(msg.type, ofp.OFPT_HELLO)
+        self.assertEquals(msg.xid, None)
+
+        msg = ofp.message.hello(xid=123)
+        self.assertEquals(msg.xid, 123)
+
+        # 0 is a valid xid distinct from None
+        msg = ofp.message.hello(xid=0)
+        self.assertEquals(msg.xid, 0)
+
+    def test_hello_unpack(self):
+        import loxi.of10 as ofp
+
+        # Normal case
+        buf = "\x01\x00\x00\x08\x12\x34\x56\x78"
+        msg = ofp.message.hello(xid=0x12345678)
+        self.assertEquals(buf, msg.pack())
+
+        # Invalid length
+        buf = "\x01\x00\x00\x09\x12\x34\x56\x78\x9a"
+        with self.assertRaisesRegexp(ofp.ProtocolError, "should be 8"):
+            ofp.message.hello.unpack(buf)
+
+    def test_echo_request_construction(self):
+        import loxi.of10 as ofp
+        msg = ofp.message.echo_request(data="abc")
+        self.assertEquals(msg.data, "abc")
+
+    def test_echo_request_pack(self):
+        import loxi.of10 as ofp
+
+        msg = ofp.message.echo_request(xid=0x12345678, data="abc")
+        buf = msg.pack()
+        self.assertEquals(buf, "\x01\x02\x00\x0b\x12\x34\x56\x78\x61\x62\x63")
+
+        msg2 = ofp.message.echo_request.unpack(buf)
+        self.assertEquals(msg, msg2)
+
+    def test_echo_request_unpack(self):
+        import loxi.of10 as ofp
+
+        # Normal case
+        buf = "\x01\x02\x00\x0b\x12\x34\x56\x78\x61\x62\x63"
+        msg = ofp.message.echo_request(xid=0x12345678, data="abc")
+        self.assertEquals(buf, msg.pack())
+
+        # Invalid length
+        buf = "\x01\x02\x00\x07\x12\x34\x56"
+        with self.assertRaisesRegexp(ofp.ProtocolError, "buffer too short"):
+            ofp.message.echo_request.unpack(buf)
+
+    def test_echo_request_equality(self):
+        import loxi.of10 as ofp
+
+        msg = ofp.message.echo_request(xid=0x12345678, data="abc")
+        msg2 = ofp.message.echo_request(xid=0x12345678, data="abc")
+        #msg2 = ofp.message.echo_request.unpack(msg.pack())
+        self.assertEquals(msg, msg2)
+
+        msg2.xid = 1
+        self.assertNotEquals(msg, msg2)
+        msg2.xid = msg.xid
+
+        msg2.data = "a"
+        self.assertNotEquals(msg, msg2)
+        msg2.data = msg.data
+
+    def test_echo_request_show(self):
+        import loxi.of10 as ofp
+        expected = "echo_request { xid = 0x12345678, data = 'ab\\x01' }"
+        msg = ofp.message.echo_request(xid=0x12345678, data="ab\x01")
+        self.assertEquals(msg.show(), expected)
+
+    def test_flow_add(self):
+        import loxi.of10 as ofp
+        match = ofp.match()
+        msg = ofp.message.flow_add(xid=1,
+                                   match=match,
+                                   cookie=1,
+                                   idle_timeout=5,
+                                   flags=ofp.OFPFF_CHECK_OVERLAP,
+                                   actions=[
+                                       ofp.action.output(port=1),
+                                       ofp.action.output(port=2),
+                                       ofp.action.output(port=ofp.OFPP_CONTROLLER,
+                                                         max_len=1024)])
+        buf = msg.pack()
+        msg2 = ofp.message.flow_add.unpack(buf)
+        self.assertEquals(msg, msg2)
+
+    def test_port_mod_pack(self):
+        import loxi.of10 as ofp
+        msg = ofp.message.port_mod(xid=2,
+                                   port_no=ofp.OFPP_CONTROLLER,
+                                   hw_addr=[1,2,3,4,5,6],
+                                   config=0x90ABCDEF,
+                                   mask=0xFF11FF11,
+                                   advertise=0xCAFE6789)
+        expected = "\x01\x0f\x00\x20\x00\x00\x00\x02\xff\xfd\x01\x02\x03\x04\x05\x06\x90\xab\xcd\xef\xff\x11\xff\x11\xca\xfe\x67\x89\x00\x00\x00\x00"
+        self.assertEquals(expected, msg.pack())
+
+    def test_desc_stats_reply_pack(self):
+        import loxi.of10 as ofp
+        msg = ofp.message.desc_stats_reply(xid=3,
+                                           flags=ofp.OFPSF_REPLY_MORE,
+                                           mfr_desc="The Indigo-2 Community",
+                                           hw_desc="Unknown server",
+                                           sw_desc="Indigo-2 LRI pre-release",
+                                           serial_num="11235813213455",
+                                           dp_desc="Indigo-2 LRI forwarding module")
+        expected = ''.join([
+            '\x01', '\x11', # version/type
+            '\x04\x2c', # length
+            '\x00\x00\x00\x03', # xid
+            '\x00\x00', # stats_type
+            '\x00\x01', # flags
+            'The Indigo-2 Community'.ljust(256, '\x00'), # mfr_desc
+            'Unknown server'.ljust(256, '\x00'), # hw_desc
+            'Indigo-2 LRI pre-release'.ljust(256, '\x00'), # sw_desc
+            '11235813213455'.ljust(32, '\x00'), # serial_num
+            'Indigo-2 LRI forwarding module'.ljust(256, '\x00'), # dp_desc
+        ])
+        self.assertEquals(expected, msg.pack())
+
+    def test_desc_stats_reply_unpack(self):
+        import loxi.of10 as ofp
+        buf = ''.join([
+            '\x01', '\x11', # version/type
+            '\x04\x2c', # length
+            '\x00\x00\x00\x03', # xid
+            '\x00\x00', # stats_type
+            '\x00\x01', # flags
+            'The Indigo-2 Community'.ljust(256, '\x00'), # mfr_desc
+            'Unknown server'.ljust(256, '\x00'), # hw_desc
+            'Indigo-2 LRI pre-release'.ljust(256, '\x00'), # sw_desc
+            '11235813213455'.ljust(32, '\x00'), # serial_num
+            'Indigo-2 LRI forwarding module'.ljust(256, '\x00'), # dp_desc
+        ])
+        msg = ofp.message.desc_stats_reply.unpack(buf)
+        self.assertEquals('Indigo-2 LRI forwarding module', msg.dp_desc)
+        self.assertEquals('11235813213455', msg.serial_num)
+        self.assertEquals(ofp.OFPST_DESC, msg.stats_type)
+        self.assertEquals(ofp.OFPSF_REPLY_MORE, msg.flags)
+
+    def test_port_status_pack(self):
+        import loxi.of10 as ofp
+
+        desc = ofp.port_desc(port_no=ofp.OFPP_CONTROLLER,
+                             hw_addr=[1,2,3,4,5,6],
+                             name="foo",
+                             config=ofp.OFPPC_NO_FLOOD,
+                             state=ofp.OFPPS_STP_FORWARD,
+                             curr=ofp.OFPPF_10MB_HD,
+                             advertised=ofp.OFPPF_1GB_FD,
+                             supported=ofp.OFPPF_AUTONEG,
+                             peer=ofp.OFPPF_PAUSE_ASYM)
+
+        msg = ofp.message.port_status(xid=4,
+                                      reason=ofp.OFPPR_DELETE,
+                                      desc=desc)
+        expected = ''.join([
+            '\x01', '\x0c', # version/type
+            '\x00\x40', # length
+            '\x00\x00\x00\x04', # xid
+            '\x01', # reason
+            '\x00\x00\x00\x00\x00\x00\x00' # pad
+            '\xff\xfd', # desc.port_no
+            '\x01\x02\x03\x04\x05\x06', # desc.hw_addr
+            'foo'.ljust(16, '\x00'), # desc.name
+            '\x00\x00\x00\x10', # desc.config
+            '\x00\x00\x02\x00', # desc.state
+            '\x00\x00\x00\x01', # desc.curr
+            '\x00\x00\x00\x20', # desc.advertised
+            '\x00\x00\x02\x00', # desc.supported
+            '\x00\x00\x08\x00', # desc.peer
+        ])
+        self.assertEquals(expected, msg.pack())
+
+    def test_port_status_unpack(self):
+        import loxi.of10 as ofp
+        buf = ''.join([
+            '\x01', '\x0c', # version/type
+            '\x00\x40', # length
+            '\x00\x00\x00\x04', # xid
+            '\x01', # reason
+            '\x00\x00\x00\x00\x00\x00\x00' # pad
+            '\xff\xfd', # desc.port_no
+            '\x01\x02\x03\x04\x05\x06', # desc.hw_addr
+            'foo'.ljust(16, '\x00'), # desc.name
+            '\x00\x00\x00\x10', # desc.config
+            '\x00\x00\x02\x00', # desc.state
+            '\x00\x00\x00\x01', # desc.curr
+            '\x00\x00\x00\x20', # desc.advertised
+            '\x00\x00\x02\x00', # desc.supported
+            '\x00\x00\x08\x00', # desc.peer
+        ])
+        msg = ofp.message.port_status.unpack(buf)
+        self.assertEquals('foo', msg.desc.name)
+        self.assertEquals(ofp.OFPPF_PAUSE_ASYM, msg.desc.peer)
+
+    def test_port_stats_reply_pack(self):
+        import loxi.of10 as ofp
+        msg = ofp.message.port_stats_reply(xid=5, flags=0, entries=[
+            ofp.port_stats_entry(port_no=1, rx_packets=56, collisions=5),
+            ofp.port_stats_entry(port_no=ofp.OFPP_LOCAL, rx_packets=1, collisions=1)])
+        expected = ''.join([
+            '\x01', '\x11', # version/type
+            '\x00\xdc', # length
+            '\x00\x00\x00\x05', # xid
+            '\x00\x04', # stats_type
+            '\x00\x00', # flags
+            '\x00\x01', # entries[0].port_no
+            '\x00\x00\x00\x00\x00\x00' # entries[0].pad
+            '\x00\x00\x00\x00\x00\x00\x00\x38', # entries[0].rx_packets
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[0].tx_packets
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[0].rx_bytes
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[0].tx_bytes
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[0].rx_dropped
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[0].tx_dropped
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[0].rx_errors
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[0].tx_errors
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[0].rx_frame_err
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[0].rx_over_err
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[0].rx_crc_err
+            '\x00\x00\x00\x00\x00\x00\x00\x05', # entries[0].collisions
+            '\xff\xfe', # entries[1].port_no
+            '\x00\x00\x00\x00\x00\x00' # entries[1].pad
+            '\x00\x00\x00\x00\x00\x00\x00\x01', # entries[1].rx_packets
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[1].tx_packets
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[1].rx_bytes
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[1].tx_bytes
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[1].rx_dropped
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[1].tx_dropped
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[1].rx_errors
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[1].tx_errors
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[1].rx_frame_err
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[1].rx_over_err
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[1].rx_crc_err
+            '\x00\x00\x00\x00\x00\x00\x00\x01', # entries[1].collisions
+        ])
+        self.assertEquals(expected, msg.pack())
+
+    def test_port_stats_reply_unpack(self):
+        import loxi.of10 as ofp
+        buf = ''.join([
+            '\x01', '\x11', # version/type
+            '\x00\xdc', # length
+            '\x00\x00\x00\x05', # xid
+            '\x00\x04', # stats_type
+            '\x00\x00', # flags
+            '\x00\x01', # entries[0].port_no
+            '\x00\x00\x00\x00\x00\x00' # entries[0].pad
+            '\x00\x00\x00\x00\x00\x00\x00\x38', # entries[0].rx_packets
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[0].tx_packets
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[0].rx_bytes
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[0].tx_bytes
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[0].rx_dropped
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[0].tx_dropped
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[0].rx_errors
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[0].tx_errors
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[0].rx_frame_err
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[0].rx_over_err
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[0].rx_crc_err
+            '\x00\x00\x00\x00\x00\x00\x00\x05', # entries[0].collisions
+            '\xff\xfe', # entries[1].port_no
+            '\x00\x00\x00\x00\x00\x00' # entries[1].pad
+            '\x00\x00\x00\x00\x00\x00\x00\x01', # entries[1].rx_packets
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[1].tx_packets
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[1].rx_bytes
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[1].tx_bytes
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[1].rx_dropped
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[1].tx_dropped
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[1].rx_errors
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[1].tx_errors
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[1].rx_frame_err
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[1].rx_over_err
+            '\x00\x00\x00\x00\x00\x00\x00\x00', # entries[1].rx_crc_err
+            '\x00\x00\x00\x00\x00\x00\x00\x01', # entries[1].collisions
+        ])
+        msg = ofp.message.port_stats_reply.unpack(buf)
+        self.assertEquals(ofp.OFPST_PORT, msg.stats_type)
+        self.assertEquals(2, len(msg.entries))
+
+    sample_flow_stats_reply_buf = ''.join([
+        '\x01', '\x11', # version/type
+        '\x00\xe4', # length
+        '\x00\x00\x00\x06', # xid
+        '\x00\x01', # stats_type
+        '\x00\x00', # flags
+        '\x00\x68', # entries[0].length
+        '\x03', # entries[0].table_id
+        '\x00', # entries[0].pad
+        '\x00\x3f\xff\xff', # entries[0].match.wildcards
+        '\x00' * 36, # remaining match fields
+        '\x00\x00\x00\x01', # entries[0].duration_sec
+        '\x00\x00\x00\x02', # entries[0].duration_nsec
+        '\x00\x64', # entries[0].priority
+        '\x00\x05', # entries[0].idle_timeout
+        '\x00\x0a', # entries[0].hard_timeout
+        '\x00' * 6, # entries[0].pad2
+        '\x01\x23\x45\x67\x89\xab\xcd\xef', # entries[0].cookie
+        '\x00\x00\x00\x00\x00\x00\x00\x0a', # entries[0].packet_count
+        '\x00\x00\x00\x00\x00\x00\x03\xe8', # entries[0].byte_count
+        '\x00\x00', # entries[0].actions[0].type
+        '\x00\x08', # entries[0].actions[0].len
+        '\x00\x01', # entries[0].actions[0].port
+        '\x00\x00', # entries[0].actions[0].max_len
+        '\x00\x00', # entries[0].actions[1].type
+        '\x00\x08', # entries[0].actions[1].len
+        '\x00\x02', # entries[0].actions[1].port
+        '\x00\x00', # entries[0].actions[1].max_len
+        '\x00\x70', # entries[1].length
+        '\x04', # entries[1].table_id
+        '\x00', # entries[1].pad
+        '\x00\x3f\xff\xff', # entries[1].match.wildcards
+        '\x00' * 36, # remaining match fields
+        '\x00\x00\x00\x01', # entries[1].duration_sec
+        '\x00\x00\x00\x02', # entries[1].duration_nsec
+        '\x00\x64', # entries[1].priority
+        '\x00\x05', # entries[1].idle_timeout
+        '\x00\x0a', # entries[1].hard_timeout
+        '\x00' * 6, # entries[1].pad2
+        '\x01\x23\x45\x67\x89\xab\xcd\xef', # entries[1].cookie
+        '\x00\x00\x00\x00\x00\x00\x00\x0a', # entries[1].packet_count
+        '\x00\x00\x00\x00\x00\x00\x03\xe8', # entries[1].byte_count
+        '\x00\x00', # entries[1].actions[0].type
+        '\x00\x08', # entries[1].actions[0].len
+        '\x00\x01', # entries[1].actions[0].port
+        '\x00\x00', # entries[1].actions[0].max_len
+        '\x00\x00', # entries[1].actions[1].type
+        '\x00\x08', # entries[1].actions[1].len
+        '\x00\x02', # entries[1].actions[1].port
+        '\x00\x00', # entries[1].actions[1].max_len
+        '\x00\x00', # entries[1].actions[2].type
+        '\x00\x08', # entries[1].actions[2].len
+        '\x00\x03', # entries[1].actions[2].port
+        '\x00\x00', # entries[1].actions[2].max_len
+    ])
+
+    def test_flow_stats_reply_pack(self):
+        import loxi.of10 as ofp
+        msg = ofp.message.flow_stats_reply(xid=6, flags=0, entries=[
+            ofp.flow_stats_entry(table_id=3,
+                                 match=ofp.match(),
+                                 duration_sec=1,
+                                 duration_nsec=2,
+                                 priority=100,
+                                 idle_timeout=5,
+                                 hard_timeout=10,
+                                 cookie=0x0123456789abcdef,
+                                 packet_count=10,
+                                 byte_count=1000,
+                                 actions=[ofp.action.output(port=1),
+                                          ofp.action.output(port=2)]),
+            ofp.flow_stats_entry(table_id=4,
+                                 match=ofp.match(),
+                                 duration_sec=1,
+                                 duration_nsec=2,
+                                 priority=100,
+                                 idle_timeout=5,
+                                 hard_timeout=10,
+                                 cookie=0x0123456789abcdef,
+                                 packet_count=10,
+                                 byte_count=1000,
+                                 actions=[ofp.action.output(port=1),
+                                          ofp.action.output(port=2),
+                                          ofp.action.output(port=3)])])
+        self.assertEquals(self.sample_flow_stats_reply_buf, msg.pack())
+
+    def test_flow_stats_reply_unpack(self):
+        import loxi.of10 as ofp
+        msg = ofp.message.flow_stats_reply.unpack(self.sample_flow_stats_reply_buf)
+        self.assertEquals(ofp.OFPST_FLOW, msg.stats_type)
+        self.assertEquals(2, len(msg.entries))
+        self.assertEquals(2, len(msg.entries[0].actions))
+        self.assertEquals(3, len(msg.entries[1].actions))
+
+    def test_flow_add_show(self):
+        import loxi.of10 as ofp
+        expected = """\
+flow_add {
+  xid = None,
+  match = match_v1 {
+    wildcards = OFPFW_DL_SRC|OFPFW_DL_DST,
+    in_port = 3,
+    eth_src = 01:23:45:67:89:ab,
+    eth_dst = cd:ef:01:23:45:67,
+    vlan_vid = 0x0,
+    vlan_pcp = 0x0,
+    pad1 = 0x0,
+    eth_type = 0x0,
+    ip_dscp = 0x0,
+    ip_proto = 0x0,
+    pad2 = [ 0, 0 ],
+    ipv4_src = 192.168.3.127,
+    ipv4_dst = 255.255.255.255,
+    tcp_src = 0x0,
+    tcp_dst = 0x0
+  },
+  cookie = 0x0,
+  idle_timeout = 0x0,
+  hard_timeout = 0x0,
+  priority = 0x0,
+  buffer_id = 0x0,
+  out_port = 0,
+  flags = 0x0,
+  actions = [
+    output { port = OFPP_FLOOD, max_len = 0x0 },
+    nicira_dec_ttl { pad = 0x0, pad2 = 0x0 },
+    bsn_set_tunnel_dst { dst = 0x0 }
+  ]
+}"""
+        msg = ofp.message.flow_add(
+            match=ofp.match(
+                wildcards=ofp.OFPFW_DL_SRC|ofp.OFPFW_DL_DST,
+                in_port=3,
+                ipv4_src=0xc0a8037f,
+                ipv4_dst=0xffffffff,
+                eth_src=[0x01, 0x23, 0x45, 0x67, 0x89, 0xab],
+                eth_dst=[0xcd, 0xef, 0x01, 0x23, 0x45, 0x67]),
+            actions=[
+                ofp.action.output(port=ofp.OFPP_FLOOD),
+                ofp.action.nicira_dec_ttl(),
+                ofp.action.bsn_set_tunnel_dst()])
+        self.assertEquals(msg.show(), expected)
+
+    sample_packet_out_buf = ''.join([
+        '\x01', '\x0d', # version/type
+        '\x00\x23', # length
+        '\x12\x34\x56\x78', # xid
+        '\xab\xcd\xef\x01', # buffer_id
+        '\xff\xfe', # in_port
+        '\x00\x10', # actions_len
+        '\x00\x00', # actions[0].type
+        '\x00\x08', # actions[0].len
+        '\x00\x01', # actions[0].port
+        '\x00\x00', # actions[0].max_len
+        '\x00\x00', # actions[1].type
+        '\x00\x08', # actions[1].len
+        '\x00\x02', # actions[1].port
+        '\x00\x00', # actions[1].max_len
+        'abc' # data
+    ])
+
+    def test_packet_out_pack(self):
+        import loxi.of10 as ofp
+        msg = ofp.message.packet_out(
+            xid=0x12345678,
+            buffer_id=0xabcdef01,
+            in_port=ofp.OFPP_LOCAL,
+            actions=[
+                ofp.action.output(port=1),
+                ofp.action.output(port=2)],
+            data='abc')
+        self.assertEquals(self.sample_packet_out_buf, msg.pack())
+
+    def test_packet_out_unpack(self):
+        import loxi.of10 as ofp
+        msg = ofp.message.packet_out.unpack(self.sample_packet_out_buf)
+        self.assertEquals(0x12345678, msg.xid)
+        self.assertEquals(0xabcdef01, msg.buffer_id)
+        self.assertEquals(ofp.OFPP_LOCAL, msg.in_port)
+        self.assertEquals(2, len(msg.actions))
+        self.assertEquals(1, msg.actions[0].port)
+        self.assertEquals(2, msg.actions[1].port)
+        self.assertEquals('abc', msg.data)
+
+    sample_packet_in_buf = ''.join([
+        '\x01', '\x0a', # version/type
+        '\x00\x15', # length
+        '\x12\x34\x56\x78', # xid
+        '\xab\xcd\xef\x01', # buffer_id
+        '\x00\x09', # total_len
+        '\xff\xfe', # in_port
+        '\x01', # reason
+        '\x00', # pad
+        'abc', # data
+    ])
+
+    def test_packet_in_pack(self):
+        import loxi.of10 as ofp
+        msg = ofp.message.packet_in(
+            xid=0x12345678,
+            buffer_id=0xabcdef01,
+            total_len=9,
+            in_port=ofp.OFPP_LOCAL,
+            reason=ofp.OFPR_ACTION,
+            data='abc')
+        self.assertEquals(self.sample_packet_in_buf, msg.pack())
+
+    def test_packet_in_unpack(self):
+        import loxi.of10 as ofp
+        msg = ofp.message.packet_in.unpack(self.sample_packet_in_buf)
+        self.assertEquals(0x12345678, msg.xid)
+        self.assertEquals(0xabcdef01, msg.buffer_id)
+        self.assertEquals(9, msg.total_len)
+        self.assertEquals(ofp.OFPP_LOCAL, msg.in_port)
+        self.assertEquals(ofp.OFPR_ACTION, msg.reason)
+        self.assertEquals('abc', msg.data)
+
+    sample_queue_get_config_reply_buf = ''.join([
+        '\x01', '\x15', # version/type
+        '\x00\x50', # length
+        '\x12\x34\x56\x78', # xid
+        '\xff\xfe', # port
+        '\x00\x00\x00\x00\x00\x00', # pad
+        '\x00\x00\x00\x01', # queues[0].queue_id
+        '\x00\x18', # queues[0].len
+        '\x00\x00', # queues[0].pad
+        '\x00\x01', # queues[0].properties[0].type
+        '\x00\x10', # queues[0].properties[0].length
+        '\x00\x00\x00\x00', # queues[0].properties[0].pad
+        '\x00\x05', # queues[0].properties[0].rate
+        '\x00\x00\x00\x00\x00\x00', # queues[0].properties[0].pad2
+        '\x00\x00\x00\x02', # queues[1].queue_id
+        '\x00\x28', # queues[1].len
+        '\x00\x00', # queues[1].pad
+        '\x00\x01', # queues[1].properties[0].type
+        '\x00\x10', # queues[1].properties[0].length
+        '\x00\x00\x00\x00', # queues[1].properties[0].pad
+        '\x00\x06', # queues[1].properties[0].rate
+        '\x00\x00\x00\x00\x00\x00', # queues[1].properties[0].pad2
+        '\x00\x01', # queues[1].properties[1].type
+        '\x00\x10', # queues[1].properties[1].length
+        '\x00\x00\x00\x00', # queues[1].properties[1].pad
+        '\x00\x07', # queues[1].properties[1].rate
+        '\x00\x00\x00\x00\x00\x00', # queues[1].properties[1].pad2
+    ])
+
+    def test_queue_get_config_reply_pack(self):
+        import loxi.of10 as ofp
+        msg = ofp.message.queue_get_config_reply(
+            xid=0x12345678,
+            port=ofp.OFPP_LOCAL,
+            queues=[
+                ofp.packet_queue(queue_id=1, properties=[
+                    ofp.queue_prop_min_rate(rate=5)]),
+                ofp.packet_queue(queue_id=2, properties=[
+                    ofp.queue_prop_min_rate(rate=6),
+                    ofp.queue_prop_min_rate(rate=7)])])
+        self.assertEquals(self.sample_queue_get_config_reply_buf, msg.pack())
+
+    def test_queue_get_config_reply_unpack(self):
+        import loxi.of10 as ofp
+        msg = ofp.message.queue_get_config_reply.unpack(self.sample_queue_get_config_reply_buf)
+        self.assertEquals(ofp.OFPP_LOCAL, msg.port)
+        self.assertEquals(msg.queues[0].queue_id, 1)
+        self.assertEquals(msg.queues[0].properties[0].rate, 5)
+        self.assertEquals(msg.queues[1].queue_id, 2)
+        self.assertEquals(msg.queues[1].properties[0].rate, 6)
+        self.assertEquals(msg.queues[1].properties[1].rate, 7)
+
+class TestParse(unittest.TestCase):
+    def test_parse_header(self):
+        import loxi
+        import loxi.of10 as ofp
+
+        msg_ver, msg_type, msg_len, msg_xid = ofp.message.parse_header("\x01\x04\xAF\xE8\x12\x34\x56\x78")
+        self.assertEquals(1, msg_ver)
+        self.assertEquals(4, msg_type)
+        self.assertEquals(45032, msg_len)
+        self.assertEquals(0x12345678, msg_xid)
+
+        with self.assertRaisesRegexp(loxi.ProtocolError, "too short"):
+            ofp.message.parse_header("\x01\x04\xAF\xE8\x12\x34\x56")
+
+    def test_parse_message(self):
+        import loxi
+        import loxi.of10 as ofp
+
+        buf = "\x01\x00\x00\x08\x12\x34\x56\x78"
+        msg = ofp.message.parse_message(buf)
+        assert(msg.xid == 0x12345678)
+
+        # Get a list of all 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]
+
+        for klass in test_klasses:
+            self.assertIsInstance(ofp.message.parse_message(klass(xid=1).pack()), klass)
+
+class TestUtils(unittest.TestCase):
+    def test_unpack_array(self):
+        import loxi
+        import loxi.of10.util as util
+
+        a = util.unpack_array(str, 3, "abcdefghi")
+        self.assertEquals(['abc', 'def', 'ghi'], a)
+
+        with self.assertRaisesRegexp(loxi.ProtocolError, "invalid array length"):
+            util.unpack_array(str, 3, "abcdefgh")
+
+    def test_pretty_wildcards(self):
+        import loxi.of10 as ofp
+        self.assertEquals("OFPFW_ALL", ofp.util.pretty_wildcards(ofp.OFPFW_ALL))
+        self.assertEquals("0", ofp.util.pretty_wildcards(0))
+        self.assertEquals("OFPFW_DL_SRC|OFPFW_DL_DST",
+                          ofp.util.pretty_wildcards(ofp.OFPFW_DL_SRC|ofp.OFPFW_DL_DST))
+        self.assertEquals("OFPFW_NW_SRC_MASK&0x2000",
+                          ofp.util.pretty_wildcards(ofp.OFPFW_NW_SRC_ALL))
+        self.assertEquals("OFPFW_NW_SRC_MASK&0x1a00",
+                          ofp.util.pretty_wildcards(0x00001a00))
+        self.assertEquals("OFPFW_IN_PORT|0x80000000",
+                          ofp.util.pretty_wildcards(ofp.OFPFW_IN_PORT|0x80000000))
+
+class TestAll(unittest.TestCase):
+    """
+    Round-trips every class through serialization/deserialization.
+    Not a replacement for handcoded tests because it only uses the
+    default member values.
+    """
+
+    def setUp(self):
+        import loxi.of10 as ofp
+        mods = [ofp.action,ofp.message,ofp.common]
+        self.klasses = [klass for mod in mods
+                              for klass in mod.__dict__.values()
+                              if hasattr(klass, 'show')]
+        self.klasses.sort(key=lambda x: str(x))
+
+    def test_serialization(self):
+        import loxi.of10 as ofp
+        expected_failures = []
+        for klass in self.klasses:
+            def fn():
+                obj = klass()
+                if hasattr(obj, "xid"): obj.xid = 42
+                buf = obj.pack()
+                obj2 = klass.unpack(buf)
+                self.assertEquals(obj, obj2)
+            if klass in expected_failures:
+                self.assertRaises(Exception, fn)
+            else:
+                fn()
+
+    def test_show(self):
+        import loxi.of10 as ofp
+        expected_failures = []
+        for klass in self.klasses:
+            def fn():
+                obj = klass()
+                if hasattr(obj, "xid"): obj.xid = 42
+                obj.show()
+            if klass in expected_failures:
+                self.assertRaises(Exception, fn)
+            else:
+                fn()
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/py_gen/util.py b/py_gen/util.py
new file mode 100644
index 0000000..fbb2825
--- /dev/null
+++ b/py_gen/util.py
@@ -0,0 +1,79 @@
+# Copyright 2013, Big Switch Networks, Inc.
+#
+# LoxiGen is licensed under the Eclipse Public License, version 1.0 (EPL), with
+# the following special exception:
+#
+# LOXI Exception
+#
+# As a special exception to the terms of the EPL, you may distribute libraries
+# generated by LoxiGen (LoxiGen Libraries) under the terms of your choice, provided
+# that copyright and licensing notices generated by LoxiGen are not altered or removed
+# from the LoxiGen Libraries and the notice provided below is (i) included in
+# the LoxiGen Libraries, if distributed in source code form and (ii) included in any
+# documentation for the LoxiGen Libraries, if distributed in binary form.
+#
+# Notice: "Copyright 2013, Big Switch Networks, Inc. This library was generated by the LoxiGen Compiler."
+#
+# You may not use this file except in compliance with the EPL or LOXI Exception. You may obtain
+# a copy of the EPL at:
+#
+# http://www.eclipse.org/legal/epl-v10.html
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# EPL for the specific language governing permissions and limitations
+# under the EPL.
+
+"""
+Utilities for generating the target Python code
+"""
+
+import os
+import of_g
+import loxi_front_end.type_maps as type_maps
+import loxi_utils.loxi_utils as utils
+
+templates_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'templates')
+
+def render_template(out, name, **context):
+    utils.render_template(out, name, [templates_dir], context)
+
+def render_static(out, name):
+    utils.render_static(out, name, [templates_dir])
+
+def lookup_unified_class(cls, version):
+    unified_class = of_g.unified[cls][version]
+    if "use_version" in unified_class: # deref version ref
+        ref_version = unified_class["use_version"]
+        unified_class = of_g.unified[cls][ref_version]
+    return unified_class
+
+def primary_wire_type(cls, version):
+    if cls in type_maps.stats_reply_list:
+        return type_maps.type_val[("of_stats_reply", version)]
+    elif cls in type_maps.stats_request_list:
+        return type_maps.type_val[("of_stats_request", version)]
+    elif cls in type_maps.flow_mod_list:
+        return type_maps.type_val[("of_flow_mod", version)]
+    elif (cls, version) in type_maps.type_val:
+        return type_maps.type_val[(cls, version)]
+    elif type_maps.message_is_extension(cls, version):
+        return type_maps.type_val[("of_experimenter", version)]
+    elif type_maps.action_is_extension(cls, version):
+        return type_maps.type_val[("of_action_experimenter", version)]
+    elif type_maps.action_id_is_extension(cls, version):
+        return type_maps.type_val[("of_action_id_experimenter", version)]
+    elif type_maps.instruction_is_extension(cls, version):
+        return type_maps.type_val[("of_instruction_experimenter", version)]
+    elif type_maps.queue_prop_is_extension(cls, version):
+        return type_maps.type_val[("of_queue_prop_experimenter", version)]
+    elif type_maps.table_feature_prop_is_extension(cls, version):
+        return type_maps.type_val[("of_table_feature_prop_experimenter", version)]
+    else:
+        raise ValueError
+
+def constant_for_value(version, group, value):
+    return (["const." + v["ofp_name"] for k, v in of_g.identifiers.items()
+             if k in of_g.identifiers_by_group[group] and
+                eval(v["values_by_version"].get(version, "None")) == value] or [value])[0]