# # This file is part of usb-protocol. # """ Helpers for creating construct-related emitters. """ import unittest import construct class ConstructEmitter: """ Class that creates a simple emitter based on a construct struct. For example, if we have a construct format that looks like the following: MyStruct = struct( "a" / Int8 "b" / Int8 ) We could create emit an object like follows: emitter = ConstructEmitter(MyStruct) emitter.a = 0xab emitter.b = 0xcd my_bytes = emitter.emit() # "\xab\xcd" """ def __init__(self, struct): """ Parmeters: construct_format -- The format for which to create an emitter. """ self.__dict__['format'] = struct self.__dict__['fields'] = {} def _format_contains_field(self, field_name): """ Returns True iff the given format has a field with the provided name. Parameters: format_object -- The Construct format to work with. This includes e.g. most descriptor types. field_name -- The field name to query. """ return any(f.name == field_name for f in self.format.subcons) def __setattr__(self, name, value): """ Hook that we used to set our fields. """ # If the field starts with a '_', don't handle it, as it's an internal field. if name.startswith('_'): super().__setattr__(name, value) return if not self._format_contains_field(name): raise AttributeError(f"emitter specification contains no field {name}") self.fields[name] = value def emit(self): """ Emits the stream of bytes associated with this object. """ try: return self.format.build(self.fields) except KeyError as e: raise KeyError(f"missing necessary field: {e}") def __getattr__(self, name): """ Retrieves an emitter field, if possible. """ if name in self.fields: return self.fields[name] else: raise AttributeError(f"descriptor emitter has no property {name}") class ConstructEmitterTest(unittest.TestCase): def test_simple_emitter(self): test_struct = construct.Struct( "a" / construct.Int8ul, "b" / construct.Int8ul ) emitter = ConstructEmitter(test_struct) emitter.a = 0xab emitter.b = 0xcd self.assertEqual(emitter.emit(), b"\xab\xcd") def emitter_for_format(construct_format): """ Creates a factory method for the relevant construct format. """ def _factory(): return ConstructEmitter(construct_format) return _factory if __name__ == "__main__": unittest.main()