[plum.structure] Tutorial: Create Custom Structure Type

This tutorial shows how to create custom structure subclasses that define the name, type, default value, and significance for each member within a structure. (The significance of a member determines whether equality comparisons of the structure consider the member.) Predefining the structure members facilitates packing and unpacking the structure’s bytes and ensures instantiations include all necessary member values.

Member Type

To define a custom structure, use a traditional class definition and subclass Structure. Within the namespace of the subclass, use the Member class to specify the member name and member type. As shown, type annotations may be used, but are optional. Within Structure classes, the member definitions control how bytes are packed and unpacked when using the structure type:

>>> from plum import pack, unpack
>>> from plum.structure import Member, Structure
>>> from plum.int.little import UInt8, UInt16
>>>
>>> class MyStruct(Structure):
...     m1: int = Member(cls=UInt8)
...     m2: int = Member(cls=UInt16)
...
>>> pack(MyStruct, {'m1': 1, 'm2': 2})
bytearray(b'\x01\x02\x00')
>>>
>>> unpack(MyStruct, b'\x01\x02\x00')
MyStruct(m1=1, m2=2)

The custom structure class uses the member definitions to ensure proper values are provided during instantiation:

>>> MyStruct(m1=1)
Traceback (most recent call last):
  ...
TypeError: __init__() missing 1 required positional argument: 'm2'
>>>
>>> MyStruct(m1=1, m2=2, m3=3)
Traceback (most recent call last):
  ...
TypeError: __init__() got an unexpected keyword argument 'm3'

Member Default

Use the default argument in the member definition to specify a default value for the member. Instantiations and pack() operations use the default value for the member when one was not provided:

>>> from plum import pack
>>> from plum.structure import Member, Structure
>>> from plum.int.little import UInt8, UInt16
>>>
>>> class MyStruct(Structure):
...     m1: int = Member(cls=UInt8)
...     m2: int = Member(cls=UInt16, default=2)
...
>>> pack(MyStruct, {'m1': 1})
bytearray(b'\x01\x02\x00')
>>>
>>> MyStruct(m1=1)
MyStruct(m1=1, m2=2)

Tip

For equality comparisons, compare against an instance of the class instead of a traditional list and skip members with defaults where the expected value is the default:

>>> # dict compare fails (requires all members, and all members match)
>>> unpack(MyStruct, b'\x01\x02\x00') == [1]
False
>>>
>>> # compare with structure instance instead
>>> unpack(MyStruct, b'\x01\x02\x00') == MyStruct(m1=1)
True

Ignore Member

To ignore certain members when evaluating equality of a structure, use the ignore argument in the member definition:

>>> from plum import pack
>>> from plum.structure import Member, Structure
>>> from plum.int.little import UInt8
>>>
>>> class MyStruct(Structure):
...     m1: int = Member(cls=UInt8)
...     m2: int = Member(cls=UInt8)
...     m3: int = Member(cls=UInt8, default=0, ignore=True)
...
>>> x = unpack(MyStruct, b'\x01\x02\x03')
>>>
>>> # normal dict requires all members be consistent (presence and value)
>>> x.asdict() == {'m1': 1, 'm2': 2}
False
>>>
>>> # structures allow ignored members to be absent or mismatched
>>> x == MyStruct(m1=1, m2=2)  # m3 absent
True
>>> x == MyStruct(m1=1, m2=2, m3=99)  # m3 mismatched
True