|
1 | 1 | import itertools |
2 | | -from collections.abc import Callable |
| 2 | +from collections.abc import Mapping |
3 | 3 | from dataclasses import field |
4 | | -from typing import Annotated, Any |
| 4 | +from types import MappingProxyType |
| 5 | +from typing import Annotated, ClassVar |
5 | 6 |
|
6 | | -from pystructtype.structdataclass import StructDataclass, struct_dataclass |
| 7 | +from pystructtype.structdataclass import StructDataclass |
7 | 8 | from pystructtype.structtypes import TypeMeta |
8 | 9 | from pystructtype.utils import int_to_bool_list |
9 | 10 |
|
10 | 11 |
|
11 | 12 | class BitsType(StructDataclass): |
12 | 13 | """ |
13 | | - Class to auto-magically decode/encode struct data into separate variables |
14 | | - for separate bits based on the given definition |
| 14 | + Base class for bitfield structs. Subclasses must define __bits_type__ and __bits_definition__. |
15 | 15 | """ |
16 | 16 |
|
17 | | - _raw: Any |
| 17 | + __bits_type__: ClassVar[type] |
| 18 | + __bits_definition__: ClassVar[dict[str, int | list[int]] | Mapping[str, int | list[int]]] |
| 19 | + |
| 20 | + _raw: int |
18 | 21 | _meta: dict[str, int | list[int]] |
19 | | - _meta_tuple: tuple[tuple[str, ...], tuple[int | list[int], ...]] |
| 22 | + |
| 23 | + def __init_subclass__(cls, **kwargs): |
| 24 | + super().__init_subclass__(**kwargs) |
| 25 | + # Check for required attributes |
| 26 | + if not hasattr(cls, "__bits_type__") or not hasattr(cls, "__bits_definition__"): |
| 27 | + raise TypeError( |
| 28 | + "Subclasses of BitsType must define __bits_type__ and __bits_definition__ class attributes." |
| 29 | + ) |
| 30 | + bits_type = cls.__bits_type__ |
| 31 | + definition = cls.__bits_definition__ |
| 32 | + |
| 33 | + # Automatically wrap in MappingProxyType if it's a dict and not already immutable |
| 34 | + if isinstance(definition, dict) and not isinstance(definition, MappingProxyType): |
| 35 | + definition = MappingProxyType(definition) |
| 36 | + cls.__bits_definition__ = definition |
| 37 | + |
| 38 | + # # Remove __bits_type__ and __bits_definition__ from __annotations__ if present |
| 39 | + # cls.__annotations__.pop("__bits_type__", None) |
| 40 | + # cls.__annotations__.pop("__bits_definition__", None) |
| 41 | + |
| 42 | + # Set the correct type for the raw data |
| 43 | + cls._raw = 0 |
| 44 | + cls.__annotations__["_raw"] = bits_type |
| 45 | + |
| 46 | + cls._meta = field(default_factory=dict) |
| 47 | + |
| 48 | + # Create the defined attributes, defaults, and annotations in the class |
| 49 | + for key, value in definition.items(): |
| 50 | + if isinstance(value, list): |
| 51 | + setattr( |
| 52 | + cls, |
| 53 | + key, |
| 54 | + field(default_factory=lambda v=len(value): [False for _ in range(v)]), # type: ignore |
| 55 | + ) |
| 56 | + cls.__annotations__[key] = Annotated[list[bool], TypeMeta(size=len(value))] |
| 57 | + else: |
| 58 | + setattr(cls, key, False) |
| 59 | + cls.__annotations__[key] = bool |
20 | 60 |
|
21 | 61 | def __post_init__(self) -> None: |
22 | 62 | super().__post_init__() |
23 | | - |
24 | | - # Convert the _meta_tuple data into a dictionary and put it into _meta |
25 | | - self._meta = dict(zip(*self._meta_tuple, strict=False)) |
| 63 | + self._meta = dict(self.__bits_definition__) |
26 | 64 |
|
27 | 65 | def _decode(self, data: list[int]) -> None: |
28 | | - """ |
29 | | - Internal decoding function |
30 | | -
|
31 | | - :param data: A list of ints to decode |
32 | | - """ |
33 | | - # First call the super function to put the values in to _raw |
34 | 66 | super()._decode(data) |
35 | | - |
36 | | - # Combine all data in _raw as binary and convert to bools |
37 | 67 | bin_data = int_to_bool_list(self._raw, self._byte_length) |
38 | | - |
39 | | - # Apply bits to the defined structure |
40 | 68 | for k, v in self._meta.items(): |
41 | 69 | if isinstance(v, list): |
42 | | - steps = [] |
43 | | - for idx in v: |
44 | | - steps.append(bin_data[idx]) |
| 70 | + steps = [bin_data[idx] for idx in v] |
45 | 71 | setattr(self, k, steps) |
46 | 72 | else: |
47 | 73 | setattr(self, k, bin_data[v]) |
48 | 74 |
|
49 | 75 | def _encode(self) -> list[int]: |
50 | | - """ |
51 | | - Internal encoding function |
52 | | -
|
53 | | - :returns: A list of encoded ints |
54 | | - """ |
55 | | - # Fill a correctly sized variable with all False/0 bits |
56 | 76 | bin_data = list(itertools.repeat(False, self._byte_length * 8)) |
57 | | - |
58 | | - # Assign the correct values from the defined attributes into bin_data |
59 | 77 | for k, v in self._meta.items(): |
60 | 78 | if isinstance(v, list): |
61 | 79 | steps = getattr(self, k) |
62 | 80 | for idx, bit_idx in enumerate(v): |
63 | 81 | bin_data[bit_idx] = steps[idx] |
64 | 82 | else: |
65 | 83 | bin_data[v] = getattr(self, k) |
66 | | - |
67 | | - # Convert bin_data back into their correct integer locations |
68 | 84 | self._raw = sum(int(v) << i for i, v in enumerate(bin_data)) |
69 | | - |
70 | | - # Run the super function to return the data in self._raw() |
| 85 | + # Return _raw as a list of bytes (little-endian) |
71 | 86 | return super()._encode() |
72 | | - |
73 | | - |
74 | | -def bits(_type: Any, definition: dict[str, int | list[int]]) -> Callable[[type[BitsType]], type[StructDataclass]]: |
75 | | - """ |
76 | | - Decorator that does a bunch of metaprogramming magic to properly set up the |
77 | | - defined Subclass of StructDataclass for Bits handling |
78 | | -
|
79 | | - The definition must be a dict of ints or a list of ints. The int values denote the position of the bits. |
80 | | -
|
81 | | - Example: |
82 | | - @bits(uint8_t, {"a": 0, "b": [1, 2, 4], "c": 3}) |
83 | | - class MyBits(BitsType): ... |
84 | | -
|
85 | | - For an uint8_t defined as 0b01010101, the resulting class will be: |
86 | | - MyBits(a=1, b=[0, 1, 1], c=0) |
87 | | -
|
88 | | - :param _type: The type of data that the bits are stored in (ex. uint8_t, etc.) |
89 | | - :param definition: The bits definition that defines attributes and bit locations |
90 | | - :return: A Callable that performs the metaprogramming magic and returns the modified StructDataclass |
91 | | - """ |
92 | | - |
93 | | - def inner(_cls: type[BitsType]) -> type[StructDataclass]: |
94 | | - """ |
95 | | - The inner function to modify a StructDataclass into a BitsType class |
96 | | -
|
97 | | - :param _cls: A Subclass of BitsType |
98 | | - :return: Modified StructDataclass |
99 | | - """ |
100 | | - # Create class attributes based on the definition |
101 | | - # TODO: Maybe a sanity check to make sure the definition is the right format, and no overlapping bits, etc |
102 | | - |
103 | | - new_cls = _cls |
104 | | - |
105 | | - # Set the correct type for the raw data |
106 | | - new_cls.__annotations__["_raw"] = _type |
107 | | - |
108 | | - # Override the annotations for the _meta attribute, and set a default |
109 | | - # TODO: This probably isn't really needed unless we end up changing the int value to bool or something |
110 | | - new_cls._meta = field(default_factory=dict) |
111 | | - new_cls.__annotations__["_meta"] = dict[str, int] |
112 | | - |
113 | | - # Convert the definition to a named tuple, so it's Immutable |
114 | | - meta_tuple = (tuple(definition.keys()), tuple(definition.values())) |
115 | | - new_cls._meta_tuple = field(default_factory=lambda d=meta_tuple: d) # type: ignore |
116 | | - new_cls.__annotations__["_meta_tuple"] = tuple |
117 | | - |
118 | | - # TODO: Support int, or list of ints as defaults |
119 | | - # TODO: Support dict, and dict of lists, or list of dicts, etc for definition |
120 | | - # TODO: ex. definition = {"a": {"b": 0, "c": [1, 2, 3]}, "d": [4, 5, 6], "e": {"f": 7}} |
121 | | - # TODO: Can't decide if the line above this is a good idea or not |
122 | | - # Create the defined attributes, defaults, and annotations in the class |
123 | | - for key, value in definition.items(): |
124 | | - if isinstance(value, list): |
125 | | - # Use Annotated with TypeMeta(size=len=value) for list fields |
126 | | - setattr( |
127 | | - new_cls, |
128 | | - key, |
129 | | - field(default_factory=lambda v=len(value): [False for _ in range(v)]), # type: ignore |
130 | | - ) |
131 | | - new_cls.__annotations__[key] = Annotated[list[bool], TypeMeta(size=len(value))] |
132 | | - else: |
133 | | - setattr(new_cls, key, False) |
134 | | - new_cls.__annotations__[key] = bool |
135 | | - |
136 | | - return struct_dataclass(new_cls) |
137 | | - |
138 | | - return inner |
0 commit comments