Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 5 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ struct MyStruct {
```

```python
@struct_dataclass
class MyStruct(StructDataclass):
myNum: int16_t
myLetter: char_t
Expand All @@ -61,7 +60,6 @@ struct MyStruct {
};
```
```python
@struct_dataclass
class MyStruct(StructDataclass):
myInts: Annotated[list[uint8_t], TypeMeta(size=4)]
myBiggerInts: Annotated[list[uint16_t], TypeMeta(size=2)]
Expand All @@ -87,7 +85,6 @@ struct MyStruct {
```

```python
@struct_dataclass
class MyStruct(StructDataclass):
myInt: uint8_t = 5
myInts: Annnotated[list[uint8_t], TypeMeta(size=2, default=1)]
Expand Down Expand Up @@ -117,7 +114,6 @@ struct MyStruct {
};
```
```python
@struct_dataclass
class MyStruct(StructDataclass):
myStr: Annotated[string_t, TypeMeta[str](chunk_size=3)]
myStrList: Annotated[list[string_t], TypeMeta[str](size=2, chunk_size=3)]
Expand Down Expand Up @@ -147,8 +143,9 @@ enum ConfigFlags {
```

```python
@bits(uint8_t, {"lights_flag": 0, "platform_flag": 1})
class FlagsType(BitsType): ...
class FlagsType(BitsType):
__bits_type__ = uint8_t
__bits_definition__ = {"lights_flag": 0, "platform_flag": 1}

f = FlagsType()
f.decode([3])
Expand Down Expand Up @@ -178,7 +175,6 @@ struct MyStruct {
```

```python
@struct_dataclass
class EnabledSensors(StructDataclass):
# We can define the actual data we are ingesting here
# This mirrors the `uint8_t enabledSensors[5]` data
Expand Down Expand Up @@ -264,15 +260,13 @@ struct LEDS {
```

```python
@struct_dataclass
class RGB(StructDataclass):
r: uint8_t
g: uint8_t
b: uint8_t

@struct_dataclass
class LEDS(StructDataclass):
lights: Annotated[list[RGB], TypeMeta(size=3])]
lights: Annotated[list[RGB], TypeMeta(size=3)]

l = LEDS()
l.decode([1, 2, 3, 4, 5, 6, 7, 8, 9])
Expand All @@ -288,4 +282,4 @@ l.decode([1, 2, 3, 4, 5, 6, 7, 8, 9])

# Examples

You can see a more fully fledged example in the `test/examples.py` file.
You can see a more fully fledged example in the `test/examples.py` file.
6 changes: 2 additions & 4 deletions src/pystructtype/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from pystructtype.bitstype import BitsType, bits
from pystructtype.structdataclass import StructDataclass, struct_dataclass
from pystructtype.bitstype import BitsType
from pystructtype.structdataclass import StructDataclass
from pystructtype.structtypes import (
TypeInfo,
TypeMeta,
Expand All @@ -22,7 +22,6 @@
"StructDataclass",
"TypeInfo",
"TypeMeta",
"bits",
"char_t",
"double_t",
"float_t",
Expand All @@ -31,7 +30,6 @@
"int32_t",
"int64_t",
"string_t",
"struct_dataclass",
"uint8_t",
"uint16_t",
"uint32_t",
Expand Down
152 changes: 50 additions & 102 deletions src/pystructtype/bitstype.py
Original file line number Diff line number Diff line change
@@ -1,138 +1,86 @@
import itertools
from collections.abc import Callable
from collections.abc import Mapping
from dataclasses import field
from typing import Annotated, Any
from types import MappingProxyType
from typing import Annotated, ClassVar

from pystructtype.structdataclass import StructDataclass, struct_dataclass
from pystructtype.structdataclass import StructDataclass
from pystructtype.structtypes import TypeMeta
from pystructtype.utils import int_to_bool_list


class BitsType(StructDataclass):
"""
Class to auto-magically decode/encode struct data into separate variables
for separate bits based on the given definition
Base class for bitfield structs. Subclasses must define __bits_type__ and __bits_definition__.
"""

_raw: Any
__bits_type__: ClassVar[type]
__bits_definition__: ClassVar[dict[str, int | list[int]] | Mapping[str, int | list[int]]]

_raw: int
_meta: dict[str, int | list[int]]
_meta_tuple: tuple[tuple[str, ...], tuple[int | list[int], ...]]

def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
# Check for required attributes
if not hasattr(cls, "__bits_type__") or not hasattr(cls, "__bits_definition__"):
raise TypeError(
"Subclasses of BitsType must define __bits_type__ and __bits_definition__ class attributes."
)
bits_type = cls.__bits_type__
definition = cls.__bits_definition__

# Automatically wrap in MappingProxyType if it's a dict and not already immutable
if isinstance(definition, dict) and not isinstance(definition, MappingProxyType):
definition = MappingProxyType(definition)
cls.__bits_definition__ = definition

# # Remove __bits_type__ and __bits_definition__ from __annotations__ if present
# cls.__annotations__.pop("__bits_type__", None)
# cls.__annotations__.pop("__bits_definition__", None)

# Set the correct type for the raw data
cls._raw = 0
cls.__annotations__["_raw"] = bits_type

cls._meta = field(default_factory=dict)

# Create the defined attributes, defaults, and annotations in the class
for key, value in definition.items():
if isinstance(value, list):
setattr(
cls,
key,
field(default_factory=lambda v=len(value): [False for _ in range(v)]), # type: ignore
)
cls.__annotations__[key] = Annotated[list[bool], TypeMeta(size=len(value))]
else:
setattr(cls, key, False)
cls.__annotations__[key] = bool

def __post_init__(self) -> None:
super().__post_init__()

# Convert the _meta_tuple data into a dictionary and put it into _meta
self._meta = dict(zip(*self._meta_tuple, strict=False))
self._meta = dict(self.__bits_definition__)

def _decode(self, data: list[int]) -> None:
"""
Internal decoding function

:param data: A list of ints to decode
"""
# First call the super function to put the values in to _raw
super()._decode(data)

# Combine all data in _raw as binary and convert to bools
bin_data = int_to_bool_list(self._raw, self._byte_length)

# Apply bits to the defined structure
for k, v in self._meta.items():
if isinstance(v, list):
steps = []
for idx in v:
steps.append(bin_data[idx])
steps = [bin_data[idx] for idx in v]
setattr(self, k, steps)
else:
setattr(self, k, bin_data[v])

def _encode(self) -> list[int]:
"""
Internal encoding function

:returns: A list of encoded ints
"""
# Fill a correctly sized variable with all False/0 bits
bin_data = list(itertools.repeat(False, self._byte_length * 8))

# Assign the correct values from the defined attributes into bin_data
for k, v in self._meta.items():
if isinstance(v, list):
steps = getattr(self, k)
for idx, bit_idx in enumerate(v):
bin_data[bit_idx] = steps[idx]
else:
bin_data[v] = getattr(self, k)

# Convert bin_data back into their correct integer locations
self._raw = sum(int(v) << i for i, v in enumerate(bin_data))

# Run the super function to return the data in self._raw()
# Return _raw as a list of bytes (little-endian)
return super()._encode()


def bits(_type: Any, definition: dict[str, int | list[int]]) -> Callable[[type[BitsType]], type[StructDataclass]]:
"""
Decorator that does a bunch of metaprogramming magic to properly set up the
defined Subclass of StructDataclass for Bits handling

The definition must be a dict of ints or a list of ints. The int values denote the position of the bits.

Example:
@bits(uint8_t, {"a": 0, "b": [1, 2, 4], "c": 3})
class MyBits(BitsType): ...

For an uint8_t defined as 0b01010101, the resulting class will be:
MyBits(a=1, b=[0, 1, 1], c=0)

:param _type: The type of data that the bits are stored in (ex. uint8_t, etc.)
:param definition: The bits definition that defines attributes and bit locations
:return: A Callable that performs the metaprogramming magic and returns the modified StructDataclass
"""

def inner(_cls: type[BitsType]) -> type[StructDataclass]:
"""
The inner function to modify a StructDataclass into a BitsType class

:param _cls: A Subclass of BitsType
:return: Modified StructDataclass
"""
# Create class attributes based on the definition
# TODO: Maybe a sanity check to make sure the definition is the right format, and no overlapping bits, etc

new_cls = _cls

# Set the correct type for the raw data
new_cls.__annotations__["_raw"] = _type

# Override the annotations for the _meta attribute, and set a default
# TODO: This probably isn't really needed unless we end up changing the int value to bool or something
new_cls._meta = field(default_factory=dict)
new_cls.__annotations__["_meta"] = dict[str, int]

# Convert the definition to a named tuple, so it's Immutable
meta_tuple = (tuple(definition.keys()), tuple(definition.values()))
new_cls._meta_tuple = field(default_factory=lambda d=meta_tuple: d) # type: ignore
new_cls.__annotations__["_meta_tuple"] = tuple

# TODO: Support int, or list of ints as defaults
# TODO: Support dict, and dict of lists, or list of dicts, etc for definition
# TODO: ex. definition = {"a": {"b": 0, "c": [1, 2, 3]}, "d": [4, 5, 6], "e": {"f": 7}}
# TODO: Can't decide if the line above this is a good idea or not
# Create the defined attributes, defaults, and annotations in the class
for key, value in definition.items():
if isinstance(value, list):
# Use Annotated with TypeMeta(size=len=value) for list fields
setattr(
new_cls,
key,
field(default_factory=lambda v=len(value): [False for _ in range(v)]), # type: ignore
)
new_cls.__annotations__[key] = Annotated[list[bool], TypeMeta(size=len(value))]
else:
setattr(new_cls, key, False)
new_cls.__annotations__[key] = bool

return struct_dataclass(new_cls)

return inner
Loading
Loading