Skip to content

Commit

Permalink
Merge pull request #2 from fchorney/cstr-implementation
Browse files Browse the repository at this point in the history
Add string_t type
  • Loading branch information
fchorney authored Feb 22, 2025
2 parents 1a90c95 + 4336fdb commit c5cca8f
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 42 deletions.
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.

0 comments on commit c5cca8f

Please sign in to comment.