From 7c5bfcc9388dfa52f29bfc96f4e34f9b2261f1f0 Mon Sep 17 00:00:00 2001 From: Kyle Husmann Date: Sat, 3 May 2025 14:00:52 -0700 Subject: [PATCH] add bit reordering (via BitfieldConfig) I'm not sure if I like this feature, so I'm keeping it off the main branch for now and will consider adding it in later. --- src/bydantic/__init__.py | 2 ++ src/bydantic/core.py | 27 ++++++++++++++++++++++++- src/bydantic/utils.py | 43 ++++++++++++++++++++++++++++++++++++++++ tests/test_reorder.py | 30 ++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 tests/test_reorder.py diff --git a/src/bydantic/__init__.py b/src/bydantic/__init__.py index 41bea52..97dae9b 100644 --- a/src/bydantic/__init__.py +++ b/src/bydantic/__init__.py @@ -17,6 +17,7 @@ dynamic_field, mapped_field, Bitfield, + BitfieldConfig, ValueMapper, Scale, IntScale, @@ -43,6 +44,7 @@ "lit_str_field", "dynamic_field", "Bitfield", + "BitfieldConfig", "ValueMapper", "Scale", "IntScale", diff --git a/src/bydantic/core.py b/src/bydantic/core.py index f4468c7..5cae315 100644 --- a/src/bydantic/core.py +++ b/src/bydantic/core.py @@ -1,5 +1,7 @@ from __future__ import annotations +from dataclasses import dataclass, field as dataclass_field + from typing_extensions import dataclass_transform, TypeVar as TypeVarDefault, Self import typing as t import inspect @@ -1116,6 +1118,20 @@ class Foo(bd.Bitfield): ContextT = TypeVarDefault("ContextT", default=None) +@dataclass() +class BitfieldConfig: + """ + A configuration object for the bitfield. This can be used to + configure the bitfield's behavior, such as whether to reorder + bits when serializing and deserializing. + """ + + reorder_bits: t.Sequence[int] = dataclass_field(default_factory=list) + """ + A list of bit positions to reorder when serializing and deserializing. + """ + + @dataclass_transform( kw_only_default=True, field_specifiers=( @@ -1146,6 +1162,13 @@ class Bitfield(t.Generic[ContextT]): __BYDANTIC_CONTEXT_STR__: t.ClassVar[str] = "ctx" + bitfield_config: t.ClassVar[BitfieldConfig] = BitfieldConfig() + """ + A configuration object for the bitfield. This can be used to + configure the bitfield's behavior, such as whether to reorder + bits when serializing and deserializing. + """ + ctx: ContextT | None = None """ A context object that can be referenced by dynamic fields while @@ -1389,6 +1412,8 @@ def __bydantic_read_stream__( ): proxy: AttrProxy = AttrProxy({cls.__BYDANTIC_CONTEXT_STR__: ctx}) + stream = stream.reorder(cls.bitfield_config.reorder_bits) + for name, field in cls.__bydantic_fields__.items(): try: value, stream = _read_bftype( @@ -1426,7 +1451,7 @@ def __bydantic_write_stream__( e, self.__class__.__name__, name ) from e - return stream + return stream.unreorder(self.bitfield_config.reorder_bits) def _read_bftype( diff --git a/src/bydantic/utils.py b/src/bydantic/utils.py index 105aa5f..b712077 100644 --- a/src/bydantic/utils.py +++ b/src/bydantic/utils.py @@ -2,6 +2,43 @@ import typing as t +def _make_pairs(order: t.Sequence[int], size: int): + if not all(i < size for i in order) or not all(i >= 0 for i in order): + raise ValueError( + f"some indices in the reordering are out-of-bounds" + ) + + order_set = frozenset(order) + + if len(order_set) != len(order): + raise ValueError( + f"duplicate indices in reordering" + ) + + return zip( + range(size), + (*order, *(i for i in range(size) if i not in order_set)) + ) + + +def reorder_bits(data: t.Sequence[bool], order: t.Sequence[int]) -> t.Tuple[bool, ...]: + if not order: + return tuple(data) + + pairs = _make_pairs(order, len(data)) + + return tuple(data[i] for _, i in pairs) + + +def unreorder_bits(data: t.Sequence[bool], order: t.Sequence[int]) -> t.Tuple[bool, ...]: + if not order: + return tuple(data) + + pairs = sorted(_make_pairs(order, len(data)), key=lambda x: x[1]) + + return tuple(data[i] for i, _ in pairs) + + def bytes_to_bits(data: t.ByteString) -> t.Tuple[bool, ...]: return tuple( bit for byte in data for bit in uint_to_bits(byte, 8) @@ -63,6 +100,9 @@ def as_bits(self) -> t.Tuple[bool, ...]: def as_bytes(self) -> bytes: return bits_to_bytes(self._bits) + def unreorder(self, order: t.Sequence[int]) -> BitstreamWriter: + return BitstreamWriter(unreorder_bits(self._bits, order)) + class BitstreamReader: _bits: t.Tuple[bool, ...] @@ -118,6 +158,9 @@ def as_bits(self) -> t.Tuple[bool, ...]: def as_bytes(self) -> bytes: return self.take_bytes(self.bytes_remaining())[0] + def reorder(self, order: t.Sequence[int]) -> BitstreamReader: + return BitstreamReader(reorder_bits(self._bits, order)) + class AttrProxy(t.Mapping[str, t.Any]): _data: t.Dict[str, t.Any] diff --git a/tests/test_reorder.py b/tests/test_reorder.py new file mode 100644 index 0000000..3987604 --- /dev/null +++ b/tests/test_reorder.py @@ -0,0 +1,30 @@ +import bydantic as bd +import typing as t +from bydantic.utils import ( + reorder_bits, + unreorder_bits +) + + +def test_bit_reorder(): + b = tuple(i == "1" for i in "101100") + order = [1, 3, 5] + + assert reorder_bits(b, order) == tuple(i == "1" for i in "010110") + assert unreorder_bits(reorder_bits(b, order), order) == b + + +def test_basic_reorder(): + class Work(bd.Bitfield): + a: int = bd.uint_field(4) + b: t.List[int] = bd.list_field(bd.uint_field(3), 4) + c: str = bd.str_field(n_bytes=3) + d: bytes = bd.bytes_field(n_bytes=4) + + bitfield_config = bd.BitfieldConfig( + reorder_bits=[*range(56, 56+16)] + ) + + work = Work(a=1, b=[1, 2, 3, 4], c="abc", d=b"abcd") + assert work.to_bytes() == b'abcabcd\x12\x9c' + assert Work.from_bytes_exact(work.to_bytes()) == work