Skip to content

Commit 7c5bfcc

Browse files
committed
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.
1 parent 77ff9f7 commit 7c5bfcc

4 files changed

Lines changed: 101 additions & 1 deletion

File tree

src/bydantic/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
dynamic_field,
1818
mapped_field,
1919
Bitfield,
20+
BitfieldConfig,
2021
ValueMapper,
2122
Scale,
2223
IntScale,
@@ -43,6 +44,7 @@
4344
"lit_str_field",
4445
"dynamic_field",
4546
"Bitfield",
47+
"BitfieldConfig",
4648
"ValueMapper",
4749
"Scale",
4850
"IntScale",

src/bydantic/core.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
from dataclasses import dataclass, field as dataclass_field
4+
35
from typing_extensions import dataclass_transform, TypeVar as TypeVarDefault, Self
46
import typing as t
57
import inspect
@@ -1116,6 +1118,20 @@ class Foo(bd.Bitfield):
11161118
ContextT = TypeVarDefault("ContextT", default=None)
11171119

11181120

1121+
@dataclass()
1122+
class BitfieldConfig:
1123+
"""
1124+
A configuration object for the bitfield. This can be used to
1125+
configure the bitfield's behavior, such as whether to reorder
1126+
bits when serializing and deserializing.
1127+
"""
1128+
1129+
reorder_bits: t.Sequence[int] = dataclass_field(default_factory=list)
1130+
"""
1131+
A list of bit positions to reorder when serializing and deserializing.
1132+
"""
1133+
1134+
11191135
@dataclass_transform(
11201136
kw_only_default=True,
11211137
field_specifiers=(
@@ -1146,6 +1162,13 @@ class Bitfield(t.Generic[ContextT]):
11461162

11471163
__BYDANTIC_CONTEXT_STR__: t.ClassVar[str] = "ctx"
11481164

1165+
bitfield_config: t.ClassVar[BitfieldConfig] = BitfieldConfig()
1166+
"""
1167+
A configuration object for the bitfield. This can be used to
1168+
configure the bitfield's behavior, such as whether to reorder
1169+
bits when serializing and deserializing.
1170+
"""
1171+
11491172
ctx: ContextT | None = None
11501173
"""
11511174
A context object that can be referenced by dynamic fields while
@@ -1389,6 +1412,8 @@ def __bydantic_read_stream__(
13891412
):
13901413
proxy: AttrProxy = AttrProxy({cls.__BYDANTIC_CONTEXT_STR__: ctx})
13911414

1415+
stream = stream.reorder(cls.bitfield_config.reorder_bits)
1416+
13921417
for name, field in cls.__bydantic_fields__.items():
13931418
try:
13941419
value, stream = _read_bftype(
@@ -1426,7 +1451,7 @@ def __bydantic_write_stream__(
14261451
e, self.__class__.__name__, name
14271452
) from e
14281453

1429-
return stream
1454+
return stream.unreorder(self.bitfield_config.reorder_bits)
14301455

14311456

14321457
def _read_bftype(

src/bydantic/utils.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,43 @@
22
import typing as t
33

44

5+
def _make_pairs(order: t.Sequence[int], size: int):
6+
if not all(i < size for i in order) or not all(i >= 0 for i in order):
7+
raise ValueError(
8+
f"some indices in the reordering are out-of-bounds"
9+
)
10+
11+
order_set = frozenset(order)
12+
13+
if len(order_set) != len(order):
14+
raise ValueError(
15+
f"duplicate indices in reordering"
16+
)
17+
18+
return zip(
19+
range(size),
20+
(*order, *(i for i in range(size) if i not in order_set))
21+
)
22+
23+
24+
def reorder_bits(data: t.Sequence[bool], order: t.Sequence[int]) -> t.Tuple[bool, ...]:
25+
if not order:
26+
return tuple(data)
27+
28+
pairs = _make_pairs(order, len(data))
29+
30+
return tuple(data[i] for _, i in pairs)
31+
32+
33+
def unreorder_bits(data: t.Sequence[bool], order: t.Sequence[int]) -> t.Tuple[bool, ...]:
34+
if not order:
35+
return tuple(data)
36+
37+
pairs = sorted(_make_pairs(order, len(data)), key=lambda x: x[1])
38+
39+
return tuple(data[i] for i, _ in pairs)
40+
41+
542
def bytes_to_bits(data: t.ByteString) -> t.Tuple[bool, ...]:
643
return tuple(
744
bit for byte in data for bit in uint_to_bits(byte, 8)
@@ -63,6 +100,9 @@ def as_bits(self) -> t.Tuple[bool, ...]:
63100
def as_bytes(self) -> bytes:
64101
return bits_to_bytes(self._bits)
65102

103+
def unreorder(self, order: t.Sequence[int]) -> BitstreamWriter:
104+
return BitstreamWriter(unreorder_bits(self._bits, order))
105+
66106

67107
class BitstreamReader:
68108
_bits: t.Tuple[bool, ...]
@@ -118,6 +158,9 @@ def as_bits(self) -> t.Tuple[bool, ...]:
118158
def as_bytes(self) -> bytes:
119159
return self.take_bytes(self.bytes_remaining())[0]
120160

161+
def reorder(self, order: t.Sequence[int]) -> BitstreamReader:
162+
return BitstreamReader(reorder_bits(self._bits, order))
163+
121164

122165
class AttrProxy(t.Mapping[str, t.Any]):
123166
_data: t.Dict[str, t.Any]

tests/test_reorder.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import bydantic as bd
2+
import typing as t
3+
from bydantic.utils import (
4+
reorder_bits,
5+
unreorder_bits
6+
)
7+
8+
9+
def test_bit_reorder():
10+
b = tuple(i == "1" for i in "101100")
11+
order = [1, 3, 5]
12+
13+
assert reorder_bits(b, order) == tuple(i == "1" for i in "010110")
14+
assert unreorder_bits(reorder_bits(b, order), order) == b
15+
16+
17+
def test_basic_reorder():
18+
class Work(bd.Bitfield):
19+
a: int = bd.uint_field(4)
20+
b: t.List[int] = bd.list_field(bd.uint_field(3), 4)
21+
c: str = bd.str_field(n_bytes=3)
22+
d: bytes = bd.bytes_field(n_bytes=4)
23+
24+
bitfield_config = bd.BitfieldConfig(
25+
reorder_bits=[*range(56, 56+16)]
26+
)
27+
28+
work = Work(a=1, b=[1, 2, 3, 4], c="abc", d=b"abcd")
29+
assert work.to_bytes() == b'abcabcd\x12\x9c'
30+
assert Work.from_bytes_exact(work.to_bytes()) == work

0 commit comments

Comments
 (0)