Skip to content

Commit 64b27f7

Browse files
authored
Merge pull request #6 from fchorney/fc/more-cleanup
Big Refactor and More Cleanup
2 parents f1c2692 + 85c7371 commit 64b27f7

10 files changed

Lines changed: 222 additions & 402 deletions

README.md

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ struct MyStruct {
3434
```
3535

3636
```python
37-
@struct_dataclass
3837
class MyStruct(StructDataclass):
3938
myNum: int16_t
4039
myLetter: char_t
@@ -61,7 +60,6 @@ struct MyStruct {
6160
};
6261
```
6362
```python
64-
@struct_dataclass
6563
class MyStruct(StructDataclass):
6664
myInts: Annotated[list[uint8_t], TypeMeta(size=4)]
6765
myBiggerInts: Annotated[list[uint16_t], TypeMeta(size=2)]
@@ -87,7 +85,6 @@ struct MyStruct {
8785
```
8886

8987
```python
90-
@struct_dataclass
9188
class MyStruct(StructDataclass):
9289
myInt: uint8_t = 5
9390
myInts: Annnotated[list[uint8_t], TypeMeta(size=2, default=1)]
@@ -117,7 +114,6 @@ struct MyStruct {
117114
};
118115
```
119116
```python
120-
@struct_dataclass
121117
class MyStruct(StructDataclass):
122118
myStr: Annotated[string_t, TypeMeta[str](chunk_size=3)]
123119
myStrList: Annotated[list[string_t], TypeMeta[str](size=2, chunk_size=3)]
@@ -147,8 +143,9 @@ enum ConfigFlags {
147143
```
148144

149145
```python
150-
@bits(uint8_t, {"lights_flag": 0, "platform_flag": 1})
151-
class FlagsType(BitsType): ...
146+
class FlagsType(BitsType):
147+
__bits_type__ = uint8_t
148+
__bits_definition__ = {"lights_flag": 0, "platform_flag": 1}
152149

153150
f = FlagsType()
154151
f.decode([3])
@@ -178,7 +175,6 @@ struct MyStruct {
178175
```
179176

180177
```python
181-
@struct_dataclass
182178
class EnabledSensors(StructDataclass):
183179
# We can define the actual data we are ingesting here
184180
# This mirrors the `uint8_t enabledSensors[5]` data
@@ -264,15 +260,13 @@ struct LEDS {
264260
```
265261

266262
```python
267-
@struct_dataclass
268263
class RGB(StructDataclass):
269264
r: uint8_t
270265
g: uint8_t
271266
b: uint8_t
272267

273-
@struct_dataclass
274268
class LEDS(StructDataclass):
275-
lights: Annotated[list[RGB], TypeMeta(size=3])]
269+
lights: Annotated[list[RGB], TypeMeta(size=3)]
276270

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

289283
# Examples
290284

291-
You can see a more fully fledged example in the `test/examples.py` file.
285+
You can see a more fully fledged example in the `test/examples.py` file.

src/pystructtype/__init__.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
from pystructtype.bitstype import BitsType, bits
2-
from pystructtype.structdataclass import StructDataclass, struct_dataclass
1+
from pystructtype.bitstype import BitsType
2+
from pystructtype.structdataclass import StructDataclass
33
from pystructtype.structtypes import (
44
TypeInfo,
55
TypeMeta,
@@ -22,7 +22,6 @@
2222
"StructDataclass",
2323
"TypeInfo",
2424
"TypeMeta",
25-
"bits",
2625
"char_t",
2726
"double_t",
2827
"float_t",
@@ -31,7 +30,6 @@
3130
"int32_t",
3231
"int64_t",
3332
"string_t",
34-
"struct_dataclass",
3533
"uint8_t",
3634
"uint16_t",
3735
"uint32_t",

src/pystructtype/bitstype.py

Lines changed: 50 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,138 +1,86 @@
11
import itertools
2-
from collections.abc import Callable
2+
from collections.abc import Mapping
33
from dataclasses import field
4-
from typing import Annotated, Any
4+
from types import MappingProxyType
5+
from typing import Annotated, ClassVar
56

6-
from pystructtype.structdataclass import StructDataclass, struct_dataclass
7+
from pystructtype.structdataclass import StructDataclass
78
from pystructtype.structtypes import TypeMeta
89
from pystructtype.utils import int_to_bool_list
910

1011

1112
class BitsType(StructDataclass):
1213
"""
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__.
1515
"""
1616

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
1821
_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
2060

2161
def __post_init__(self) -> None:
2262
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__)
2664

2765
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
3466
super()._decode(data)
35-
36-
# Combine all data in _raw as binary and convert to bools
3767
bin_data = int_to_bool_list(self._raw, self._byte_length)
38-
39-
# Apply bits to the defined structure
4068
for k, v in self._meta.items():
4169
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]
4571
setattr(self, k, steps)
4672
else:
4773
setattr(self, k, bin_data[v])
4874

4975
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
5676
bin_data = list(itertools.repeat(False, self._byte_length * 8))
57-
58-
# Assign the correct values from the defined attributes into bin_data
5977
for k, v in self._meta.items():
6078
if isinstance(v, list):
6179
steps = getattr(self, k)
6280
for idx, bit_idx in enumerate(v):
6381
bin_data[bit_idx] = steps[idx]
6482
else:
6583
bin_data[v] = getattr(self, k)
66-
67-
# Convert bin_data back into their correct integer locations
6884
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)
7186
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

Comments
 (0)