[plum] Tutorial: Dynamically Typed Structure Member¶
This tutorial shows how to use features of the Member
class within a
structure definition to control one member’s type based on the value of
another structure member.
>>> from plum.littleendian import uint8, uint16, uint32
>>> from plum.structure import Structure, member
Basics¶
To demonstrate, examine the example below. The datatype
member controls
the data
member’s type. The FMT_MAP
dictionary’s __getitem__()
method serves as a format factory and the fmt_arg
defines the member
value that is passed to the format factory function.
During unpacking of the structure, the the value of the unpacked datatype
member determines the format of the data
member per the defined mapping:
>>> FMT_MAP = {0: uint8, 1: uint16, 2: uint32}
>>>
>>> class Struct1(Structure):
... datatype: int = member(fmt=uint8)
... data: int = member(fmt=FMT_MAP.__getitem__, fmt_arg=datatype)
...
>>> struct1 = Struct1.unpack(b'\x01\x02\x00')
>>> struct1
Struct1(datatype=1, data=2)
>>> struct1.dump()
+--------+----------+-------+-------+---------------------+
| Offset | Access | Value | Bytes | Format |
+--------+----------+-------+-------+---------------------+
| | | | | Struct1 (Structure) |
| 0 | datatype | 1 | 01 | uint8 |
| 1 | data | 2 | 02 00 | uint16 |
+--------+----------+-------+-------+---------------------+
Note
When fmt_arg
is specified, the format factory is passed just the value
of the specified structure member. When left unspecified, the entire
structure instances is passed giving access to all available members (as
shown in the example in the next section).
Similarly in packing operations, the value of the datatype
member determines
the format of the data
member bytes:
>>> # datatype -> uint8 (0)
>>> Struct1(datatype=0, data=2).ipack()
b'\x00\x02'
>>>
>>> # datatype -> uint16 (1)
>>> Struct1(datatype=1, data=2).ipack()
b'\x01\x02\x00'
Custom Variable Member Type¶
The Member
definition class accepts factory functions (or properties)
for both the fmt
and default
arguments. The following example shows a
more sophisticated application of factory functions that determine the type
based the magnitude of the value.
>>> class Sample(Structure):
...
... def determine_datatype(self):
... return 1 if self.data >= 256 else 0
...
... @property
... def data_cls(self):
... return uint16 if self.datatype else uint8
...
... datatype: int = member(fmt=uint8, default=determine_datatype)
... data: int = member(fmt=data_cls)
...
... @data.setter
... def data(self, value):
... self[1] = value # use index to avoid recursion
... self.datatype = self.determine_datatype()
The data_cls
property (which could have been a simple function
by leaving @property
decorator off) used as the fmt
of the
data
member controls the class used when packing and unpacking
the data
member. The data_cls
factory function facilitates
leaving the datatype
member unspecified when instantiation or
packing and having it determined based on the value magnitude in the
data
member. For example:
>>> # integer that fits in a uint8
>>> Sample(data=0).dump()
+--------+----------+-------+-------+--------------------+
| Offset | Access | Value | Bytes | Format |
+--------+----------+-------+-------+--------------------+
| | | | | Sample (Structure) |
| 0 | datatype | 0 | 00 | uint8 |
| 1 | data | 0 | 00 | uint8 |
+--------+----------+-------+-------+--------------------+
>>>
>>> # integer that requires a uint16
>>> Sample(data=256).dump()
+--------+----------+-------+-------+--------------------+
| Offset | Access | Value | Bytes | Format |
+--------+----------+-------+-------+--------------------+
| | | | | Sample (Structure) |
| 0 | datatype | 1 | 01 | uint8 |
| 1 | data | 256 | 00 01 | uint16 |
+--------+----------+-------+-------+--------------------+
The setter
decorator of the data
property overrides the normal
attribute set mechanism for the data
member. In addition to storing the new
value, it also updates the datatype
member to reflect the new data
value.
>>> # 'datatype' starts out as uint8
>>> sample = Sample(data=0)
>>> sample.dump()
+--------+----------+-------+-------+--------------------+
| Offset | Access | Value | Bytes | Format |
+--------+----------+-------+-------+--------------------+
| | | | | Sample (Structure) |
| 0 | datatype | 0 | 00 | uint8 |
| 1 | data | 0 | 00 | uint8 |
+--------+----------+-------+-------+--------------------+
>>>
>>> # but changes based on setting 'data' attribute to a new value
>>> sample.data = 256
>>> sample.dump()
+--------+----------+-------+-------+--------------------+
| Offset | Access | Value | Bytes | Format |
+--------+----------+-------+-------+--------------------+
| | | | | Sample (Structure) |
| 0 | datatype | 1 | 01 | uint8 |
| 1 | data | 256 | 00 01 | uint16 |
+--------+----------+-------+-------+--------------------+
Instead of providing a default factory function for the datatype
member, consider providing a complete implementation for __init__
(rather than relying on the __init__
that would otherwise be
generated). Besides being more explicit, it helps IDEs offer type ahead
hints when instantiating the structure:
>>> class Sample(Structure):
...
... def __init__(self, data, datatype=None):
... if datatype is None:
... datatype = 1 if data >> 8 else 0
...
... self[:] = datatype, data
...
... @property
... def data_cls(self):
... return uint16 if self.datatype else uint8
...
... datatype: int = member(fmt=uint8) # no default
... data: int = member(fmt=data_cls)
...
... @data.setter
... def data(self, value):
... self[1] = value # use index to avoid recursion
... self.datatype = 1 if value >> 8 else 0
...
>>> # and works the same
>>> sample = Sample(data=0)
>>> sample.dump()
+--------+----------+-------+-------+--------------------+
| Offset | Access | Value | Bytes | Format |
+--------+----------+-------+-------+--------------------+
| | | | | Sample (Structure) |
| 0 | datatype | 0 | 00 | uint8 |
| 1 | data | 0 | 00 | uint8 |
+--------+----------+-------+-------+--------------------+
>>>
>>> sample.data = 256
>>> sample.dump()
+--------+----------+-------+-------+--------------------+
| Offset | Access | Value | Bytes | Format |
+--------+----------+-------+-------+--------------------+
| | | | | Sample (Structure) |
| 0 | datatype | 1 | 01 | uint8 |
| 1 | data | 256 | 00 01 | uint16 |
+--------+----------+-------+-------+--------------------+