Source code for runtimepy.primitives.types.base

"""
A module for implementing a base class for primitive types.
"""

# built-in
import ctypes as _ctypes
from struct import calcsize as _calcsize
from struct import pack as _pack
from struct import unpack as _unpack
from typing import BinaryIO as _BinaryIO
from typing import Generic as _Generic
from typing import Optional as _Optional
from typing import TypeVar as _TypeVar
from typing import Union as _Union
from typing import cast as _cast

# third-party
from vcorelib.io import BinaryMessage

# internal
from runtimepy.primitives.byte_order import (
    DEFAULT_BYTE_ORDER as _DEFAULT_BYTE_ORDER,
)
from runtimepy.primitives.byte_order import ByteOrder as _ByteOrder
from runtimepy.primitives.types.bounds import IntegerBounds

# Integer type aliases.
Int8Ctype = _ctypes.c_byte
Int16Ctype = _ctypes.c_short
Int32Ctype = _ctypes.c_int
Int64Ctype = _ctypes.c_longlong
Uint8Ctype = _ctypes.c_ubyte
Uint16Ctype = _ctypes.c_ushort
Uint32Ctype = _ctypes.c_uint
Uint64Ctype = _ctypes.c_ulonglong

# Floating-point type aliases.
FloatCtype = _ctypes.c_float
DoubleCtype = _ctypes.c_double

# Boolean type alias.
BoolCtype = _ctypes.c_bool

# This variable declaration doesn't seem to work well using unions of types
# (e.g. if "signed integers" and "unsigned integers" were unions of respective
# ctypes, and this was a union of those).
T = _TypeVar(
    "T",
    # Integer types.
    Int8Ctype,
    Int16Ctype,
    Int32Ctype,
    Int64Ctype,
    Uint8Ctype,
    Uint16Ctype,
    Uint32Ctype,
    Uint64Ctype,
    # Floating-point types.
    FloatCtype,
    DoubleCtype,
    # Boolean type.
    BoolCtype,
)

PythonPrimitive = _Union[bool, int, float]


[docs] class PrimitiveType(_Generic[T]): """A simple wrapper around ctype primitives.""" # Sub-classes must set these class attributes. name: str c_type: type[T] python_type: type[PythonPrimitive] def __init__(self, struct_format: str, signed: bool = True) -> None: """Initialize this primitive type.""" self.format = struct_format self.signed = signed self.int_bounds: _Optional[IntegerBounds] = None # Make sure that the struct size and ctype size match. There's # unfortunately no obvious (or via public interfaces) way to just # obtain the intended struct formatter for each ctype. self.size = _calcsize(self.format) self.bits = self.size * 8 c_type_size = _ctypes.sizeof(self.c_type) # This won't match for half-precision floating-point. if self.format != "e": assert self.size == c_type_size, "{self.size} != {c_type_size}!" # Convenient attributes to determine if this type is which one of # Python's primitive types. self.is_boolean = self.c_type == _ctypes.c_bool assert not self.is_boolean or self.python_type is bool self.is_float = any( self.c_type == x for x in [_ctypes.c_float, _ctypes.c_double] ) assert not self.is_float or self.python_type is float self.is_integer = (not self.is_boolean) and (not self.is_float) assert not self.is_integer or self.python_type is int
[docs] @classmethod def valid_primitive(cls, primitive: PythonPrimitive) -> bool: """Determine if a Python primitive is valid for this class.""" return isinstance(primitive, cls.python_type)
def __str__(self) -> str: """Get this type as a string.""" return self.name def __hash__(self) -> int: """Get a hash for this type.""" return hash(self.c_type) def __eq__(self, other) -> bool: """Determine if two types are equal.""" if isinstance(other, PrimitiveType): other = other.c_type return _cast(bool, self.c_type == other)
[docs] @classmethod def instance(cls) -> T: """Get an instance of this primitive type.""" return cls.c_type()
[docs] def encode( self, value: PythonPrimitive, byte_order: _ByteOrder = _DEFAULT_BYTE_ORDER, ) -> bytes: """Encode a primitive value.""" return _pack(byte_order.fmt + self.format, value)
[docs] def decode( self, data: BinaryMessage, byte_order: _ByteOrder = _DEFAULT_BYTE_ORDER ) -> PythonPrimitive: """Decode primitive based on this type.""" return _unpack(byte_order.fmt + self.format, data)[0] # type: ignore
[docs] def read( self, stream: _BinaryIO, byte_order: _ByteOrder = _DEFAULT_BYTE_ORDER ) -> PythonPrimitive: """Read a primitive from a stream based on this type.""" return self.decode(stream.read(self.size), byte_order=byte_order)
[docs] def write( self, value: PythonPrimitive, stream: _BinaryIO, byte_order: _ByteOrder = _DEFAULT_BYTE_ORDER, ) -> int: """Write a primitive to a stream based on this type.""" return stream.write(self.encode(value, byte_order=byte_order))