pystructtype 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,493 @@
1
+ import inspect
2
+ import itertools
3
+ import re
4
+ import struct
5
+ from collections.abc import Callable, Generator
6
+ from copy import deepcopy
7
+ from dataclasses import dataclass, field, is_dataclass
8
+ from typing import (
9
+ Annotated,
10
+ Any,
11
+ TypeVar,
12
+ cast,
13
+ get_args,
14
+ get_origin,
15
+ get_type_hints,
16
+ overload,
17
+ )
18
+
19
+
20
+ def list_chunks(_list: list, n: int) -> Generator[list]:
21
+ """
22
+ Yield successive n-sized chunks from a list.
23
+ :param _list: List to chunk out
24
+ :param n: Size of chunks
25
+ :return: Generator of n-sized chunks of _list
26
+ """
27
+ yield from (_list[i : i + n] for i in range(0, len(_list), n))
28
+
29
+
30
+ def type_from_annotation(_type: type) -> type:
31
+ """
32
+ Find the base type from an Annotated type, or return it unchanged if
33
+ not Annotated
34
+ :param _type: Type to check
35
+ :return: Annotated base type or the given type if not Annotated
36
+ """
37
+ # If we have an origin for the given type, and it's Annotated
38
+ if (origin := get_origin(_type)) and origin is Annotated:
39
+ # Keep running `get_args` on the first element of whatever
40
+ # `get_args` returns, until we get nothing back
41
+ arg = _type
42
+ t: Any = _type
43
+ while t := get_args(t):
44
+ arg = t[0]
45
+
46
+ # This will be the base type
47
+ return arg
48
+ # No origin, or the origin is not Annotated, just return the given type
49
+ return _type
50
+
51
+
52
+ T = TypeVar("T", int, float, default=int)
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class TypeMeta[T]:
57
+ size: int = 1
58
+ default: T | None = None
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class TypeInfo:
63
+ format: str
64
+ byte_size: int
65
+
66
+
67
+ # TODO: Support proper "c-string" types
68
+
69
+ # Fixed Size Types
70
+ char_t = Annotated[int, TypeInfo("c", 1)]
71
+ int8_t = Annotated[int, TypeInfo("b", 1)]
72
+ uint8_t = Annotated[int, TypeInfo("B", 1)]
73
+ int16_t = Annotated[int, TypeInfo("h", 2)]
74
+ uint16_t = Annotated[int, TypeInfo("H", 2)]
75
+ int32_t = Annotated[int, TypeInfo("i", 4)]
76
+ uint32_t = Annotated[int, TypeInfo("I", 4)]
77
+ int64_t = Annotated[int, TypeInfo("q", 8)]
78
+ uint64_t = Annotated[int, TypeInfo("Q", 8)]
79
+
80
+ # TODO: Make a special Bool class to auto-convert from int to bool
81
+
82
+ # Named Types
83
+ float_t = Annotated[float, TypeInfo("f", 4)]
84
+ double_t = Annotated[float, TypeInfo("d", 8)]
85
+
86
+
87
+ @dataclass
88
+ class TypeIterator:
89
+ key: str
90
+ base_type: type
91
+ type_info: TypeInfo | None
92
+ type_meta: TypeMeta | None
93
+ is_list: bool
94
+ is_pystructtype: bool
95
+
96
+ @property
97
+ def size(self):
98
+ return getattr(self.type_meta, "size", 1)
99
+
100
+
101
+ def iterate_types(cls) -> Generator[TypeIterator]:
102
+ for key, hint in get_type_hints(cls, include_extras=True).items():
103
+ # Grab the base type from a possibly annotated type hint
104
+ base_type = type_from_annotation(hint)
105
+
106
+ # Determine if the type is a list
107
+ # ex. list[bool] (yes) vs bool (no)
108
+ is_list = issubclass(origin, list) if (origin := get_origin(base_type)) else False
109
+
110
+ # Grab the type hints top args and look for any TypeMeta objects
111
+ type_args = get_args(hint)
112
+ type_meta = next((x for x in type_args if isinstance(x, TypeMeta)), None)
113
+
114
+ # type_args has the possibility of being nested within more tuples
115
+ # drill down the type_args until we hit empty, then we know we're at the bottom
116
+ # which is where type_info will exist
117
+ if type_args and len(type_args) > 1:
118
+ while args := get_args(type_args[0]):
119
+ type_args = args
120
+
121
+ # Find the TypeInfo object on the lowest rung of the type_args
122
+ type_info = next((x for x in type_args if isinstance(x, TypeInfo)), None)
123
+
124
+ # At this point we may have possibly drilled down into `type_args` to find the true base type
125
+ if type_args:
126
+ base_type = type_from_annotation(type_args[0])
127
+
128
+ # Determine if we are a subclass of a pystructtype
129
+ # If we have a type_info object in the Annotation, or we're actually a subtype of StructDataclass
130
+ is_pystructtype = type_info is not None or (
131
+ inspect.isclass(base_type) and issubclass(base_type, StructDataclass)
132
+ )
133
+
134
+ yield TypeIterator(key, base_type, type_info, type_meta, is_list, is_pystructtype)
135
+
136
+
137
+ @dataclass
138
+ class StructState:
139
+ name: str
140
+ struct_fmt: str
141
+ size: int
142
+
143
+
144
+ class StructDataclass:
145
+ def __post_init__(self):
146
+ self._state: list[StructState] = []
147
+ # Grab Struct Format
148
+ self.struct_fmt = ""
149
+ for type_iterator in iterate_types(self.__class__):
150
+ if type_iterator.type_info:
151
+ self._state.append(
152
+ StructState(
153
+ type_iterator.key,
154
+ type_iterator.type_info.format,
155
+ type_iterator.size,
156
+ )
157
+ )
158
+ self.struct_fmt += (
159
+ f"{type_iterator.size if type_iterator.size > 1 else ''}{type_iterator.type_info.format}"
160
+ )
161
+ elif inspect.isclass(type_iterator.base_type) and issubclass(type_iterator.base_type, StructDataclass):
162
+ attr = getattr(self, type_iterator.key)
163
+ if type_iterator.is_list:
164
+ fmt = attr[0].struct_fmt
165
+ else:
166
+ fmt = attr.struct_fmt
167
+ self._state.append(StructState(type_iterator.key, fmt, type_iterator.size))
168
+ self.struct_fmt += fmt * type_iterator.size
169
+ else:
170
+ # We have no TypeInfo object, and we're not a StructDataclass
171
+ # This means we're a regularly defined class variable, and we
172
+ # Don't have to do anything about this.
173
+ pass
174
+ self._simplify_format()
175
+ self._byte_length = struct.calcsize("=" + self.struct_fmt)
176
+ # print(f"{self.__class__.__name__}: {self._byte_length} : {self.struct_fmt}")
177
+
178
+ def _simplify_format(self) -> None:
179
+ # First expand the format
180
+ expanded_format = ""
181
+ items = re.findall(r"([a-zA-Z]|\d+)", self.struct_fmt)
182
+ items_len = len(items)
183
+ idx = 0
184
+ while idx < items_len:
185
+ if "0" <= (item := items[idx]) <= "9":
186
+ idx += 1
187
+ expanded_format += items[idx] * int(item)
188
+ else:
189
+ expanded_format += item
190
+ idx += 1
191
+
192
+ simplified_format = ""
193
+ for group in (x[0] for x in re.findall(r"(([a-zA-Z])\2*)", expanded_format)):
194
+ group_len = len(group)
195
+ simplified_format += f"{group_len if group_len > 1 else ''}{group[0]}"
196
+
197
+ self.struct_fmt = simplified_format
198
+
199
+ def size(self) -> int:
200
+ return sum(state.size for state in self._state)
201
+
202
+ @staticmethod
203
+ def _endian(little_endian: bool) -> str:
204
+ return "<" if little_endian else ">"
205
+
206
+ @staticmethod
207
+ def _to_bytes(data: list[int] | bytes) -> bytes:
208
+ if isinstance(data, bytes):
209
+ return data
210
+ return bytes(data)
211
+
212
+ @staticmethod
213
+ def _to_list(data: list[int] | bytes) -> list[int]:
214
+ if isinstance(data, bytes):
215
+ return list(data)
216
+ return data
217
+
218
+ def _decode(self, data: list[int]) -> None:
219
+ idx = 0
220
+
221
+ for state in self._state:
222
+ attr = getattr(self, state.name)
223
+
224
+ if isinstance(attr, list) and isinstance(attr[0], StructDataclass):
225
+ list_idx = 0
226
+ sub_struct_byte_length = attr[0].size()
227
+ while list_idx < state.size:
228
+ instance: StructDataclass = attr[list_idx]
229
+ instance._decode(data[idx : idx + sub_struct_byte_length])
230
+ list_idx += 1
231
+ idx += sub_struct_byte_length
232
+ elif isinstance(attr, StructDataclass):
233
+ if state.size != 1:
234
+ raise Exception("This should be a size of one, dingus")
235
+
236
+ sub_struct_byte_length = attr.size()
237
+ attr._decode(data[idx : idx + sub_struct_byte_length])
238
+ idx += sub_struct_byte_length
239
+ elif state.size == 1:
240
+ setattr(self, state.name, data[idx])
241
+ idx += 1
242
+ else:
243
+ list_idx = 0
244
+ while list_idx < state.size:
245
+ getattr(self, state.name)[list_idx] = data[idx]
246
+ list_idx += 1
247
+ idx += 1
248
+
249
+ def decode(self, data: list[int] | bytes, little_endian=False) -> None:
250
+ data = self._to_bytes(data)
251
+
252
+ # Decode
253
+ self._decode(list(struct.unpack(self._endian(little_endian) + self.struct_fmt, data)))
254
+
255
+ def _encode(self) -> list[int]:
256
+ result: list[int] = []
257
+
258
+ for state in self._state:
259
+ attr = getattr(self, state.name)
260
+
261
+ if isinstance(attr, list) and isinstance(attr[0], StructDataclass):
262
+ item: StructDataclass
263
+ for item in attr:
264
+ result.extend(item._encode())
265
+ elif isinstance(attr, StructDataclass):
266
+ if state.size != 1:
267
+ raise Exception("This should be a size of one, dingus")
268
+ result.extend(attr._encode())
269
+ elif state.size == 1:
270
+ result.append(getattr(self, state.name))
271
+ else:
272
+ result.extend(getattr(self, state.name))
273
+ return result
274
+
275
+ def encode(self, little_endian=False) -> bytes:
276
+ result = self._encode()
277
+ return struct.pack(self._endian(little_endian) + self.struct_fmt, *result)
278
+
279
+
280
+ @overload
281
+ def struct_dataclass(_cls: type[StructDataclass]) -> type[StructDataclass]: ...
282
+
283
+
284
+ @overload
285
+ def struct_dataclass(_cls: None) -> Callable[[type[StructDataclass]], type[StructDataclass]]: ...
286
+
287
+
288
+ def struct_dataclass(
289
+ _cls: type[StructDataclass] | None = None,
290
+ ) -> Callable[[type[StructDataclass]], type[StructDataclass]] | type[StructDataclass]:
291
+ def inner(_cls: type[StructDataclass]) -> type[StructDataclass]:
292
+ new_cls = _cls
293
+
294
+ # new_cls should not already be a dataclass
295
+ if is_dataclass(new_cls):
296
+ return cast(type[StructDataclass], new_cls)
297
+
298
+ # Make sure any fields without a default have one
299
+ for type_iterator in iterate_types(new_cls):
300
+ if not type_iterator.is_pystructtype:
301
+ continue
302
+
303
+ if not type_iterator.type_meta or type_iterator.type_meta.size == 1:
304
+ if type_iterator.is_list:
305
+ raise Exception("You said this should be size 1, so this shouldn't be a list")
306
+
307
+ # Set a default if it does not yet exist
308
+ if not getattr(new_cls, type_iterator.key, None):
309
+ default: type | int | float = type_iterator.base_type
310
+ if type_iterator.type_meta and type_iterator.type_meta.default:
311
+ default = type_iterator.type_meta.default
312
+ if isinstance(default, list):
313
+ raise Exception("A default for a size 1 should not be a list")
314
+
315
+ # Create a new instance of the class
316
+ if inspect.isclass(default):
317
+ default = field(default_factory=lambda d=default: d()) # type: ignore
318
+ else:
319
+ default = field(default_factory=lambda d=default: deepcopy(d)) # type: ignore
320
+
321
+ setattr(new_cls, type_iterator.key, default)
322
+ else:
323
+ # This assumes we want multiple items of base_type, so make sure the given base_type is
324
+ # properly set to be a list as well
325
+ if not type_iterator.is_list:
326
+ raise Exception("You want a list, so make it a list you dummy")
327
+
328
+ # We have a meta type and the size is > 1 so make the default a field
329
+ default = type_iterator.base_type
330
+ if type_iterator.type_meta and type_iterator.type_meta.default:
331
+ default = type_iterator.type_meta.default
332
+
333
+ default_list = []
334
+ if isinstance(default, list):
335
+ # TODO: Implement having the entire list be a default rather than needing to set each
336
+ # TODO: element as the same base object.
337
+ pass
338
+ else:
339
+ # Create a new instance of the class
340
+ if inspect.isclass(default):
341
+ default_list = field(
342
+ default_factory=lambda d=default, s=type_iterator.type_meta.size: [ # type: ignore
343
+ d() for _ in range(s)
344
+ ]
345
+ )
346
+ else:
347
+ default_list = field(
348
+ default_factory=lambda d=default, s=type_iterator.type_meta.size: [ # type: ignore
349
+ deepcopy(d) for _ in range(s)
350
+ ]
351
+ )
352
+
353
+ setattr(new_cls, type_iterator.key, default_list)
354
+ return cast(type[StructDataclass], dataclass(new_cls))
355
+
356
+ if _cls is None:
357
+ return inner
358
+ return inner(_cls)
359
+
360
+
361
+ def int_to_bool_list(data: int | list[int], byte_length: int) -> list[bool]:
362
+ """
363
+ Converts integer or a list of integers into a list of bools representing the bits
364
+
365
+ ex. ord("A") = [False, True, False, False, False, False, False, True]
366
+
367
+ ex. [ord("A"), ord("B")] = [False, True, False, False, False, False, False, True,
368
+ False, True, False, False, False, False, True, False]
369
+
370
+ :param data: Integer(s) to be converted
371
+ :param byte_length: Number of bytes to extract from integer(s)
372
+ :return: List of bools representing each bit in the data
373
+ """
374
+ # Convert a single int into a list, so we can assume we're always working with a list here
375
+ data = [data] if isinstance(data, int) else data
376
+
377
+ # The amount of bits we end up with will be the number of bytes we expect in the int times 8 (8 bits in a byte)
378
+ # For example uint8_t would have 1 byte, but uint16_t would have 2 bytes
379
+ byte_size = (byte_length * 8) // len(data)
380
+
381
+ bit_strs = []
382
+ for val in data:
383
+ # Convert the int(s) in to a string of bits (add 2 to account for the `0b` prefix)
384
+ tmp_str = format(val, f"#0{byte_size + 2}b")
385
+ # Cut off the `0b` prefix of the bit string, and reverse it
386
+ bit_strs.append(tmp_str.removeprefix("0b")[::-1])
387
+ # Convert the bit_str to a list of ints
388
+ bit_list = map(int, "".join(bit_strs[::-1]))
389
+ # Convert the bit list to bools and return
390
+ return list(map(bool, bit_list))
391
+
392
+
393
+ class BitsType(StructDataclass):
394
+ _raw: Any
395
+ _meta: dict
396
+ _meta_tuple: tuple
397
+
398
+ def __post_init__(self):
399
+ super().__post_init__()
400
+
401
+ self._meta = {k: v for k, v in zip(*self._meta_tuple, strict=False)}
402
+
403
+ def _decode(self, data: list[int]) -> None:
404
+ # First call the super function to put the values in to _raw
405
+ super()._decode(data)
406
+
407
+ # Combine all data in _raw as binary and convert to bools
408
+ bin_data = int_to_bool_list(self._raw, self._byte_length)
409
+
410
+ for k, v in self._meta.items():
411
+ if isinstance(v, list):
412
+ steps = []
413
+ for idx in v:
414
+ steps.append(bin_data[idx])
415
+ setattr(self, k, steps)
416
+ else:
417
+ setattr(self, k, bin_data[v])
418
+
419
+ def _encode(self) -> list[int]:
420
+ bin_data = list(itertools.repeat(False, self._byte_length * 8))
421
+ for k, v in self._meta.items():
422
+ if isinstance(v, list):
423
+ steps = getattr(self, k)
424
+ for idx, bit_idx in enumerate(v):
425
+ bin_data[bit_idx] = steps[idx]
426
+ else:
427
+ bin_data[v] = getattr(self, k)
428
+
429
+ if isinstance(self._raw, list):
430
+ self._raw = [
431
+ sum(v << i for i, v in enumerate(chunk))
432
+ for chunk in list_chunks(bin_data, (self._byte_length // len(self._raw)) * 8)
433
+ ][::-1]
434
+ else:
435
+ self._raw = sum(v << i for i, v in enumerate(bin_data))
436
+
437
+ # Run the super function to return the data in self._raw()
438
+ return super()._encode()
439
+
440
+
441
+ def bits(_type: Any, definition: dict[str, int | list[int]]) -> Callable[[type[BitsType]], type[StructDataclass]]:
442
+ def inner(_cls: type[BitsType]) -> type[StructDataclass]:
443
+ # Create class attributes based on the definition
444
+ # TODO: Maybe a sanity check to make sure the definition is the right format, and no overlapping bits, etc
445
+
446
+ new_cls = _cls
447
+
448
+ new_cls.__annotations__["_raw"] = _type
449
+
450
+ new_cls._meta = field(default_factory=dict)
451
+ new_cls.__annotations__["_meta"] = dict[str, int]
452
+
453
+ # Convert the definition to a named tuple, so it's Immutable
454
+ meta_tuple = (tuple(definition.keys()), tuple(definition.values()))
455
+ new_cls._meta_tuple = field(default_factory=lambda d=meta_tuple: d) # type: ignore
456
+ new_cls.__annotations__["_meta_tuple"] = tuple
457
+
458
+ # TODO: Support int, or list of ints as defaults
459
+ # TODO: Support dict, and dict of lists, or list of dicts, etc for definition
460
+ # TODO: ex. definition = {"a": {"b": 0, "c": [1, 2, 3]}, "d": [4, 5, 6], "e": {"f": 7}}
461
+ # TODO: Can't decide if the line above this is a good idea or not
462
+ for key, value in definition.items():
463
+ if isinstance(value, list):
464
+ setattr(
465
+ new_cls,
466
+ key,
467
+ field(default_factory=lambda v=len(value): [False for _ in range(v)]), # type: ignore # noqa: B008
468
+ )
469
+ new_cls.__annotations__[key] = list[bool]
470
+ else:
471
+ setattr(new_cls, key, False)
472
+ new_cls.__annotations__[key] = bool
473
+
474
+ return struct_dataclass(new_cls)
475
+
476
+ return inner
477
+
478
+
479
+ # XXX: This is how class decorators essentially work
480
+ # @foo
481
+ # class gotem(): ...
482
+ #
483
+ # is equal to: foo(gotem)
484
+ #
485
+ # @foo()
486
+ # class gotem(): ...
487
+ #
488
+ # is equal to: foo()(gotem)
489
+ #
490
+ # @foo(bar=2)
491
+ # class gotem(): ...
492
+ #
493
+ # is equal to: foo(bar=2)(gotem)
pystructtype/py.typed ADDED
File without changes
@@ -0,0 +1,276 @@
1
+ Metadata-Version: 2.4
2
+ Name: pystructtype
3
+ Version: 0.1.0
4
+ Summary: Leverage Python Types to Define C-Struct Interfaces
5
+ Project-URL: Homepage, https://github.com/fchorney/pystructtype
6
+ Project-URL: Documentation, https://pystructtype.readthedocs.io/en/latest/
7
+ Project-URL: Repository, https://github.com/fchorney/pystructtype
8
+ Project-URL: Issues, https://github.com/fchorney/pystructtype/issues
9
+ Author-email: fchorney <github@djsbx.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE.txt
12
+ Keywords: cstruct,struct,type
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Natural Language :: English
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.13
21
+ Requires-Dist: loguru>=0.7.3
22
+ Description-Content-Type: text/markdown
23
+
24
+ # PyStructTypes
25
+
26
+ Leverage Python Types to Define C-Struct Interfaces
27
+
28
+
29
+ # Reasoning
30
+
31
+ I made this project for 2 reasons:
32
+ 1. I wanted to see if I could leverage the typing system to effectively automatically
33
+ decode and encode c-type structs in python.
34
+ 2. Build a tool to do this for a separate project I am working on.
35
+
36
+ I am aware of other very similar c-type struct to python class libraries available,
37
+ but I wanted to try something new so here we are.
38
+
39
+ This may or may not end up being super useful, as there are quite a few bits of
40
+ hacky metaprogramming to get the type system to play nicely for what I want, but
41
+ perhaps over time it can be cleaned up and made more useful.
42
+
43
+ # StructDataclass
44
+
45
+ The `StructDataclass` class is based off of the `Dataclass` class, and thus
46
+ is used in a similar fashion.
47
+
48
+ # Basic Structs
49
+
50
+ Basic structs can mostly be copied over 1:1
51
+
52
+ ```c
53
+ struct MyStruct {
54
+ int16_t myNum;
55
+ char myLetter;
56
+ };
57
+ ```
58
+
59
+ ```python
60
+ @struct_dataclass
61
+ class MyStruct(StructDataclass):
62
+ myNum: int16_t
63
+ myLetter: char_t
64
+
65
+ s = MyStruct()
66
+ s.decode([4, 2, 65])
67
+ # MyStruct(myNum=1026, myLetter=b"A")
68
+ s.decode([4, 2, 65], little_endian=True)
69
+ # MyStruct(myNum=516, myLetter=b"A")
70
+ ```
71
+
72
+ For arrays of basic elements, you need to Annotate them with
73
+ the `TypeMeta` object, and set their type to `list[_type_]`.
74
+
75
+ ```c
76
+ struct MyStruct {
77
+ uint8_t myInts[4];
78
+ uint16_t myBiggerInts[2];
79
+ };
80
+ ```
81
+ ```python
82
+ @struct_dataclass
83
+ class MyStruct(StructDataclass):
84
+ myInts: Annotated[list[uint8_t], TypeMeta(size=4)]
85
+ myBiggerInts: Annotated[list[uint16_t], TypeMeta(size=2)]
86
+
87
+ s = MyStruct()
88
+ s.decode([0, 64, 128, 255, 16, 0, 255, 255])
89
+ # MyStruct(myInts=[0, 64, 128, 255], myBiggerInts=[4096, 65535])
90
+ ```
91
+
92
+ You can also set defaults for both basic types and lists.
93
+
94
+ All values will default to 0 or the initialized value for the chosen class if no
95
+ specific value is set.
96
+
97
+ List defaults will set all items in the list to the same value. Currently
98
+ setting a complete default list for all values is not implemented.
99
+
100
+ ```c
101
+ struct MyStruct P
102
+ uint8_t myInt = 5;
103
+ uint8_t myInts[2];
104
+ ```
105
+
106
+ ```python
107
+ @struct_dataclass
108
+ class MyStruct(StructDataclass):
109
+ myInt: uint8_t = 5
110
+ myInts: Annnotated[list[uint8_t], TypeMeta(size=2, default=1)]
111
+
112
+ s = MyStruct()
113
+ # MyStruct(myInt=5, myInts=[1, 1])
114
+ s.decode([10, 5, 6])
115
+ # MyStruct(myInt=10, myInts=[5, 6])
116
+ ```
117
+
118
+ # The Bits Abstraction
119
+
120
+ This library includes a `bits` abstraction to map bits to variables for easier access.
121
+
122
+ One example of this is converting a C enum like so:
123
+
124
+ ```c
125
+ enum ConfigFlags {
126
+ lights_flag = 1 << 0,
127
+ platform_flag = 1 << 1,
128
+ };
129
+ #pragma pack(push, 1)
130
+ ```
131
+
132
+ ```python
133
+ @bits(uint8_t, {"lights_flag": 0, "platform_flag": 1})
134
+ class FlagsType(BitsType): ...
135
+
136
+ f = FlagsType()
137
+ f.decode([3])
138
+ # FlagsType(lights_flag=True, platform_flag=True)
139
+ f.decode([2])
140
+ # FlagsType(lights_flag=False, platform_flag=True)
141
+ f.decode([1])
142
+ # FlagsType(lights_flag=True, platform_flag=False)
143
+ ```
144
+
145
+ # Custom StructDataclass Processing and Extensions
146
+
147
+ There may be times when you want to make the python class do
148
+ cool fun python class type of stuff with the data structure.
149
+ We can extend the class functions `_decode` and `_encode` to
150
+ handle this processing.
151
+
152
+ In this example, lets say you want to be able to read/write the
153
+ class object as a list, using `__getitem__` and `__setitem__` as well
154
+ as keeping the data in a different data structure than what the
155
+ c struct defines.
156
+
157
+ ```c
158
+ struct MyStruct {
159
+ uint8_t enabledSensors[5];
160
+ };
161
+ ```
162
+
163
+ ```python
164
+ @struct_dataclass
165
+ class EnabledSensors(StructDataclass):
166
+ # We can define the actual data we are ingesting here
167
+ # This mirrors the `uint8_t enabledSensors[5]` data
168
+ _raw: Annotated[list[uint8_t], TypeMeta(size=5)]
169
+
170
+ # We use this to store the data in the way we actually want
171
+ _data: list[list[bool]] = field(default_factory=list)
172
+
173
+ def _decode(self, data: list[int]) -> None:
174
+ # First call the super function. This will store the raw values into `_raw`
175
+ super()._decode(data)
176
+
177
+ # Erase everything in self._data to remove any old data
178
+ self._data = []
179
+
180
+ # 2 Panels are packed into a single uint8_t, the left most 4 bits for the first
181
+ # and the right most 4 bits for the second
182
+ for bitlist in (list(map(bool, map(int, format(_byte, "#010b")[2:]))) for _byte in self._raw):
183
+ self._data.append(bitlist[0:4])
184
+ self._data.append(bitlist[4:])
185
+
186
+ # Remove the last item in self._data as there are only 9 panels
187
+ del self._data[-1]
188
+
189
+ def _encode(self) -> list[int]:
190
+ # Modify self._raw with updated values from self._data
191
+ for idx, items in enumerate(list_chunks(self._data, 2)):
192
+ # Last chunk
193
+ if len(items) == 1:
194
+ items.append([False, False, False, False])
195
+ self._raw[idx] = sum(v << i for i, v in enumerate(list(itertools.chain.from_iterable(items))[::-1]))
196
+
197
+ # Run the super function to return the encoded data from self._raw()
198
+ return super()._encode()
199
+
200
+ def __getitem__(self, index: int) -> list[bool]:
201
+ # This lets us access the data with square brackets
202
+ # ex. `config.enabled_sensors[Panel.UP][Sensor.RIGHT]`
203
+ return self._data[index]
204
+
205
+ def __setitem__(self, index: int, value: list[bool]) -> None:
206
+ # Only use this to set a complete set for a panel
207
+ # ex. `config.enabled_sensors[Panel.UP] = [True, True, False, True]`
208
+ if len(value) != 4 or not all(isinstance(x, bool) for x in value):
209
+ raise Exception("must set all 4 items at once")
210
+
211
+ s = EnabledSensors()
212
+ s.decode([15, 15, 15, 15, 0])
213
+
214
+ # The `self._data` here would look like:
215
+ # [
216
+ # [False, False, False, False],
217
+ # [True, True, True, True],
218
+ # [False, False, False, False],
219
+ # [True, True, True, True],
220
+ # [False, False, False, False],
221
+ # [True, True, True, True],
222
+ # [False, False, False, False],
223
+ # [True, True, True, True],
224
+ # [False, False, False, False],
225
+ # [False, False, False, False]
226
+ # ]
227
+
228
+ # With the get/set functioned defined, we can access the data
229
+ # with square accessors.
230
+ # s[1][2] == True
231
+ ```
232
+
233
+ # StructDataclass is Composable
234
+
235
+ You can use StructDataclasses in other StructDataclasses to create more complex
236
+ structs.
237
+
238
+ ```c
239
+ struct RGB {
240
+ uint8_t r;
241
+ uint8_t g;
242
+ uint8_t b;
243
+ };
244
+
245
+ struct LEDS {
246
+ RGB lights[3];
247
+ };
248
+ ```
249
+
250
+ ```python
251
+ @struct_dataclass
252
+ class RGB(StructDataclass):
253
+ r: uint8_t
254
+ g: uint8_t
255
+ b: uint8_t
256
+
257
+ @struct_dataclass
258
+ class LEDS(StructDataclass):
259
+ lights: Annotated[list[RGB], TypeMeta(size=3])]
260
+
261
+ l = LEDS()
262
+ l.decode([1, 2, 3, 4, 5, 6, 7, 8, 9])
263
+ # LEDS(lights=[RGB(r=1, g=2, b=3), RGB(r=4, g=5, b=6), RGB(r=7, g=8, b=9)])
264
+ ```
265
+
266
+ # Future Updates
267
+
268
+ - Bitfield: Similar to the `Bits` abstraction. An easy way to define bitfields
269
+ - C-Strings: Make a base class to handle C strings (arrays of chars)
270
+ - Potentially more ways to define bits (dicts/lists/etc).
271
+ - Potentially allowing list defaults to be entire pre-defined lists.
272
+ - ???
273
+
274
+ # Examples
275
+
276
+ You can see a more fully fledged example in the `test/examples.py` file.
@@ -0,0 +1,6 @@
1
+ pystructtype/__init__.py,sha256=OLjKzt0Rer02l0WpG0TtICYhqyyxeGiHmLQeU_Aq284,18097
2
+ pystructtype/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ pystructtype-0.1.0.dist-info/METADATA,sha256=uQWihEXins96KB5SX70jU08ZpshjITFhcq5oA9tR1oE,7984
4
+ pystructtype-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
+ pystructtype-0.1.0.dist-info/licenses/LICENSE.txt,sha256=2Vm7iCFyISqDsn9ED-_pZOrTEUneAIuMCk8TKdIL9W4,1073
6
+ pystructtype-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Fernando Chorney
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.