bitarray 3.12.2__cp314-cp314-pyemscripten_2026_0_wasm32.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bitarray/__init__.py +66 -0
- bitarray/__init__.pyi +176 -0
- bitarray/_bitarray.cpython-314-wasm32-emscripten.so +0 -0
- bitarray/_util.cpython-314-wasm32-emscripten.so +0 -0
- bitarray/bitarray.h +357 -0
- bitarray/bitfields.py +447 -0
- bitarray/py.typed +0 -0
- bitarray/pythoncapi_compat.h +3138 -0
- bitarray/test_281.pickle +0 -0
- bitarray/test_bitarray.py +5788 -0
- bitarray/test_bitfields.py +721 -0
- bitarray/test_free_threading.py +840 -0
- bitarray/test_util.py +3166 -0
- bitarray/util.py +694 -0
- bitarray/util.pyi +86 -0
- bitarray-3.12.2.dist-info/METADATA +1134 -0
- bitarray-3.12.2.dist-info/RECORD +20 -0
- bitarray-3.12.2.dist-info/WHEEL +5 -0
- bitarray-3.12.2.dist-info/licenses/LICENSE +46 -0
- bitarray-3.12.2.dist-info/top_level.txt +1 -0
bitarray/bitfields.py
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
# Copyright (c) 2026, Ilan Schnell; All Rights Reserved
|
|
2
|
+
# bitarray is published under the PSF license.
|
|
3
|
+
#
|
|
4
|
+
# Author: Ilan Schnell
|
|
5
|
+
"""
|
|
6
|
+
Pack and unpack bit-level structures using `bitarray`.
|
|
7
|
+
|
|
8
|
+
https://github.com/ilanschnell/bitarray/blob/master/doc/bitfields.rst
|
|
9
|
+
"""
|
|
10
|
+
import re
|
|
11
|
+
import struct
|
|
12
|
+
import operator
|
|
13
|
+
import functools
|
|
14
|
+
import collections
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Any, Tuple
|
|
17
|
+
|
|
18
|
+
from bitarray import bitarray
|
|
19
|
+
from bitarray.util import int2ba, ba2int, hex2ba, ba2hex
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
__all__ = ["Struct", "compile", "pack", "pack_into", "unpack", "unpack_from",
|
|
23
|
+
"calcsize", "PaddingError"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
DEFAULT_ENDIAN = "little"
|
|
27
|
+
|
|
28
|
+
_ENDIAN_FROM_PREFIX = {"<": "little", ">": "big"}
|
|
29
|
+
_PREFIX_FROM_ENDIAN = {v: k for k, v in _ENDIAN_FROM_PREFIX.items()}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class _Field:
|
|
34
|
+
|
|
35
|
+
width: int
|
|
36
|
+
endian: str
|
|
37
|
+
name: str
|
|
38
|
+
has_value = True
|
|
39
|
+
code = ""
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def prefix(self):
|
|
43
|
+
return _PREFIX_FROM_ENDIAN[self.endian]
|
|
44
|
+
|
|
45
|
+
def format(self):
|
|
46
|
+
res = "%s%s%d" % (self.prefix, self.code, self.width)
|
|
47
|
+
if self.name:
|
|
48
|
+
res += "{%s}" % self.name
|
|
49
|
+
return res
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class _IntField(_Field):
|
|
54
|
+
|
|
55
|
+
signed: bool
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def code(self):
|
|
59
|
+
return "s" if self.signed else "u"
|
|
60
|
+
|
|
61
|
+
def pack(self, value):
|
|
62
|
+
return int2ba(value, length=self.width, endian=self.endian,
|
|
63
|
+
signed=self.signed)
|
|
64
|
+
|
|
65
|
+
def unpack(self, a):
|
|
66
|
+
return ba2int(a, signed=self.signed)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class _BoolField(_Field):
|
|
71
|
+
|
|
72
|
+
code = "?"
|
|
73
|
+
|
|
74
|
+
def __post_init__(self):
|
|
75
|
+
if self.width != 1:
|
|
76
|
+
raise ValueError("bool field width must be 1, "
|
|
77
|
+
"got %d" % self.width)
|
|
78
|
+
|
|
79
|
+
def pack(self, value):
|
|
80
|
+
return bitarray("1" if value else "0")
|
|
81
|
+
|
|
82
|
+
def unpack(self, a):
|
|
83
|
+
return bool(a[0])
|
|
84
|
+
|
|
85
|
+
def format(self):
|
|
86
|
+
res = "%s?" % self.prefix
|
|
87
|
+
if self.name:
|
|
88
|
+
res += "{%s}" % self.name
|
|
89
|
+
return res
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(frozen=True)
|
|
93
|
+
class _FloatField(_Field):
|
|
94
|
+
|
|
95
|
+
code = "f"
|
|
96
|
+
formats = {16: "e", 32: "f", 64: "d"}
|
|
97
|
+
|
|
98
|
+
def __post_init__(self):
|
|
99
|
+
if self.width not in self.formats:
|
|
100
|
+
raise ValueError("float field width must be 16, 32 or 64, "
|
|
101
|
+
"got %d" % self.width)
|
|
102
|
+
|
|
103
|
+
def struct_format(self):
|
|
104
|
+
return self.prefix + self.formats[self.width]
|
|
105
|
+
|
|
106
|
+
def pack(self, x):
|
|
107
|
+
return bitarray(struct.pack(self.struct_format(), x),
|
|
108
|
+
endian=self.endian)
|
|
109
|
+
|
|
110
|
+
def unpack(self, a):
|
|
111
|
+
return struct.unpack(self.struct_format(), bytes(a))[0]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(frozen=True)
|
|
115
|
+
class _HexField(_Field):
|
|
116
|
+
|
|
117
|
+
code = "h"
|
|
118
|
+
|
|
119
|
+
def __post_init__(self):
|
|
120
|
+
if self.width % 4:
|
|
121
|
+
raise ValueError("hexadecimal field width must be a "
|
|
122
|
+
"multiple of 4, got %d" % self.width)
|
|
123
|
+
|
|
124
|
+
def pack(self, hexstr):
|
|
125
|
+
if not isinstance(hexstr, str):
|
|
126
|
+
raise TypeError("str expected, got %r" % type(hexstr).__name__)
|
|
127
|
+
a = hex2ba(hexstr, self.endian)
|
|
128
|
+
if len(a) != self.width:
|
|
129
|
+
raise ValueError("expected hex string of length %d, got %d" %
|
|
130
|
+
(self.width // 4, len(a) // 4))
|
|
131
|
+
return a
|
|
132
|
+
|
|
133
|
+
def unpack(self, a):
|
|
134
|
+
return ba2hex(a)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass(frozen=True)
|
|
138
|
+
class _BitarrayField(_Field):
|
|
139
|
+
|
|
140
|
+
code = "b"
|
|
141
|
+
|
|
142
|
+
def pack(self, a):
|
|
143
|
+
if not isinstance(a, bitarray):
|
|
144
|
+
raise TypeError("bitarray expected, got %r" % type(a).__name__)
|
|
145
|
+
if len(a) != self.width:
|
|
146
|
+
raise ValueError("expected bitarray of length %d, got %d" %
|
|
147
|
+
(self.width, len(a)))
|
|
148
|
+
return a
|
|
149
|
+
|
|
150
|
+
def unpack(self, a):
|
|
151
|
+
return a
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@dataclass(frozen=True)
|
|
155
|
+
class _BytesField(_Field):
|
|
156
|
+
|
|
157
|
+
code = "B"
|
|
158
|
+
|
|
159
|
+
def __post_init__(self):
|
|
160
|
+
if self.width % 8:
|
|
161
|
+
raise ValueError("bytes field width must be a "
|
|
162
|
+
"multiple of 8, got %d" % self.width)
|
|
163
|
+
|
|
164
|
+
def pack(self, b):
|
|
165
|
+
if not isinstance(b, (bytes, bytearray)):
|
|
166
|
+
raise TypeError("bytes expected, got %r" % type(b).__name__)
|
|
167
|
+
if len(b) != self.width // 8:
|
|
168
|
+
raise ValueError("expected bytes of length %d, got %d" %
|
|
169
|
+
(self.width // 8, len(b)))
|
|
170
|
+
return bitarray(b, endian=self.endian)
|
|
171
|
+
|
|
172
|
+
def unpack(self, a):
|
|
173
|
+
return bytes(a)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class PaddingError(ValueError):
|
|
177
|
+
"Raised when validated padding bits do not match."
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@dataclass(frozen=True)
|
|
181
|
+
class _PaddingField(_Field):
|
|
182
|
+
# For padding fields, the inherited name attribute stores the canonical
|
|
183
|
+
# explicit bit pattern, or "" when the field uses repeated padding.
|
|
184
|
+
|
|
185
|
+
value: bool
|
|
186
|
+
validate: bool
|
|
187
|
+
has_value = False
|
|
188
|
+
|
|
189
|
+
@classmethod
|
|
190
|
+
def from_code(cls, code, width, endian, name):
|
|
191
|
+
if name:
|
|
192
|
+
a = bitarray(name) # validates and ignores underscores
|
|
193
|
+
if width != len(a):
|
|
194
|
+
raise ValueError("pad-bits width %d does not match pattern "
|
|
195
|
+
"length %d" % (width, len(a)))
|
|
196
|
+
if (not a[0]) in a: # we have both 0 and 1 in a
|
|
197
|
+
value, name = False, a.to01()
|
|
198
|
+
else:
|
|
199
|
+
value, name = bool(a[0]), ""
|
|
200
|
+
else:
|
|
201
|
+
value = code.isupper()
|
|
202
|
+
return cls(width, endian, name, value=value,
|
|
203
|
+
validate=(code.lower() == "p"))
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def code(self):
|
|
207
|
+
return ("pP" if self.validate else "xX")[self.value]
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def bits(self):
|
|
211
|
+
return bitarray(self.name or self.width * ("01"[self.value]))
|
|
212
|
+
|
|
213
|
+
def pack(self, unused_value):
|
|
214
|
+
return self.bits
|
|
215
|
+
|
|
216
|
+
def unpack(self, a):
|
|
217
|
+
if self.validate and a != self.bits:
|
|
218
|
+
raise PaddingError("expected pad-bits %s, got %s" %
|
|
219
|
+
(self.bits.to01(), a.to01()))
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@dataclass(frozen=True)
|
|
223
|
+
class Struct:
|
|
224
|
+
"""Struct(format) -> compiled struct object
|
|
225
|
+
|
|
226
|
+
Central class for packing and unpacking bit-level structures.
|
|
227
|
+
"""
|
|
228
|
+
_fields: tuple
|
|
229
|
+
width: int
|
|
230
|
+
values: int
|
|
231
|
+
_names: tuple
|
|
232
|
+
|
|
233
|
+
def __init__(self, format: str) -> None:
|
|
234
|
+
fields = self._fields_from_format(format)
|
|
235
|
+
object.__setattr__(self, "_fields", tuple(fields))
|
|
236
|
+
object.__setattr__(self, "width", sum(f.width for f in fields))
|
|
237
|
+
object.__setattr__(self, "values", sum(f.has_value for f in fields))
|
|
238
|
+
object.__setattr__(self, "_names", self._get_names())
|
|
239
|
+
|
|
240
|
+
def _get_names(self):
|
|
241
|
+
names = tuple(f.name for f in self._fields if f.has_value)
|
|
242
|
+
if not any(names):
|
|
243
|
+
return ()
|
|
244
|
+
if not all(names):
|
|
245
|
+
raise ValueError("Some but not all fields have a name")
|
|
246
|
+
self._result_type(names) # validate and warm cache
|
|
247
|
+
return names
|
|
248
|
+
|
|
249
|
+
@staticmethod
|
|
250
|
+
@functools.lru_cache()
|
|
251
|
+
def _result_type(names):
|
|
252
|
+
return collections.namedtuple("Unpacked", names)
|
|
253
|
+
|
|
254
|
+
_pat = re.compile(r"""
|
|
255
|
+
([<>])? # optional prefix
|
|
256
|
+
([a-zA-Z?]) # code character
|
|
257
|
+
(\d+)? # optional bit width
|
|
258
|
+
(?:\{(\w+)\})? # optional name (or padding bit pattern)
|
|
259
|
+
\s* # optional whitespace
|
|
260
|
+
""", re.VERBOSE | re.ASCII)
|
|
261
|
+
|
|
262
|
+
def _fields_from_format(self, format):
|
|
263
|
+
endian = DEFAULT_ENDIAN
|
|
264
|
+
fields = []
|
|
265
|
+
format = re.sub(r"#[^\r\n]*", "", format) # strip comments
|
|
266
|
+
format = format.lstrip()
|
|
267
|
+
while format:
|
|
268
|
+
m = self._pat.match(format)
|
|
269
|
+
if m is None:
|
|
270
|
+
raise ValueError("invalid format: %r" % format)
|
|
271
|
+
pre = m.group(1)
|
|
272
|
+
if pre:
|
|
273
|
+
endian = _ENDIAN_FROM_PREFIX[pre]
|
|
274
|
+
code = m.group(2)
|
|
275
|
+
name = m.group(4) or ""
|
|
276
|
+
width = int(m.group(3) or self._default_width(code, name))
|
|
277
|
+
if width == 0:
|
|
278
|
+
raise ValueError("field width cannot be zero: %r" % format)
|
|
279
|
+
fields.append(self._field_from_code(code, width, endian, name))
|
|
280
|
+
format = format[m.end():]
|
|
281
|
+
return fields
|
|
282
|
+
|
|
283
|
+
@staticmethod
|
|
284
|
+
def _default_width(code, name):
|
|
285
|
+
if code in "pPxX" and name:
|
|
286
|
+
# Underscores do not contribute to the width.
|
|
287
|
+
return len(name) - name.count("_")
|
|
288
|
+
return {"h": 4, "B": 8}.get(code, 1)
|
|
289
|
+
|
|
290
|
+
@staticmethod
|
|
291
|
+
def _field_from_code(code, width, endian, name):
|
|
292
|
+
if code in "us":
|
|
293
|
+
return _IntField(width, endian, name, signed=(code == "s"))
|
|
294
|
+
if code == "?":
|
|
295
|
+
return _BoolField(width, endian, name)
|
|
296
|
+
if code == "f":
|
|
297
|
+
return _FloatField(width, endian, name)
|
|
298
|
+
if code == "h":
|
|
299
|
+
return _HexField(width, endian, name)
|
|
300
|
+
if code == "b":
|
|
301
|
+
return _BitarrayField(width, endian, name)
|
|
302
|
+
if code == "B":
|
|
303
|
+
return _BytesField(width, endian, name)
|
|
304
|
+
if code in "pPxX":
|
|
305
|
+
return _PaddingField.from_code(code, width, endian, name)
|
|
306
|
+
raise ValueError("invalid code: %r" % code)
|
|
307
|
+
|
|
308
|
+
def format(self) -> str:
|
|
309
|
+
"""format() -> str
|
|
310
|
+
|
|
311
|
+
Return the canonical format string reconstructed from this compiled format.
|
|
312
|
+
"""
|
|
313
|
+
return " ".join(field.format() for field in self._fields)
|
|
314
|
+
|
|
315
|
+
def __repr__(self):
|
|
316
|
+
return "Struct(%r)" % self.format()
|
|
317
|
+
|
|
318
|
+
def __reduce__(self):
|
|
319
|
+
return Struct, (self.format(),)
|
|
320
|
+
|
|
321
|
+
def _normalize_offset(self, length, offset, op):
|
|
322
|
+
i = operator.index(offset)
|
|
323
|
+
if i < 0:
|
|
324
|
+
i += length
|
|
325
|
+
if not 0 <= i <= length:
|
|
326
|
+
raise ValueError("offset %d out of range for bitarray size %d" %
|
|
327
|
+
(offset, length))
|
|
328
|
+
if i + self.width > length:
|
|
329
|
+
raise ValueError(
|
|
330
|
+
"bitarray of at least %d bits required for %sing %d bits "
|
|
331
|
+
"at offset %d (actual bitarray size is %d)" %
|
|
332
|
+
(i + self.width, op, self.width, i, length))
|
|
333
|
+
return i
|
|
334
|
+
|
|
335
|
+
def pack(self, *values: Any) -> bitarray:
|
|
336
|
+
"""pack(v1, v2, ...) -> bitarray
|
|
337
|
+
|
|
338
|
+
Return a bitarray containing the values v1, v2, ... packed according to this
|
|
339
|
+
compiled format.
|
|
340
|
+
"""
|
|
341
|
+
if len(values) != self.values:
|
|
342
|
+
raise ValueError("expected %d values to pack, got %d" %
|
|
343
|
+
(self.values, len(values)))
|
|
344
|
+
value_iter = iter(values)
|
|
345
|
+
fields = self._fields
|
|
346
|
+
a = bitarray(0, fields[0].endian if fields else DEFAULT_ENDIAN)
|
|
347
|
+
for field in fields:
|
|
348
|
+
value = next(value_iter) if field.has_value else None
|
|
349
|
+
a.extend(field.pack(value))
|
|
350
|
+
return a
|
|
351
|
+
|
|
352
|
+
def pack_into(self, a: bitarray, offset: int, *values: Any) -> None:
|
|
353
|
+
"""pack_into(bitarray, offset, v1, v2, ...) -> None
|
|
354
|
+
|
|
355
|
+
Pack the values v1, v2, ... into the writable bitarray starting at bit
|
|
356
|
+
offset `offset`. A negative offset counts from the end of bitarray.
|
|
357
|
+
"""
|
|
358
|
+
if not isinstance(a, bitarray):
|
|
359
|
+
raise TypeError("bitarray expected, got %r" % type(a).__name__)
|
|
360
|
+
if a.readonly:
|
|
361
|
+
raise TypeError("cannot pack into read-only bitarray")
|
|
362
|
+
offset = self._normalize_offset(len(a), offset, "pack")
|
|
363
|
+
a[offset:offset + self.width] = self.pack(*values)
|
|
364
|
+
|
|
365
|
+
def _unpack(self, a, i=0):
|
|
366
|
+
values = []
|
|
367
|
+
for field in self._fields:
|
|
368
|
+
j = i + field.width
|
|
369
|
+
# Copy logical bits into endianness required by this field.
|
|
370
|
+
value = field.unpack(bitarray(a[i:j], field.endian))
|
|
371
|
+
if field.has_value:
|
|
372
|
+
values.append(value)
|
|
373
|
+
i = j
|
|
374
|
+
if self._names:
|
|
375
|
+
return self._result_type(self._names)(*values)
|
|
376
|
+
return tuple(values)
|
|
377
|
+
|
|
378
|
+
def unpack(self, a: bitarray) -> Tuple[Any, ...]:
|
|
379
|
+
"""unpack(bitarray) -> tuple
|
|
380
|
+
|
|
381
|
+
Return a tuple containing values unpacked according to this compiled format.
|
|
382
|
+
"""
|
|
383
|
+
if not isinstance(a, bitarray):
|
|
384
|
+
raise TypeError("bitarray expected, got %r" % type(a).__name__)
|
|
385
|
+
if len(a) != self.width:
|
|
386
|
+
raise ValueError("expected bitarray of length %d to unpack, "
|
|
387
|
+
"got %d" % (self.width, len(a)))
|
|
388
|
+
return self._unpack(a)
|
|
389
|
+
|
|
390
|
+
def unpack_from(self, a: bitarray, offset: int = 0) -> Tuple[Any, ...]:
|
|
391
|
+
"""unpack_from(bitarray, offset=0) -> tuple
|
|
392
|
+
|
|
393
|
+
Unpack values from bitarray starting at bit offset `offset`, and return a
|
|
394
|
+
tuple. A negative offset counts from the end of bitarray.
|
|
395
|
+
"""
|
|
396
|
+
if not isinstance(a, bitarray):
|
|
397
|
+
raise TypeError("bitarray expected, got %r" % type(a).__name__)
|
|
398
|
+
offset = self._normalize_offset(len(a), offset, "unpack")
|
|
399
|
+
return self._unpack(a, offset)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
@functools.lru_cache()
|
|
403
|
+
def compile(format: str) -> Struct:
|
|
404
|
+
"""compile(format) -> Struct
|
|
405
|
+
|
|
406
|
+
Compile given format string and return a compiled format object that
|
|
407
|
+
can be used to pack and/or unpack data multiple times.
|
|
408
|
+
"""
|
|
409
|
+
return Struct(format)
|
|
410
|
+
|
|
411
|
+
def pack(format: str, *values: Any) -> bitarray:
|
|
412
|
+
"""pack(format, v1, v2, ...) -> bitarray
|
|
413
|
+
|
|
414
|
+
Return a bitarray containing the values v1, v2, ... packed according
|
|
415
|
+
to the format string.
|
|
416
|
+
"""
|
|
417
|
+
return compile(format).pack(*values)
|
|
418
|
+
|
|
419
|
+
def pack_into(format: str, a: bitarray, offset: int, *values: Any) -> None:
|
|
420
|
+
"""pack_into(format, bitarray, offset, v1, v2, ...) -> None
|
|
421
|
+
|
|
422
|
+
Pack the values v1, v2, ... into the writable bitarray starting at bit
|
|
423
|
+
offset `offset`. A negative offset counts from the end of bitarray.
|
|
424
|
+
"""
|
|
425
|
+
return compile(format).pack_into(a, offset, *values)
|
|
426
|
+
|
|
427
|
+
def unpack(format: str, a: bitarray) -> Tuple[Any, ...]:
|
|
428
|
+
"""unpack(format, bitarray) -> tuple
|
|
429
|
+
|
|
430
|
+
Return a tuple containing values unpacked according to the format string.
|
|
431
|
+
"""
|
|
432
|
+
return compile(format).unpack(a)
|
|
433
|
+
|
|
434
|
+
def unpack_from(format: str, a: bitarray, offset: int = 0) -> Tuple[Any, ...]:
|
|
435
|
+
"""unpack_from(format, bitarray, offset=0) -> tuple
|
|
436
|
+
|
|
437
|
+
Unpack values from bitarray starting at bit offset `offset`, and return a
|
|
438
|
+
tuple. A negative offset counts from the end of bitarray.
|
|
439
|
+
"""
|
|
440
|
+
return compile(format).unpack_from(a, offset)
|
|
441
|
+
|
|
442
|
+
def calcsize(format: str) -> int:
|
|
443
|
+
"""calcsize(format) -> int
|
|
444
|
+
|
|
445
|
+
Return the size (in bits) of the struct corresponding to the format string.
|
|
446
|
+
"""
|
|
447
|
+
return compile(format).width
|
bitarray/py.typed
ADDED
|
File without changes
|