diff --git a/README.md b/README.md index 6592a4d..24dd40d 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 @@ -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. - ??? diff --git a/pyproject.toml b/pyproject.toml index a086188..46574ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/src/pystructtype/__init__.py b/src/pystructtype/__init__.py index df1f6ed..1a7a340 100644 --- a/src/pystructtype/__init__.py +++ b/src/pystructtype/__init__.py @@ -10,6 +10,7 @@ int16_t, int32_t, int64_t, + string_t, uint8_t, uint16_t, uint32_t, @@ -29,6 +30,7 @@ "int16_t", "int32_t", "int64_t", + "string_t", "struct_dataclass", "uint8_t", "uint16_t", diff --git a/src/pystructtype/structdataclass.py b/src/pystructtype/structdataclass.py index 196ec4f..9afa752 100644 --- a/src/pystructtype/structdataclass.py +++ b/src/pystructtype/structdataclass.py @@ -19,6 +19,7 @@ class StructState: name: str struct_fmt: str size: int + chunk_size: int class StructDataclass: @@ -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 @@ -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 diff --git a/src/pystructtype/structtypes.py b/src/pystructtype/structtypes.py index 5c9b419..c3246e7 100644 --- a/src/pystructtype/structtypes.py +++ b/src/pystructtype/structtypes.py @@ -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""" @@ -17,6 +17,7 @@ class TypeMeta[T]: """ size: int = 1 + chunk_size: int = 1 default: T | None = None @@ -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""" @@ -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 @@ -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]: """ diff --git a/test/test_ctypes.py b/test/test_ctypes.py index 8d78d14..64fe9f1 100644 --- a/test/test_ctypes.py +++ b/test/test_ctypes.py @@ -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() diff --git a/uv.lock b/uv.lock index 7e20de4..e7256af 100644 --- a/uv.lock +++ b/uv.lock @@ -182,19 +182,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 }, ] -[[package]] -name = "loguru" -version = "0.7.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "win32-setctime", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595 }, -] - [[package]] name = "markupsafe" version = "3.0.2" @@ -301,11 +288,8 @@ wheels = [ [[package]] name = "pystructtype" -version = "0.0.2" +version = "0.2.0" source = { editable = "." } -dependencies = [ - { name = "loguru" }, -] [package.dev-dependencies] dev = [ @@ -322,7 +306,6 @@ dev = [ ] [package.metadata] -requires-dist = [{ name = "loguru", specifier = ">=0.7.3" }] [package.metadata.requires-dev] dev = [ @@ -644,12 +627,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/f1/88/dacc875dd54a8acad wheels = [ { url = "https://files.pythonhosted.org/packages/93/fa/849483d56773ae29740ae70043ad88e068f98a6401aa819b5d6bee604683/virtualenv-20.29.2-py3-none-any.whl", hash = "sha256:febddfc3d1ea571bdb1dc0f98d7b45d24def7428214d4fb73cc486c9568cce6a", size = 4301478 }, ] - -[[package]] -name = "win32-setctime" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083 }, -]