[plum.int] Tutorial: Creating and Using Integer Enumeration Types

The plum.int.enum subpackage provides a plum type class for packing and unpacking integer enumeration members. Compared to normal integers, enumerations offer better representations within the bytes summary dumps as well as the ability to restrict a value to a specific set of valid values. This tutorial teaches how to create and use integer enumeration plum types.

Creating an Integer Enumeration Type

To create a plum integer enumeration type, subclass the Enum class (similar to subclassing IntEnum from the enum standard library module). Use arguments to control the number of bytes, byte order, and sign:

>>> from plum.int.enum import Enum
>>>
>>> class Register(Enum, nbytes=2, signed=False, byteorder='big'):
...     PC = 0
...     SP = 1
...     R0 = 2
...     R1 = 3
...

Besides having plum behaviors (discussed in next sections), the enumeration behaves as subclasses of enum.IntEnum do. For example:

>>> Register.PC == 0
True
>>> Register(0)
<Register.PC: 0>
>>> Register['PC']
<Register.PC: 0>

Unpacking Bytes

plum integer enumeration types convert bytes into an enumeration member instance when used with the various plum unpacking mechanisms. For example (using the enumeration from the previous section):

>>> from plum import unpack, Buffer
>>>
>>> # utility function
>>> unpack(Register, b'\x00\x00')
<Register.PC: 0>
>>>
>>> # class method
>>> Register.unpack(b'\x00\x01')
<Register.SP: 1>
>>>
>>> # bytes buffer
>>> with Buffer(b'\x00\x00\x00\x01') as buffer:
...     a = buffer.unpack(Register)
...     b = buffer.unpack(Register)
...
>>> a, b
(<Register.PC: 0>, <Register.SP: 1>)

Packing Bytes

plum integer enumeration types convert integers (or anything int-like) into bytes when used with the various plum packing mechanisms. For example (using the enumeration from the previous sections):

>>> from plum import pack
>>>
>>> # utility function
>>> pack(Register, Register.PC)
bytearray(b'\x00\x00')
>>>
>>> # class method
>>> Register.pack(0)
bytearray(b'\x00\x00')
>>>
>>> # instance method
>>> Register.PC.pack()
bytearray(b'\x00\x00')

Strict vs. Tolerant

By default, plum enumeration types require valid values when packing, otherwise the operation raises an exception. For example (using the enumeration from the previous sections):

>>> Register.pack(0xff)
Traceback (most recent call last):
  ...
plum._exceptions.PackError:
<BLANKLINE>
+--------+-------+-------+----------+
| Offset | Value | Bytes | Type     |
+--------+-------+-------+----------+
|        | 255   |       | Register |
+--------+-------+-------+----------+
<BLANKLINE>
ValueError occurred during pack operation:
<BLANKLINE>
255 is not a valid Register

Similarly, plum enumeration types require valid values when unpacking:

>>> Register.unpack(b'\x00\xff')
Traceback (most recent call last):
  ...
plum._exceptions.UnpackError:
<BLANKLINE>
+--------+-------+-------+----------+
| Offset | Value | Bytes | Type     |
+--------+-------+-------+----------+
| 0      |       | 00 ff | Register |
+--------+-------+-------+----------+
<BLANKLINE>
ValueError occurred during unpack operation:
<BLANKLINE>
255 is not a valid Register
>>>

To tolerate values not defined within the enumeration, set the strict argument to False when creating the enumeration type. For example:

>>> from plum.int.enum import Enum
>>>
>>> class Register(Enum, nbytes=2, signed=False, byteorder='big', strict=False):
...     PC = 0
...     SP = 1
...     R0 = 2
...     R1 = 3
...
>>> Register.pack(0xff)
bytearray(b'\x00\xff')
>>> Register.unpack(b'\x00\xff')
255

Inheriting Enumeration Members

To create a plum integer enumeration type with members inherited from another enumeration, subclass the Enum class and pass the enumeration with the members to copy using the source argument. For example:

>>> from enum import IntEnum
>>> from plum.int.enum import Enum
>>>
>>> class Pet(IntEnum):
...     """Normal enumeration without pack/unpack properties."""
...     CAT = 0
...     DOG = 1
...
>>>
>>> class Pet8(Enum, nbytes=1, source=Pet):
...     """Plum enumeration with pack/unpack properties"""
...     LIZARD = 2  # optional additional member
...

The resulting plum enumeration contains members from the source plus members defined in the class body:

>>> for member in Pet8:
...     print(repr(member))
...
<Pet8.CAT: 0>
<Pet8.DOG: 1>
<Pet8.LIZARD: 2>
>>>
>>> Pet8.DOG.dump()
+--------+----------+-------+------+
| Offset | Value    | Bytes | Type |
+--------+----------+-------+------+
| 0      | Pet8.DOG | 01    | Pet8 |
+--------+----------+-------+------+