Skip to content

Commit 3eb20c1

Browse files
committed
datatypes restructured
1 parent 6d081ca commit 3eb20c1

12 files changed

Lines changed: 136 additions & 139 deletions

File tree

umbi/binary/api.py

Lines changed: 10 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,15 @@
22
A convenient interface for all (de)serializers in this package.
33
"""
44

5-
from typing import Union
6-
75
from .booleans import *
8-
from .intervals import *
6+
from .floats import *
97
from .integers import *
8+
from .intervals import *
109
from .rationals import *
1110
from .strings import *
12-
from .floats import *
1311

14-
Numeric = Union[int, float]
1512

16-
def standard_value_type_size(value_type: str) -> int:
17-
"""Return the number of bytes needed to represent a value of the given type."""
18-
if "-interval" in value_type:
19-
base_value_type = value_type.replace("-interval", "")
20-
return 2 * standard_value_type_size(base_value_type)
21-
if value_type == "rational":
22-
# for rationals, the standard size is 8+8 bytes
23-
return standard_value_type_size("int64") + standard_value_type_size("uint64")
24-
if value_type == "double":
25-
return 8 # size of double
26-
_, num_bytes = fixed_size_integer_base_and_size(value_type)
27-
return num_bytes
28-
29-
30-
def value_to_bytes(value: str | Numeric | Fraction | tuple, value_type: str, little_endian: bool = True) -> bytes:
13+
def value_to_bytes(value: str | int | float | Fraction | tuple, value_type: str, little_endian: bool = True) -> bytes:
3114
"""
3215
Convert a value of a given type to a bytestring.
3316
:param value_type: either string or one of {int32|uint32|int64|uint64|double|rational}[-interval]
@@ -49,7 +32,7 @@ def value_to_bytes(value: str | Numeric | Fraction | tuple, value_type: str, lit
4932
return fixed_size_integer_to_bytes(value, value_type, little_endian)
5033

5134

52-
def bytes_to_value(data: bytes, value_type: str, little_endian: bool = True) -> str | Numeric | Fraction | tuple:
35+
def bytes_to_value(data: bytes, value_type: str, little_endian: bool = True) -> str | int | float | Fraction | tuple:
5336
"""
5437
Convert a binary string to a single value of the given type.
5538
:param value_type: string or one of {int32|uint32|int64|uint64|double|rational}[-interval]
@@ -67,26 +50,22 @@ def bytes_to_value(data: bytes, value_type: str, little_endian: bool = True) ->
6750
return bytes_to_fixed_size_integer(data, value_type, little_endian)
6851

6952

70-
def numeric_pack(value: Numeric, value_type: str, num_bits: int) -> BitArray:
53+
def numeric_pack(value: int | float, value_type: str, num_bits: int) -> BitArray:
7154
"""Convert a single primitive value of the given type to a fixed-length bit representation."""
7255
assert value_type in ["int", "uint", "double"], f"unsupported primitive type: {value_type}"
7356
if value_type == "double":
7457
assert isinstance(value, float)
7558
assert num_bits == 64, "double must be represented with 64 bits"
7659
return double_pack(value)
7760
assert isinstance(value, int)
78-
if value_type == "int":
79-
return int_pack(value, num_bits)
80-
else:
81-
return uint_pack(value, num_bits)
61+
signed = value_type == "int"
62+
return integer_pack(value, num_bits, signed)
8263

8364

84-
def numeric_unpack(bits: BitArray, value_type: str) -> Numeric:
65+
def numeric_unpack(bits: BitArray, value_type: str) -> int | float:
8566
"""Convert a BitArray to a single primitive value of the given type."""
8667
assert value_type in ["int", "uint", "double"], f"unsupported primitive type: {value_type}"
8768
if value_type == "double":
8869
return double_unpack(bits)
89-
elif value_type == "int":
90-
return int_unpack(bits)
91-
else:
92-
return uint_unpack(bits)
70+
signed = value_type == "int"
71+
return integer_unpack(bits, signed)

umbi/binary/booleans.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
from typing import Optional
6+
67
from bitstring import BitArray
78

89

umbi/binary/composites.py

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,7 @@ class Field:
3333

3434
name: str
3535
type: str
36-
size: Optional[int] = (
37-
None # number of bits for values of fixed-size types; None for variable-size types (string, rational)
38-
)
36+
size: Optional[int] = None # number of bits for values of fixed-size types; None for variable-size types (string, rational)
3937
lower: Optional[float] = None # lower bound (for numeric types)
4038
upper: Optional[float] = None # upper bound (for numeric types)
4139
offset: Optional[float] = None # lower value offset (for numeric types)
@@ -99,39 +97,40 @@ def fields(self):
9997
def validate(self):
10098
for item in self._fields:
10199
item.validate()
102-
103-
@staticmethod
104-
def new_padding(total_bits: int) -> Padding | None:
105-
"""Create a new padding field to align the total_bits to the next byte boundary."""
106-
padding = (8 - total_bits % 8) % 8
107-
if padding > 0:
108-
return Padding(padding)
109-
return None
110-
111-
def add_paddings(self):
112-
"""Add padding fields to properly align the composite to byte boundaries."""
113-
new_fields = []
114-
total_bits = 0
115-
for field in self._fields:
116-
if isinstance(field, Padding):
117-
total_bits += field.padding
118-
else:
119-
# isinstance(field, Field)
120-
if field.type in {"string", "rational"}:
121-
# add padding to next byte boundary
122-
padding = self.new_padding(total_bits)
123-
if padding is not None:
124-
new_fields.append(padding)
125-
total_bits += padding.padding
126-
else:
127-
assert field.size is not None
128-
total_bits += field.size
129-
new_fields.append(field)
130-
# add final padding to byte boundary
131-
padding = self.new_padding(total_bits)
132-
if padding is not None:
133-
new_fields.append(padding)
134-
self._fields = new_fields
100+
#TODO check alignment
101+
102+
# @staticmethod
103+
# def new_padding(total_bits: int) -> Padding | None:
104+
# """Create a new padding field to align the total_bits to the next byte boundary."""
105+
# padding = (8 - total_bits % 8) % 8
106+
# if padding > 0:
107+
# return Padding(padding)
108+
# return None
109+
110+
# def add_paddings(self):
111+
# """Add padding fields to properly align the composite to byte boundaries."""
112+
# new_fields = []
113+
# total_bits = 0
114+
# for field in self._fields:
115+
# if isinstance(field, Padding):
116+
# total_bits += field.padding
117+
# else:
118+
# # isinstance(field, Field)
119+
# if field.type in {"string", "rational"}:
120+
# # add padding to next byte boundary
121+
# padding = self.new_padding(total_bits)
122+
# if padding is not None:
123+
# new_fields.append(padding)
124+
# total_bits += padding.padding
125+
# else:
126+
# assert field.size is not None
127+
# total_bits += field.size
128+
# new_fields.append(field)
129+
# # add final padding to byte boundary
130+
# padding = self.new_padding(total_bits)
131+
# if padding is not None:
132+
# new_fields.append(padding)
133+
# self._fields = new_fields
135134

136135

137136
class CompositePacker:
@@ -190,7 +189,7 @@ def pack_fields(self, value_type: CompositeType, values: dict[str, object]) -> b
190189
self.add_padding(field.padding)
191190
continue
192191
if field.name not in values:
193-
raise ValueError(f"Missing value for field {field.name}")
192+
raise ValueError(f"missing value for field {field.name}")
194193
self.pack_field(field, values[field.name])
195194
self.assert_buffer_empty()
196195
return self.bytestring

umbi/binary/floats.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
import struct
2+
23
from bitstring import BitArray
34

5+
46
def double_to_bytes(value: float, little_endian: bool = True) -> bytes:
57
ef = "<" if little_endian else ">"
68
return struct.pack(f"{ef}d", value)
79

10+
811
def bytes_to_double(data: bytes, little_endian: bool = True) -> float:
912
ef = "<" if little_endian else ">"
1013
return struct.unpack(f"{ef}d", data)[0]
1114

15+
1216
def double_pack(value: float) -> BitArray:
1317
"""Convert a single double value to a fixed-length bit representation."""
1418
return BitArray(float=value, length=64)
1519

20+
1621
def double_unpack(bits: BitArray) -> float:
1722
"""Convert a fixed-length bit representation to a single double value."""
1823
assert len(bits) == 64, "double must be represented with 64 bits"
19-
return bits.float
24+
return bits.float

umbi/binary/integers.py

Lines changed: 40 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,76 +5,65 @@
55
from bitstring import BitArray
66

77

8-
def check_int_range(value: int, num_bits: int):
9-
max_value = (1 << (num_bits - 1)) - 1
10-
min_value = -(1 << (num_bits - 1))
11-
if not (min_value <= value <= max_value):
12-
raise ValueError(f"integer value {value} is out of range for a {num_bits}-bit int [{min_value}, {max_value}]")
13-
14-
def check_uint_range(value: int, num_bits: int):
15-
max_value = (1 << num_bits) - 1
16-
min_value = 0
8+
def assert_integer_range(value: int, num_bits: int, signed: bool = True):
9+
"""Assert that an integer fits in the given number of bits."""
10+
if signed:
11+
max_value = (1 << (num_bits - 1)) - 1
12+
min_value = -(1 << (num_bits - 1))
13+
else:
14+
max_value = (1 << num_bits) - 1
15+
min_value = 0
1716
if not (min_value <= value <= max_value):
18-
raise ValueError(f"integer value {value} is out of range for a {num_bits}-bit uint [{min_value}, {max_value}]")
19-
17+
raise ValueError(
18+
f"integer value {value} is out of range for a {num_bits}-bit {'signed' if signed else 'unsigned'} integer [{min_value}, {max_value}]"
19+
)
2020

21-
def int_to_bytes(value: int, num_bytes: int, little_endian: bool = True) -> bytes:
22-
check_int_range(value, num_bytes * 8)
23-
return value.to_bytes(num_bytes, byteorder="little" if little_endian else "big", signed=True)
2421

25-
def uint_to_bytes(value: int, num_bytes: int, little_endian: bool = True) -> bytes:
26-
check_uint_range(value, num_bytes * 8)
27-
return value.to_bytes(num_bytes, byteorder="little" if little_endian else "big", signed=False)
22+
def integer_to_bytes(value: int, num_bytes: int, signed: bool = True, little_endian: bool = True) -> bytes:
23+
assert_integer_range(value, num_bytes * 8, signed)
24+
return value.to_bytes(num_bytes, byteorder="little" if little_endian else "big", signed=signed)
2825

29-
def bytes_to_int(data: bytes, little_endian: bool = True) -> int:
30-
return int.from_bytes(data, byteorder="little" if little_endian else "big", signed=True)
31-
32-
def bytes_to_uint(data: bytes, little_endian: bool = True) -> int:
33-
return int.from_bytes(data, byteorder="little" if little_endian else "big", signed=False)
3426

27+
def bytes_to_integer(data: bytes, signed: bool = True, little_endian: bool = True) -> int:
28+
return int.from_bytes(data, byteorder="little" if little_endian else "big", signed=signed)
3529

3630

3731
def fixed_size_integer_base_and_size(value_type: str) -> tuple[str, int]:
3832
fixed_size_integers = {"int16", "uint16", "int32", "int64", "uint32", "uint64"}
3933
if value_type not in fixed_size_integers:
40-
raise ValueError(f"value type must be one of {list(fixed_size_integers)} but is {value_type}")
41-
if value_type.startswith("int"):
42-
base_type = "int"
43-
else:
44-
base_type = "uint"
34+
raise ValueError(f"expected value type one of {list(fixed_size_integers)} but is {value_type}")
35+
base_type = "int" if value_type.startswith("int") else "uint"
4536
num_bytes = int(value_type[-2:]) // 8
4637
return base_type, num_bytes
4738

39+
4840
def fixed_size_integer_to_bytes(value: int, value_type: str, little_endian: bool = True) -> bytes:
49-
"""Convert a single fixed-size integer value of the given type to a bytestring."""
41+
"""Convert a fixed-size integer value of the given type to a bytestring."""
5042
base_type, num_bytes = fixed_size_integer_base_and_size(value_type)
51-
method = int_to_bytes if base_type == "int" else uint_to_bytes
52-
return method(value, num_bytes, little_endian)
43+
return integer_to_bytes(value, num_bytes, signed=(base_type == "int"), little_endian=little_endian)
44+
5345

5446
def bytes_to_fixed_size_integer(data: bytes, value_type: str, little_endian: bool = True) -> int:
55-
"""Convert a binary string to a single fixed-size integer value of the given type."""
47+
"""Convert a binary string to a fixed-size integer value of the given type."""
5648
base_type, num_bytes = fixed_size_integer_base_and_size(value_type)
57-
assert len(data) == num_bytes, f"data length {len(data)} does not match expected size {num_bytes} for type {value_type}"
58-
method = bytes_to_int if base_type == "int" else bytes_to_uint
59-
return method(data, little_endian)
60-
61-
62-
def int_pack(value: int, num_bits: int) -> BitArray:
63-
"""Convert a single int value to a fixed-length bit representation."""
64-
check_int_range(value, num_bits)
65-
return BitArray(int=value, length=num_bits)
49+
assert (
50+
len(data) == num_bytes
51+
), f"data length {len(data)} does not match expected size {num_bytes} for type {value_type}"
52+
return bytes_to_integer(data, signed=(base_type == "int"), little_endian=little_endian)
6653

67-
def uint_pack(value: int, num_bits: int) -> BitArray:
68-
"""Convert a single uint value to a fixed-length bit representation."""
69-
check_uint_range(value, num_bits)
70-
return BitArray(uint=value, length=num_bits)
7154

55+
def integer_pack(value: int, num_bits: int, signed: bool = True) -> BitArray:
56+
"""Convert a single integer value to a fixed-length bit representation."""
57+
assert_integer_range(value, num_bits, signed)
58+
if signed:
59+
return BitArray(int=value, length=num_bits)
60+
else:
61+
return BitArray(uint=value, length=num_bits)
7262

73-
def int_unpack(bits: BitArray) -> int:
74-
"""Convert a BitArray to a single int value."""
75-
return bits.int
76-
77-
def uint_unpack(bits: BitArray) -> int:
78-
"""Convert a BitArray to a single uint value."""
79-
return bits.uint
8063

64+
def integer_unpack(bits: BitArray, signed: bool = True) -> int:
65+
"""Convert a BitArray to a single integer value."""
66+
if signed:
67+
return bits.int
68+
else:
69+
return bits.uint

umbi/binary/intervals.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@
22

33
from fractions import Fraction
44

5-
from .rationals import rational_size, rational_to_bytes, bytes_to_rational
6-
from .floats import double_to_bytes, bytes_to_double
5+
from .floats import bytes_to_double, double_to_bytes
6+
from .rationals import bytes_to_rational, rational_size, rational_to_bytes
77

8+
def assert_continuous_datatype(value_type: str):
9+
"""Assert that the given value type is a continuous datatype (rational or double)."""
10+
continuous_datatypes = {"rational", "double"}
11+
if value_type not in continuous_datatypes:
12+
raise ValueError(f"expected value type one of {list(continuous_datatypes)} but is {value_type}")
813

914
def interval_to_bytes(value: tuple[object, object], value_type: str, little_endian: bool = True) -> bytes:
1015
"""Convert a tuple of two integers into a bytestring representing an interval."""
1116
assert len(value) == 2, "interval value must be a pair"
1217
base_value_type = value_type.replace("-interval", "")
13-
assert base_value_type in {"rational", "double"}, f"unsupported base value type for interval: {base_value_type}"
18+
assert_continuous_datatype(base_value_type)
1419
lower, upper = value
1520
if base_value_type == "rational":
1621
assert isinstance(lower, Fraction) and isinstance(upper, Fraction)
@@ -31,7 +36,7 @@ def bytes_to_interval(data: bytes, value_type: str, little_endian: bool = True)
3136
assert len(data) % 2 == 0, "interval data must have even length"
3237
mid = len(data) // 2
3338
base_value_type = value_type.replace("-interval", "")
34-
assert base_value_type in {"rational", "double"}, f"unsupported base value type for interval: {base_value_type}"
39+
assert_continuous_datatype(base_value_type)
3540
lower, upper = data[:mid], data[mid:]
3641
converter = {
3742
"rational": bytes_to_rational,

0 commit comments

Comments
 (0)