[plum] Tutorial: Customizing Structure and BitFields Methods

This tutorial demonstrates how to generate method implementations for subclasses of Structure and class:BitFields to override their behavior.

The tutorial examples use the following setup:

>>> from plum.bigendian import uint8, uint16
>>> from plum.bitfields import BitFields, bitfield
>>> from plum.structure import Structure, member

Structure Methods

To generate a method implementation, write a traditional Structure subclass but assign a comma separated string to the Structure.implementation property naming each method you want a generated implementation captured for (omitting leading and trailing underscores). For example:

class MyStruct(Structure):
    m1: int = member(fmt=uint16)
    m2: int = member(fmt=uint8)

    Structure.implementation = "init,pack,unpack"

When first imported, the source file is modified and the generated code appears where the assignment was:

class MyStruct(Structure):
    m1: int = member(fmt=uint16)
    m2: int = member(fmt=uint8)

    def __init__(self, *, m1: int, m2: int) -> None:
        self[:] = (m1, m2)

    @classmethod
    def __pack__(cls, value, pieces: List[bytes], dump: Optional[Record] = None) -> None:
        if isinstance(value, dict):
            value = cls._make_structure_from_dict(value)

        (m_m1, m_m2) = value

        if dump is None:
            uint16.__pack__(m_m1, pieces, dump)

            uint8.__pack__(m_m2, pieces, dump)

        else:
            m1_dump = dump.add_record(access="m1", fmt=uint16)
            uint16.__pack__(m_m1, pieces, m1_dump)

            m2_dump = dump.add_record(access="m2", fmt=uint8)
            uint8.__pack__(m_m2, pieces, m2_dump)

    @classmethod
    def __unpack__(cls, buffer: bytes, offset: int, dump: Optional[Record] = None) -> Tuple["MyStruct", int]:
        structure = list.__new__(cls)

        if dump is None:
            m_m1, offset = uint16.__unpack__(buffer, offset, dump)

            m_m2, offset = uint8.__unpack__(buffer, offset, dump)

        else:
            m1_dump = dump.add_record(access="m1", fmt=uint16)
            m_m1, offset = uint16.__unpack__(buffer, offset, m1_dump)

            m2_dump = dump.add_record(access="m2", fmt=uint8)
            m_m2, offset = uint8.__unpack__(buffer, offset, m2_dump)

        structure[:] = (m_m1, m_m2)

        return structure, offset

It’s beneficial to capture __init__ for the benefit of IDE type ahead or facilitate static code checkers. Modifying __pack__ and __unpack__ methods allows custom behaviors for member interactions that can’t be accomplished by stock features in plum.

To see all of the available generated implementations, assign “all”. For example:

class MyStruct(Structure):
    m1: int = member(fmt=uint16)
    m2: int = member(fmt=uint8)

    Structure.implementation = "all"

BitFields Methods

To generate a method implementation, write a traditional BitFields subclass and assign a comma separated string to the BitFields.implementation property naming each method you want a generated implementation captured for (omitting leading and trailing underscores). For example:

class MyBitFields(BitFields):
    m1: int = bitfield(lsb=0, size=4)
    m2: int = bitfield(lsb=4, size=4)

    BitFields.implementation = "init,repr"

When first imported, the source file is modified and the generated code appears where the assignment was:

class MyBitFields(BitFields):
    m1: int = bitfield(lsb=0, size=4)
    m2: int = bitfield(lsb=4, size=4)

    def __init__(self, *, m1: int, m2: int) -> None:
        self.__value__ = 0
        self.m1 = m1
        self.m2 = m2

    def __repr__(self) -> str:
        try:
            return f"{type(self).__name__}(m1={self.m1!r}, m2={self.m2!r})"
        except Exception:
            return f"{type(self).__name__}()"