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
36 changes: 34 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,40 @@ s.decode([10, 5, 6])
# MyStruct(myInt=10, myInts=[5, 6])
```

# String / char[] Type

Defining c-string types is a little different. Instead of using
`size` in the `TypeMeta`, we need to instead use `chunk_size`.

This is because the way the struct format is defined for c-strings needs
to know how big the string data is expected to be so that it can put the
whole string in a single variable.

The `chunk_size` is also introduced to allow for `char[][]` for converting
a list of strings.

```c
struct MyStruct {
char myStr[3];
char myStrList[2][3];
};
```
```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)]


s = MyStruct()
s.decode([65, 66, 67, 68, 69, 70, 71, 72, 73])
# MyStruct(myStr=b"ABC", myStrList=[b"DEF", b"GHI"])
```

If you instead try to define this as a list of `char_t` types,
you would only be able to end up with
`MyStruct(myStr=[b"A", b"B", b"C"], myStrList=[b"D", b"E", b"F", b"G", b"H", b"I"])`

# The Bits Abstraction

This library includes a `bits` abstraction to map bits to variables for easier access.
Expand Down Expand Up @@ -205,7 +239,6 @@ s.decode([15, 15, 15, 15, 0])
# [False, False, False, False],
# [True, True, True, True],
# [False, False, False, False],
# [False, False, False, False]
# ]

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

- Bitfield: Similar to the `Bits` abstraction. An easy way to define bitfields
- C-Strings: Make a base class to handle C strings (arrays of chars)
- Potentially more ways to define bits (dicts/lists/etc).
- Potentially allowing list defaults to be entire pre-defined lists.
- ???
Expand Down
4 changes: 1 addition & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ classifiers = [
]
keywords = ["struct", "cstruct", "type"]
requires-python = ">=3.13"
dependencies = [
"loguru>=0.7.3",
]
dependencies = []

[project.urls]
Homepage = "https://github.com/fchorney/pystructtype"
Expand Down
2 changes: 2 additions & 0 deletions src/pystructtype/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
int16_t,
int32_t,
int64_t,
string_t,
uint8_t,
uint16_t,
uint32_t,
Expand All @@ -29,6 +30,7 @@
"int16_t",
"int32_t",
"int64_t",
"string_t",
"struct_dataclass",
"uint8_t",
"uint16_t",
Expand Down
25 changes: 19 additions & 6 deletions src/pystructtype/structdataclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class StructState:
name: str
struct_fmt: str
size: int
chunk_size: int


class StructDataclass:
Expand All @@ -39,18 +40,18 @@ def __post_init__(self) -> None:
type_iterator.key,
type_iterator.type_info.format,
type_iterator.size,
type_iterator.chunk_size,
)
)
self.struct_fmt += (
f"{type_iterator.size if type_iterator.size > 1 else ''}{type_iterator.type_info.format}"
)
_fmt_prefix = type_iterator.chunk_size if type_iterator.chunk_size > 1 else ""
self.struct_fmt += f"{_fmt_prefix}{type_iterator.type_info.format}" * type_iterator.size
elif inspect.isclass(type_iterator.base_type) and issubclass(type_iterator.base_type, StructDataclass):
attr = getattr(self, type_iterator.key)
if type_iterator.is_list:
fmt = attr[0].struct_fmt
else:
fmt = attr.struct_fmt
self._state.append(StructState(type_iterator.key, fmt, type_iterator.size))
self._state.append(StructState(type_iterator.key, fmt, type_iterator.size, type_iterator.chunk_size))
self.struct_fmt += fmt * type_iterator.size
else:
# We have no TypeInfo object, and we're not a StructDataclass
Expand All @@ -76,14 +77,26 @@ def _simplify_format(self) -> None:
while idx < items_len:
if "0" <= (item := items[idx]) <= "9":
idx += 1
expanded_format += items[idx] * int(item)

if items[idx] == "s":
# Shouldn't expand actual char[]/string types as they need to be grouped
# so we know how big the strings should be
expanded_format += item + items[idx]
else:
expanded_format += items[idx] * int(item)
else:
expanded_format += item
idx += 1

# Simplify the format by turning multiple consecutive letters into a number + letter combo
simplified_format = ""
for group in (x[0] for x in re.findall(r"(([a-zA-Z])\2*)", expanded_format)):
for group in (x[0] for x in re.findall(r"(\d*([a-zA-Z])\2*)", expanded_format)):
if re.match(r"\d+", group[0]):
# Just pass through any format that we've explicitly kept
# a number in front of
simplified_format += group
continue

simplified_format += f"{group_len if (group_len := len(group)) > 1 else ''}{group[0]}"

self.struct_fmt = simplified_format
Expand Down
21 changes: 18 additions & 3 deletions src/pystructtype/structtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from pystructtype import structdataclass

T = TypeVar("T", int, float, default=int)
T = TypeVar("T", int, float, str, default=int)
"""Generic Data Type for StructDataclass Contents"""


Expand All @@ -17,6 +17,7 @@ class TypeMeta[T]:
"""

size: int = 1
chunk_size: int = 1
default: T | None = None


Expand All @@ -31,8 +32,6 @@ class TypeInfo:
byte_size: int


# TODO: Support proper "c-string" types

# Fixed Size Types
char_t = Annotated[int, TypeInfo("c", 1)]
"""1 Byte char Type"""
Expand Down Expand Up @@ -60,6 +59,8 @@ class TypeInfo:
"""4 Byte float Type"""
double_t = Annotated[float, TypeInfo("d", 8)]
"""8 Byte double Type"""
string_t = Annotated[str, TypeInfo("s", 1)]
"""1 Byte char[] Type"""


@dataclass
Expand Down Expand Up @@ -89,6 +90,20 @@ def size(self) -> int:
"""
return getattr(self.type_meta, "size", 1)

@property
def chunk_size(self) -> int:
"""
Return the chunk size of the type. Typically, this is used for char[]/string
types as these are defined in chunks rather than in a size of individual
values.

This defaults to 1, else this will return the size defined in the `type_meta` object
if it exists.

:return: integer containing the chunk size of the type
"""
return getattr(self.type_meta, "chunk_size", 1)


def iterate_types(cls: type) -> Generator[TypeIterator]:
"""
Expand Down
28 changes: 27 additions & 1 deletion test/test_ctypes.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,36 @@
from typing import Annotated

from pystructtype import BitsType, StructDataclass, TypeMeta, bits, struct_dataclass, uint8_t
from pystructtype import BitsType, StructDataclass, TypeMeta, bits, string_t, struct_dataclass, uint8_t

from .examples import TEST_CONFIG_DATA, SMXConfigType # type: ignore


def test_strings():
@struct_dataclass
class TestString(StructDataclass):
boo: uint8_t
foo: Annotated[string_t, TypeMeta[str](chunk_size=3)]
far: Annotated[list[uint8_t], TypeMeta(size=2)]
bar: Annotated[string_t, TypeMeta[str](chunk_size=5)]
rob: uint8_t
rar: Annotated[list[string_t], TypeMeta[str](size=2, chunk_size=2)]

data = [0, 65, 66, 67, 1, 2, 65, 66, 67, 68, 69, 2, 65, 66, 67, 68]

s = TestString()
s.decode(data)

assert s.foo == b"ABC"
assert s.bar == b"ABCDE"
assert s.boo == 0
assert s.far == [1, 2]
assert s.rob == 2
assert s.rar == [b"AB", b"CD"]

e = s.encode()
assert s._to_list(e) == data


def test_smx_config():
c = SMXConfigType()

Expand Down
28 changes: 1 addition & 27 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading