Skip to content

Commit 4336fdb

Browse files
committed
Add string_t type
1 parent 1a90c95 commit 4336fdb

File tree

7 files changed

+102
-42
lines changed

7 files changed

+102
-42
lines changed

README.md

+34-2
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,40 @@ s.decode([10, 5, 6])
9898
# MyStruct(myInt=10, myInts=[5, 6])
9999
```
100100

101+
# String / char[] Type
102+
103+
Defining c-string types is a little different. Instead of using
104+
`size` in the `TypeMeta`, we need to instead use `chunk_size`.
105+
106+
This is because the way the struct format is defined for c-strings needs
107+
to know how big the string data is expected to be so that it can put the
108+
whole string in a single variable.
109+
110+
The `chunk_size` is also introduced to allow for `char[][]` for converting
111+
a list of strings.
112+
113+
```c
114+
struct MyStruct {
115+
char myStr[3];
116+
char myStrList[2][3];
117+
};
118+
```
119+
```python
120+
@struct_dataclass
121+
class MyStruct(StructDataclass):
122+
myStr: Annotated[string_t, TypeMeta[str](chunk_size=3)]
123+
myStrList: Annotated[list[string_t], TypeMeta[str](size=2, chunk_size=3)]
124+
125+
126+
s = MyStruct()
127+
s.decode([65, 66, 67, 68, 69, 70, 71, 72, 73])
128+
# MyStruct(myStr=b"ABC", myStrList=[b"DEF", b"GHI"])
129+
```
130+
131+
If you instead try to define this as a list of `char_t` types,
132+
you would only be able to end up with
133+
`MyStruct(myStr=[b"A", b"B", b"C"], myStrList=[b"D", b"E", b"F", b"G", b"H", b"I"])`
134+
101135
# The Bits Abstraction
102136

103137
This library includes a `bits` abstraction to map bits to variables for easier access.
@@ -205,7 +239,6 @@ s.decode([15, 15, 15, 15, 0])
205239
# [False, False, False, False],
206240
# [True, True, True, True],
207241
# [False, False, False, False],
208-
# [False, False, False, False]
209242
# ]
210243

211244
# With the get/set functioned defined, we can access the data
@@ -249,7 +282,6 @@ l.decode([1, 2, 3, 4, 5, 6, 7, 8, 9])
249282
# Future Updates
250283

251284
- Bitfield: Similar to the `Bits` abstraction. An easy way to define bitfields
252-
- C-Strings: Make a base class to handle C strings (arrays of chars)
253285
- Potentially more ways to define bits (dicts/lists/etc).
254286
- Potentially allowing list defaults to be entire pre-defined lists.
255287
- ???

pyproject.toml

+1-3
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@ classifiers = [
1717
]
1818
keywords = ["struct", "cstruct", "type"]
1919
requires-python = ">=3.13"
20-
dependencies = [
21-
"loguru>=0.7.3",
22-
]
20+
dependencies = []
2321

2422
[project.urls]
2523
Homepage = "https://github.com/fchorney/pystructtype"

src/pystructtype/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
int16_t,
1111
int32_t,
1212
int64_t,
13+
string_t,
1314
uint8_t,
1415
uint16_t,
1516
uint32_t,
@@ -29,6 +30,7 @@
2930
"int16_t",
3031
"int32_t",
3132
"int64_t",
33+
"string_t",
3234
"struct_dataclass",
3335
"uint8_t",
3436
"uint16_t",

src/pystructtype/structdataclass.py

+19-6
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class StructState:
1919
name: str
2020
struct_fmt: str
2121
size: int
22+
chunk_size: int
2223

2324

2425
class StructDataclass:
@@ -39,18 +40,18 @@ def __post_init__(self) -> None:
3940
type_iterator.key,
4041
type_iterator.type_info.format,
4142
type_iterator.size,
43+
type_iterator.chunk_size,
4244
)
4345
)
44-
self.struct_fmt += (
45-
f"{type_iterator.size if type_iterator.size > 1 else ''}{type_iterator.type_info.format}"
46-
)
46+
_fmt_prefix = type_iterator.chunk_size if type_iterator.chunk_size > 1 else ""
47+
self.struct_fmt += f"{_fmt_prefix}{type_iterator.type_info.format}" * type_iterator.size
4748
elif inspect.isclass(type_iterator.base_type) and issubclass(type_iterator.base_type, StructDataclass):
4849
attr = getattr(self, type_iterator.key)
4950
if type_iterator.is_list:
5051
fmt = attr[0].struct_fmt
5152
else:
5253
fmt = attr.struct_fmt
53-
self._state.append(StructState(type_iterator.key, fmt, type_iterator.size))
54+
self._state.append(StructState(type_iterator.key, fmt, type_iterator.size, type_iterator.chunk_size))
5455
self.struct_fmt += fmt * type_iterator.size
5556
else:
5657
# We have no TypeInfo object, and we're not a StructDataclass
@@ -76,14 +77,26 @@ def _simplify_format(self) -> None:
7677
while idx < items_len:
7778
if "0" <= (item := items[idx]) <= "9":
7879
idx += 1
79-
expanded_format += items[idx] * int(item)
80+
81+
if items[idx] == "s":
82+
# Shouldn't expand actual char[]/string types as they need to be grouped
83+
# so we know how big the strings should be
84+
expanded_format += item + items[idx]
85+
else:
86+
expanded_format += items[idx] * int(item)
8087
else:
8188
expanded_format += item
8289
idx += 1
8390

8491
# Simplify the format by turning multiple consecutive letters into a number + letter combo
8592
simplified_format = ""
86-
for group in (x[0] for x in re.findall(r"(([a-zA-Z])\2*)", expanded_format)):
93+
for group in (x[0] for x in re.findall(r"(\d*([a-zA-Z])\2*)", expanded_format)):
94+
if re.match(r"\d+", group[0]):
95+
# Just pass through any format that we've explicitly kept
96+
# a number in front of
97+
simplified_format += group
98+
continue
99+
87100
simplified_format += f"{group_len if (group_len := len(group)) > 1 else ''}{group[0]}"
88101

89102
self.struct_fmt = simplified_format

src/pystructtype/structtypes.py

+18-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from pystructtype import structdataclass
77

8-
T = TypeVar("T", int, float, default=int)
8+
T = TypeVar("T", int, float, str, default=int)
99
"""Generic Data Type for StructDataclass Contents"""
1010

1111

@@ -17,6 +17,7 @@ class TypeMeta[T]:
1717
"""
1818

1919
size: int = 1
20+
chunk_size: int = 1
2021
default: T | None = None
2122

2223

@@ -31,8 +32,6 @@ class TypeInfo:
3132
byte_size: int
3233

3334

34-
# TODO: Support proper "c-string" types
35-
3635
# Fixed Size Types
3736
char_t = Annotated[int, TypeInfo("c", 1)]
3837
"""1 Byte char Type"""
@@ -60,6 +59,8 @@ class TypeInfo:
6059
"""4 Byte float Type"""
6160
double_t = Annotated[float, TypeInfo("d", 8)]
6261
"""8 Byte double Type"""
62+
string_t = Annotated[str, TypeInfo("s", 1)]
63+
"""1 Byte char[] Type"""
6364

6465

6566
@dataclass
@@ -89,6 +90,20 @@ def size(self) -> int:
8990
"""
9091
return getattr(self.type_meta, "size", 1)
9192

93+
@property
94+
def chunk_size(self) -> int:
95+
"""
96+
Return the chunk size of the type. Typically, this is used for char[]/string
97+
types as these are defined in chunks rather than in a size of individual
98+
values.
99+
100+
This defaults to 1, else this will return the size defined in the `type_meta` object
101+
if it exists.
102+
103+
:return: integer containing the chunk size of the type
104+
"""
105+
return getattr(self.type_meta, "chunk_size", 1)
106+
92107

93108
def iterate_types(cls: type) -> Generator[TypeIterator]:
94109
"""

test/test_ctypes.py

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,36 @@
11
from typing import Annotated
22

3-
from pystructtype import BitsType, StructDataclass, TypeMeta, bits, struct_dataclass, uint8_t
3+
from pystructtype import BitsType, StructDataclass, TypeMeta, bits, string_t, struct_dataclass, uint8_t
44

55
from .examples import TEST_CONFIG_DATA, SMXConfigType # type: ignore
66

77

8+
def test_strings():
9+
@struct_dataclass
10+
class TestString(StructDataclass):
11+
boo: uint8_t
12+
foo: Annotated[string_t, TypeMeta[str](chunk_size=3)]
13+
far: Annotated[list[uint8_t], TypeMeta(size=2)]
14+
bar: Annotated[string_t, TypeMeta[str](chunk_size=5)]
15+
rob: uint8_t
16+
rar: Annotated[list[string_t], TypeMeta[str](size=2, chunk_size=2)]
17+
18+
data = [0, 65, 66, 67, 1, 2, 65, 66, 67, 68, 69, 2, 65, 66, 67, 68]
19+
20+
s = TestString()
21+
s.decode(data)
22+
23+
assert s.foo == b"ABC"
24+
assert s.bar == b"ABCDE"
25+
assert s.boo == 0
26+
assert s.far == [1, 2]
27+
assert s.rob == 2
28+
assert s.rar == [b"AB", b"CD"]
29+
30+
e = s.encode()
31+
assert s._to_list(e) == data
32+
33+
834
def test_smx_config():
935
c = SMXConfigType()
1036

uv.lock

+1-27
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)