lvbitpack 1.0.0__py3-none-any.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.
lvbitpack/__init__.py ADDED
@@ -0,0 +1,60 @@
1
+ """LabVIEW Boolean arrays in Python: Boolean Array To Number, its inverse, and flattening.
2
+
3
+ >>> import lvbitpack
4
+ >>> lvbitpack.bools_to_number([False, True], lvbitpack.I8)
5
+ -2
6
+ >>> from lvbitpack import I8, I16, I32, I64, U8, U16, U32, U64
7
+ >>> [t.name for t in (U8, U16, U32, U64, I8, I16, I32, I64)]
8
+ ['u8', 'u16', 'u32', 'u64', 'i8', 'i16', 'i32', 'i64']
9
+ """
10
+
11
+ from ._bytes import bools_to_bytes, bytes_to_bools, bytes_to_number, number_to_bytes
12
+ from ._errors import BitpackError
13
+ from ._flatten import pack_flattened, unpack_flattened
14
+ from ._modes import SignExtension
15
+ from ._number import bools_to_number, number_to_bools
16
+ from ._pack import pack_bools, unpack_bools
17
+ from ._types import I8, I16, I32, I64, U8, U16, U32, U64, IntType
18
+
19
+ __version__ = "1.0.0"
20
+
21
+ # Functions show lvbitpack as their module in help(); classes keep their real module so
22
+ # that inspect.getsource and pickles find them where they are defined.
23
+ for _public in (
24
+ bools_to_bytes,
25
+ bools_to_number,
26
+ bytes_to_bools,
27
+ bytes_to_number,
28
+ number_to_bools,
29
+ number_to_bytes,
30
+ pack_bools,
31
+ pack_flattened,
32
+ unpack_bools,
33
+ unpack_flattened,
34
+ ):
35
+ _public.__module__ = __name__
36
+ del _public
37
+
38
+ __all__ = [
39
+ "I8",
40
+ "I16",
41
+ "I32",
42
+ "I64",
43
+ "U8",
44
+ "U16",
45
+ "U32",
46
+ "U64",
47
+ "BitpackError",
48
+ "IntType",
49
+ "SignExtension",
50
+ "bools_to_bytes",
51
+ "bools_to_number",
52
+ "bytes_to_bools",
53
+ "bytes_to_number",
54
+ "number_to_bools",
55
+ "number_to_bytes",
56
+ "pack_bools",
57
+ "pack_flattened",
58
+ "unpack_bools",
59
+ "unpack_flattened",
60
+ ]
lvbitpack/__main__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Run the command line interface with ``python -m lvbitpack``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ._cli import main
6
+
7
+ raise SystemExit(main())
lvbitpack/_bytes.py ADDED
@@ -0,0 +1,96 @@
1
+ """Fixed-width bytes for the numbers Boolean Array To Number produces.
2
+
3
+ The element to bit mapping (element 0 is the least significant bit) never depends on byte
4
+ order; byte order only lays the finished integer out across its bytes. The default is
5
+ big-endian, the order of LabVIEW's Flatten To String and Type Cast.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Iterable
11
+ from typing import Literal, SupportsIndex
12
+
13
+ from ._errors import BitpackError, Buffer, as_buffer, as_int, plain_str, quote
14
+ from ._modes import SignExtension
15
+ from ._number import bools_to_number, number_to_bools
16
+ from ._types import IntType, check_type
17
+
18
+ ByteOrder = Literal["big", "little"]
19
+
20
+
21
+ def _check_order(byteorder: object) -> Literal["big", "little"]:
22
+ text = plain_str(byteorder)
23
+ if text == "big":
24
+ return "big"
25
+ if text == "little":
26
+ return "little"
27
+ raise BitpackError(f"byteorder must be 'big' or 'little', not {quote(byteorder)}")
28
+
29
+
30
+ def number_to_bytes(
31
+ number: SupportsIndex, int_type: IntType, *, byteorder: ByteOrder = "big"
32
+ ) -> bytes:
33
+ """The ``int_type.bits // 8`` bytes of ``number``, big-endian unless told otherwise.
34
+
35
+ >>> from lvbitpack import I16, number_to_bytes
36
+ >>> number_to_bytes(-2, I16), number_to_bytes(-2, I16, byteorder="little")
37
+ (b'\\xff\\xfe', b'\\xfe\\xff')
38
+ """
39
+ int_type = check_type(int_type)
40
+ order = _check_order(byteorder)
41
+ value = as_int(number, "number")
42
+ if not int_type.contains(value):
43
+ raise BitpackError(
44
+ f"{quote(value)} is out of range for {int_type.name} [{int_type.min}, {int_type.max}]"
45
+ )
46
+ return value.to_bytes(int_type.bits // 8, order, signed=int_type.signed)
47
+
48
+
49
+ def bytes_to_number(data: Buffer, int_type: IntType, *, byteorder: ByteOrder = "big") -> int:
50
+ """Read a number from exactly ``int_type.bits // 8`` bytes.
51
+
52
+ ``data`` is any buffer-protocol object (``bytes``, ``bytearray``, ``memoryview``,
53
+ ``array.array``, NumPy arrays), read as its raw bytes in memory order.
54
+
55
+ >>> from lvbitpack import I16, U16, bytes_to_number
56
+ >>> bytes_to_number(b"\\xff\\xfe", I16), bytes_to_number(b"\\xff\\xfe", U16)
57
+ (-2, 65534)
58
+ """
59
+ int_type = check_type(int_type)
60
+ order = _check_order(byteorder)
61
+ raw = as_buffer(data)
62
+ width = int_type.bits // 8
63
+ if len(raw) != width:
64
+ unit = "byte" if width == 1 else "bytes"
65
+ raise BitpackError(f"{int_type.name} needs exactly {width} {unit}, got {len(raw)}")
66
+ return int.from_bytes(raw, order, signed=int_type.signed)
67
+
68
+
69
+ def bools_to_bytes(
70
+ values: Iterable[bool | int],
71
+ int_type: IntType,
72
+ *,
73
+ sign_extension: SignExtension = SignExtension.IF_SIGNED,
74
+ byteorder: ByteOrder = "big",
75
+ ) -> bytes:
76
+ """``bools_to_number`` followed by ``number_to_bytes``.
77
+
78
+ >>> from lvbitpack import U16, bools_to_bytes
79
+ >>> bools_to_bytes([False, True], U16), bools_to_bytes([False, True], U16, byteorder="little")
80
+ (b'\\x00\\x02', b'\\x02\\x00')
81
+ """
82
+ int_type = check_type(int_type)
83
+ order = _check_order(byteorder)
84
+ number = bools_to_number(values, int_type, sign_extension=sign_extension)
85
+ return number_to_bytes(number, int_type, byteorder=order)
86
+
87
+
88
+ def bytes_to_bools(data: Buffer, int_type: IntType, *, byteorder: ByteOrder = "big") -> list[bool]:
89
+ """``bytes_to_number`` followed by ``number_to_bools``; element 0 is the low bit.
90
+
91
+ >>> from lvbitpack import U16, bytes_to_bools
92
+ >>> bytes_to_bools(b"\\x00\\x02", U16)[:3]
93
+ [False, True, False]
94
+ """
95
+ number = bytes_to_number(data, int_type, byteorder=byteorder)
96
+ return number_to_bools(number, int_type)
lvbitpack/_cli.py ADDED
@@ -0,0 +1,154 @@
1
+ """Command line interface: Boolean arrays to numbers and flattened bytes, and back."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from collections.abc import Sequence
7
+
8
+ from . import __version__
9
+ from ._cli_io import MESSAGE_LIMIT, Parser, finish, write
10
+ from ._errors import BitpackError, quote
11
+ from ._flatten import pack_flattened, unpack_flattened
12
+ from ._modes import SignExtension
13
+ from ._number import bools_to_number, number_to_bools
14
+ from ._types import IntType
15
+
16
+ _TRUE, _FALSE, _SKIP = frozenset("1tT"), frozenset("0fF"), frozenset(" ,_")
17
+ _NUMBER_LIMIT = 100 # characters; a 64-bit value never needs more
18
+
19
+ _EXAMPLES = """\
20
+ examples:
21
+ lvbitpack pack 01 --type u8 -> 2
22
+ lvbitpack pack 01 --type i8 -> -2
23
+ lvbitpack pack 0101 --type i8 -s never -> 10
24
+ lvbitpack pack 0101 --type i8 -s always -> -6
25
+ lvbitpack pack 01 --type i8 --base hex -> 0xfe
26
+ lvbitpack unpack 2 --type u8 -> 01000000
27
+ lvbitpack unpack -2 --type i8 -> 01111111
28
+ lvbitpack flatten 1011 -> 0000000401000101
29
+ lvbitpack unflatten 0000000401000101 -> 1011
30
+
31
+ Bit strings list element 0 first; element 0 is the least significant bit of the number.
32
+ 1/t/T is TRUE, 0/f/F is FALSE; spaces, commas and underscores are ignored. flatten prints
33
+ LabVIEW's Flatten To String bytes of the Boolean array as hex. Data errors print to stderr
34
+ and exit 1; usage errors exit 2.
35
+ """
36
+
37
+
38
+ def _bits(text: str) -> list[bool]:
39
+ bits: list[bool] = []
40
+ for position, char in enumerate(text):
41
+ if char in _SKIP:
42
+ continue
43
+ if char in _TRUE or char in _FALSE:
44
+ bits.append(char in _TRUE)
45
+ else:
46
+ raise BitpackError(f"character {position} is {quote(char)}; use 0/1, f/t or F/T")
47
+ return bits
48
+
49
+
50
+ def _number(text: str) -> int:
51
+ if len(text) > _NUMBER_LIMIT:
52
+ raise BitpackError(f"the number has {len(text)} characters; at most 100 are read")
53
+ try:
54
+ if not text.isascii(): # int() would also read other scripts' digits
55
+ raise ValueError(text)
56
+ return int(text, 0)
57
+ except ValueError:
58
+ raise BitpackError(f"not an integer: {quote(text)}") from None
59
+
60
+
61
+ def _hex(text: str) -> bytes:
62
+ cleaned = text.strip().removeprefix("0x").removeprefix("0X").replace("_", "")
63
+ try:
64
+ return bytes.fromhex(cleaned)
65
+ except ValueError:
66
+ raise BitpackError(f"not valid hex: {quote(text)}") from None
67
+
68
+
69
+ def _int_type(text: str) -> IntType:
70
+ try:
71
+ return IntType.parse(text)
72
+ except BitpackError as error:
73
+ raise argparse.ArgumentTypeError(str(error)) from None
74
+
75
+
76
+ def _show(bits: list[bool]) -> str:
77
+ return "".join("1" if bit else "0" for bit in bits)
78
+
79
+
80
+ def _build_parser() -> argparse.ArgumentParser:
81
+ parser = Parser(
82
+ prog="lvbitpack",
83
+ description="LabVIEW Boolean Array To Number, Number To Boolean Array and flattening.",
84
+ epilog=_EXAMPLES,
85
+ formatter_class=argparse.RawDescriptionHelpFormatter,
86
+ )
87
+ parser.add_argument("--version", action="version", version=f"lvbitpack {__version__}")
88
+ sub = parser.add_subparsers(dest="command", required=True)
89
+ types = "u8, u16, u32, u64, i8, i16, i32 or i64"
90
+
91
+ pack = sub.add_parser("pack", help="Boolean array to number")
92
+ pack.add_argument("bits", help="bit string, element 0 (the LSB) first, e.g. 01 or 0,1")
93
+ pack.add_argument("--type", "-t", required=True, type=_int_type, help=f"integer type: {types}")
94
+ pack.add_argument(
95
+ "--sign-extension",
96
+ "-s",
97
+ choices=[mode.value for mode in SignExtension],
98
+ default=SignExtension.IF_SIGNED.value,
99
+ help="padding of a short array (default: if_signed, LabVIEW's default)",
100
+ )
101
+ pack.add_argument("--base", choices=["dec", "hex"], default="dec", help="output base")
102
+
103
+ unpack = sub.add_parser("unpack", help="number to Boolean array")
104
+ unpack.add_argument("number", help="integer: decimal, or 0x/0o/0b prefixed")
105
+ unpack.add_argument(
106
+ "--type", "-t", required=True, type=_int_type, help=f"integer type: {types}"
107
+ )
108
+
109
+ flatten = sub.add_parser("flatten", help="Boolean array to LabVIEW flattened bytes (hex)")
110
+ flatten.add_argument("bits", help="bit string, element 0 first")
111
+ unflatten = sub.add_parser("unflatten", help="LabVIEW flattened bytes (hex) to bit string")
112
+ unflatten.add_argument("hex", help="the flattened bytes as hex")
113
+ return parser
114
+
115
+
116
+ def _run(args: argparse.Namespace) -> str:
117
+ if args.command == "pack":
118
+ int_type = args.type
119
+ mode = SignExtension(args.sign_extension)
120
+ number = bools_to_number(_bits(args.bits), int_type, sign_extension=mode)
121
+ return f"0x{number & int_type.mask:x}" if args.base == "hex" else str(number)
122
+ if args.command == "unpack":
123
+ int_type = args.type
124
+ return _show(number_to_bools(_number(args.number), int_type))
125
+ if args.command == "flatten":
126
+ return pack_flattened(_bits(args.bits)).hex()
127
+ return _show(unpack_flattened(_hex(args.hex)))
128
+
129
+
130
+ def main(argv: Sequence[str] | None = None) -> int:
131
+ """Entry point for the ``lvbitpack`` console command.
132
+
133
+ Exit codes: 0 done, 1 bad data (or the reader of the output went away), 2 bad usage.
134
+
135
+ >>> from lvbitpack._cli import main
136
+ >>> main(["pack", "01", "--type", "i8"])
137
+ -2
138
+ 0
139
+ """
140
+ parser = _build_parser()
141
+ try:
142
+ args = parser.parse_args(argv)
143
+ except SystemExit as stop:
144
+ return stop.code if isinstance(stop.code, int) else 2
145
+ try:
146
+ output = _run(args)
147
+ except BitpackError as error:
148
+ write("stderr", f"lvbitpack: error: {str(error)[:MESSAGE_LIMIT]}\n")
149
+ return finish(1)
150
+ return finish(0 if write("stdout", output + "\n") else 1)
151
+
152
+
153
+ if __name__ == "__main__":
154
+ raise SystemExit(main())
lvbitpack/_cli_io.py ADDED
@@ -0,0 +1,83 @@
1
+ """Command line plumbing: bounded usage errors and quiet exits when a pipe goes away."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import re
8
+ import sys
9
+ from typing import TYPE_CHECKING, Any, NoReturn
10
+
11
+ from ._text import printable
12
+
13
+ if TYPE_CHECKING:
14
+ from _typeshed import SupportsWrite
15
+
16
+ # Usage and data error messages are cut here, so a huge argument is never echoed back.
17
+ MESSAGE_LIMIT = 300
18
+
19
+
20
+ def silence(stream: object) -> None:
21
+ """Point ``stream``'s descriptor at the null device after its reader went away.
22
+
23
+ Python flushes the streams at exit; on a closed pipe that flush fails and the process
24
+ exits 120 with a warning. Redirecting the descriptor makes every later write succeed.
25
+ Only the failing stream is touched.
26
+ """
27
+ try:
28
+ fd = stream.fileno() # type: ignore[attr-defined]
29
+ except (AttributeError, OSError, ValueError):
30
+ return
31
+ null = os.open(os.devnull, os.O_WRONLY)
32
+ if null != fd: # a closed fd is reused by open(): then it already is the null device
33
+ os.dup2(null, fd)
34
+ os.close(null)
35
+
36
+
37
+ def finish(code: int) -> int:
38
+ """Flush both streams and return ``code``; a stream whose reader left is silenced."""
39
+ for stream in (sys.stdout, sys.stderr):
40
+ try:
41
+ stream.flush()
42
+ except (AttributeError, OSError, ValueError):
43
+ silence(stream)
44
+ return code
45
+
46
+
47
+ def write(stream_name: str, text: str) -> bool:
48
+ """Write ``text`` to stdout or stderr; return False when the reader went away."""
49
+ stream = sys.stdout if stream_name == "stdout" else sys.stderr
50
+ try:
51
+ stream.write(text) # every message is printable ASCII, so any stream can encode it
52
+ stream.flush()
53
+ except (AttributeError, OSError, ValueError):
54
+ silence(stream)
55
+ return False
56
+ return True
57
+
58
+
59
+ class Parser(argparse.ArgumentParser):
60
+ """An argument parser whose errors are bounded and survive a closed stderr."""
61
+
62
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
63
+ super().__init__(*args, **kwargs)
64
+ # Before Python 3.14 only -<digits> counted as a negative number, so -0x2 or -1_0
65
+ # were taken for options; read any dash followed by a digit as a value.
66
+ self._negative_number_matcher = re.compile(r"^-\.?\d")
67
+
68
+ def error(self, message: str) -> NoReturn:
69
+ message = printable(message)
70
+ if len(message) > MESSAGE_LIMIT:
71
+ message = message[:MESSAGE_LIMIT] + "..."
72
+ self.exit(2, f"{self.format_usage()}{self.prog}: error: {message}\n")
73
+
74
+ lost_output = False # set on the instance when help or version text could not be written
75
+
76
+ def exit(self, status: int = 0, message: str | None = None) -> NoReturn:
77
+ if message:
78
+ write("stderr", message)
79
+ raise SystemExit(finish(1 if status == 0 and self.lost_output else status))
80
+
81
+ def _print_message(self, message: str, file: SupportsWrite[str] | None = None) -> None:
82
+ if message and not write("stderr" if file is sys.stderr else "stdout", message):
83
+ self.lost_output = True
lvbitpack/_errors.py ADDED
@@ -0,0 +1,138 @@
1
+ """The library's error type and the input checks every public function shares.
2
+
3
+ Every check looks at ``type(value)``, never at ``value.__class__``, so an object that lies
4
+ about its class (or whose attribute lookup fails) cannot slip past or raise its own error.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import operator
10
+ import re
11
+ from collections.abc import Iterable
12
+ from typing import Protocol
13
+
14
+ from ._text import plain_str, quote, type_name
15
+
16
+ __all__ = ["BitpackError", "Buffer", "as_bool", "as_bools", "as_buffer", "as_int"]
17
+ __all__ += ["is_integer", "numpy_bool", "plain_str", "quote", "type_name"]
18
+
19
+ _NUMPY_BOOL_NAMES = frozenset({"bool", "bool_"})
20
+ _BYTE_STRINGS = (bytes, bytearray, memoryview)
21
+ _FIELD_NAME = re.compile(r":[^:]*:") # struct-format field names, which may contain an O
22
+
23
+
24
+ class Buffer(Protocol):
25
+ """Any object with the buffer protocol: bytes, bytearray, memoryview, array, NumPy."""
26
+
27
+ def __buffer__(self, flags: int, /) -> memoryview: ...
28
+
29
+
30
+ class BitpackError(ValueError):
31
+ """Raised for any input lvbitpack refuses: a bad value, a bad type or a bad size.
32
+
33
+ It subclasses ``ValueError``, so ``except ValueError`` keeps working.
34
+
35
+ >>> from lvbitpack import BitpackError, U8, number_to_bools
36
+ >>> try:
37
+ ... number_to_bools(256, U8)
38
+ ... except BitpackError as error:
39
+ ... print(error)
40
+ 256 is out of range for u8 [0, 255]
41
+ """
42
+
43
+
44
+ def is_integer(value: object) -> bool:
45
+ """Whether ``value``'s type has ``__index__`` and is not ``bool``."""
46
+ kind = type(value)
47
+ if kind is bool or numpy_bool(value) is not None:
48
+ return False
49
+ try:
50
+ return hasattr(kind, "__index__")
51
+ except Exception: # a metaclass whose attribute lookup fails
52
+ return False
53
+
54
+
55
+ def as_int(value: object, what: str) -> int:
56
+ """``value`` as a plain ``int``; ``bool`` and non-integers are refused."""
57
+ if type(value) is bool or numpy_bool(value) is not None:
58
+ raise BitpackError(f"{what} must be an integer, not bool")
59
+ try:
60
+ return int(operator.index(value)) # type: ignore[arg-type]
61
+ except TypeError:
62
+ raise BitpackError(f"{what} must be an integer, not {type_name(value)}") from None
63
+
64
+
65
+ def numpy_bool(value: object) -> bool | None:
66
+ """The truth of a NumPy boolean scalar, or None when ``value`` is not one."""
67
+ kind = type(value)
68
+ try:
69
+ module = type.__dict__["__module__"].__get__(kind, type)
70
+ except Exception:
71
+ return None
72
+ if type_name(value) not in _NUMPY_BOOL_NAMES or type(module) is not str:
73
+ return None
74
+ if module != "numpy" and not module.startswith("numpy."):
75
+ return None
76
+ try:
77
+ truth = bool(value)
78
+ except Exception:
79
+ return None
80
+ return truth
81
+
82
+
83
+ def as_bool(value: object, index: int) -> bool:
84
+ """One array element as a ``bool``: True/False, 0/1, or a NumPy boolean."""
85
+ kind = type(value)
86
+ if kind is bool:
87
+ return value # type: ignore[return-value]
88
+ if kind is int: # the common 0/1 case, before the slower checks
89
+ if value in (0, 1):
90
+ return value == 1
91
+ raise BitpackError(
92
+ f"element {index} must be a boolean (True/False or 0/1), not {quote(value)}"
93
+ )
94
+ truth = numpy_bool(value)
95
+ if truth is not None:
96
+ return truth
97
+ try:
98
+ number = int(operator.index(value)) # type: ignore[arg-type]
99
+ except TypeError:
100
+ number = None
101
+ if number in (0, 1):
102
+ return number == 1
103
+ raise BitpackError(f"element {index} must be a boolean (True/False or 0/1), not {quote(value)}")
104
+
105
+
106
+ def as_bools(values: Iterable[object]) -> list[bool]:
107
+ """A finite iterable of booleans as a list; text and byte strings are refused."""
108
+ kind = type(values)
109
+ if issubclass(kind, (str, *_BYTE_STRINGS)):
110
+ what = "a string" if issubclass(kind, str) else "a byte string"
111
+ raise BitpackError(
112
+ f"values must be an iterable of booleans, not {type_name(values)}; "
113
+ f"{what} is not a Boolean array"
114
+ )
115
+ try:
116
+ items = iter(values)
117
+ except TypeError:
118
+ raise BitpackError(
119
+ f"values must be an iterable of booleans, not {type_name(values)}"
120
+ ) from None
121
+ return [as_bool(item, index) for index, item in enumerate(items)]
122
+
123
+
124
+ def as_buffer(data: object, what: str = "data") -> bytes:
125
+ """The raw bytes of any buffer-protocol object; text and everything else is refused."""
126
+ if not issubclass(type(data), str):
127
+ try:
128
+ with memoryview(data) as view: # type: ignore[arg-type]
129
+ if "O" in _FIELD_NAME.sub("", view.format): # object pointers are not data
130
+ raise BitpackError(f"{what} holds Python objects, not bytes")
131
+ return view.tobytes() # raw bytes in memory order, any item format
132
+ except TypeError:
133
+ pass
134
+ except BitpackError:
135
+ raise
136
+ except ValueError: # a released view, or a buffer of a kind Python cannot export
137
+ raise BitpackError(f"{what} cannot be read as bytes ({type_name(data)})") from None
138
+ raise BitpackError(f"{what} must be a bytes-like object, not {type_name(data)}")
lvbitpack/_flatten.py ADDED
@@ -0,0 +1,56 @@
1
+ """LabVIEW's flattened form of a 1-D Boolean array: an I32 count, then one byte per element.
2
+
3
+ This is what Flatten To String writes for a Boolean array (and what front-panel datalogs
4
+ hold): the count is big-endian, TRUE is written as 0x01 and FALSE as 0x00, and on reading
5
+ any nonzero byte is TRUE.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Iterable
11
+
12
+ from ._errors import BitpackError, Buffer, as_bools, as_buffer
13
+
14
+ _HEADER = 4
15
+ _MAX_COUNT = 0x7FFFFFFF
16
+
17
+
18
+ def _follow(count: int) -> str:
19
+ return "1 byte follows" if count == 1 else f"{count} bytes follow"
20
+
21
+
22
+ def pack_flattened(values: Iterable[bool | int]) -> bytes:
23
+ """Flatten a 1-D Boolean array the way LabVIEW's Flatten To String does.
24
+
25
+ >>> from lvbitpack import pack_flattened
26
+ >>> pack_flattened([True, False, True, True]).hex()
27
+ '0000000401000101'
28
+ """
29
+ bits = as_bools(values)
30
+ if len(bits) > _MAX_COUNT:
31
+ raise BitpackError(f"a LabVIEW array holds at most {_MAX_COUNT} elements")
32
+ return len(bits).to_bytes(_HEADER, "big") + bytes(bits)
33
+
34
+
35
+ def unpack_flattened(data: Buffer) -> list[bool]:
36
+ """Read a flattened 1-D Boolean array: the I32 count, then exactly that many bytes.
37
+
38
+ Any nonzero element byte is TRUE. A negative count, missing bytes or bytes left over
39
+ after the last element raise ``BitpackError``.
40
+
41
+ >>> from lvbitpack import unpack_flattened
42
+ >>> unpack_flattened(bytes.fromhex("0000000401000101"))
43
+ [True, False, True, True]
44
+ """
45
+ raw = as_buffer(data)
46
+ if len(raw) < _HEADER:
47
+ raise BitpackError(f"a flattened array starts with a 4-byte count; got {len(raw)} bytes")
48
+ count = int.from_bytes(raw[:_HEADER], "big", signed=True)
49
+ if count < 0:
50
+ raise BitpackError(f"the element count is negative ({count})")
51
+ body = len(raw) - _HEADER
52
+ if body < count:
53
+ raise BitpackError(f"the count says {count} elements but only {_follow(body)}")
54
+ if body > count:
55
+ raise BitpackError(f"{_follow(body - count)} the last of the {count} elements")
56
+ return [byte != 0 for byte in raw[_HEADER:]]
lvbitpack/_modes.py ADDED
@@ -0,0 +1,49 @@
1
+ """The sign extension modes of Boolean Array To Number."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import enum
6
+ from typing import Any
7
+
8
+ from ._errors import BitpackError, plain_str, quote
9
+
10
+
11
+ class _ModeLookup(enum.EnumMeta):
12
+ """Looks a mode up by its exact text, and refuses anything else with ``BitpackError``.
13
+
14
+ The standard lookup hashes the value and prints its repr before asking ``_missing_``,
15
+ so a huge integer or a hostile object would raise its own error instead of ours.
16
+ """
17
+
18
+ def __call__(cls, value: Any, *args: Any, **kwargs: Any) -> Any:
19
+ if args or kwargs: # the functional API cannot extend an enum that has members
20
+ raise BitpackError("SignExtension takes exactly one value")
21
+ if type(value) is cls:
22
+ return value
23
+ text = plain_str(value)
24
+ for member in cls: # type: ignore[var-annotated]
25
+ if text is not None and member.value == text:
26
+ return member
27
+ raise BitpackError(f"no sign extension mode {quote(value)}; use never, if_signed or always")
28
+
29
+
30
+ class SignExtension(enum.Enum, metaclass=_ModeLookup):
31
+ """How Boolean Array To Number pads an array shorter than the integer width.
32
+
33
+ LabVIEW sets this by right-clicking the node; ``IF_SIGNED`` is LabVIEW's default.
34
+
35
+ - ``NEVER``: pad with FALSE.
36
+ - ``IF_SIGNED``: pad unsigned outputs with FALSE and signed outputs with the last element.
37
+ - ``ALWAYS``: pad with the last element.
38
+
39
+ >>> from lvbitpack import I8, SignExtension, bools_to_number
40
+ >>> array = [False, True, False, True]
41
+ >>> [bools_to_number(array, I8, sign_extension=mode) for mode in SignExtension]
42
+ [10, -6, -6]
43
+ >>> SignExtension("always")
44
+ <SignExtension.ALWAYS: 'always'>
45
+ """
46
+
47
+ NEVER = "never"
48
+ IF_SIGNED = "if_signed"
49
+ ALWAYS = "always"
lvbitpack/_number.py ADDED
@@ -0,0 +1,88 @@
1
+ """LabVIEW's Boolean Array To Number and Number To Boolean Array for integer outputs.
2
+
3
+ Element 0 of the array is the least significant bit. The bits map straight onto one
4
+ fixed-width integer; there is no count and no byte per element, unlike a flattened array.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Iterable
10
+ from typing import SupportsIndex
11
+
12
+ from ._errors import BitpackError, as_bools, as_int, quote
13
+ from ._modes import SignExtension
14
+ from ._types import IntType, check_type
15
+
16
+
17
+ def _check_mode(mode: object) -> SignExtension:
18
+ if type(mode) is not SignExtension:
19
+ raise BitpackError(f"sign_extension must be a SignExtension, not {quote(mode)}")
20
+ return mode
21
+
22
+
23
+ def _pad_bit(bits: list[bool], int_type: IntType, mode: SignExtension) -> int:
24
+ """The bit that fills every position above the supplied elements."""
25
+ if mode is SignExtension.NEVER or not bits:
26
+ return 0
27
+ if mode is SignExtension.IF_SIGNED and not int_type.signed:
28
+ return 0
29
+ return 1 if bits[-1] else 0
30
+
31
+
32
+ def bools_to_number(
33
+ values: Iterable[bool | int],
34
+ int_type: IntType,
35
+ *,
36
+ sign_extension: SignExtension = SignExtension.IF_SIGNED,
37
+ ) -> int:
38
+ """Convert a Boolean array to an integer, as LabVIEW's Boolean Array To Number does.
39
+
40
+ Element 0 is the least significant bit. Elements beyond the width are dropped; a short
41
+ array is padded as ``sign_extension`` says (LabVIEW's default is ``IF_SIGNED``). A signed
42
+ type gives the two's complement value. ``values`` is any finite iterable of
43
+ ``True``/``False``, ``0``/``1`` or NumPy booleans; a string is refused.
44
+
45
+ >>> from lvbitpack import I8, U8, SignExtension, bools_to_number
46
+ >>> bools_to_number([False, True], U8), bools_to_number([False, True], I8)
47
+ (2, -2)
48
+ >>> bools_to_number([False, False, True, False, True], U8, sign_extension=SignExtension.ALWAYS)
49
+ 244
50
+ >>> bools_to_number([True] * 9, U8)
51
+ 255
52
+ """
53
+ int_type = check_type(int_type)
54
+ mode = _check_mode(sign_extension)
55
+ bits = as_bools(values)
56
+ width = int_type.bits
57
+ raw = 0
58
+ for index, bit in enumerate(bits[:width]):
59
+ if bit:
60
+ raw |= 1 << index
61
+ if len(bits) < width and _pad_bit(bits, int_type, mode):
62
+ raw |= int_type.mask ^ ((1 << len(bits)) - 1)
63
+ if int_type.signed and raw > int_type.max:
64
+ raw -= 1 << width
65
+ return raw
66
+
67
+
68
+ def number_to_bools(number: SupportsIndex, int_type: IntType) -> list[bool]:
69
+ """Convert an integer to a Boolean array, as LabVIEW's Number To Boolean Array does.
70
+
71
+ The result has one element per bit of the type (8, 16, 32 or 64); element 0 holds the
72
+ least significant bit of the two's complement form. ``number`` must fit the type: LabVIEW
73
+ would have coerced it to the wire's type first, so a value outside it is refused.
74
+
75
+ >>> from lvbitpack import I8, U8, number_to_bools
76
+ >>> number_to_bools(2, U8)
77
+ [False, True, False, False, False, False, False, False]
78
+ >>> number_to_bools(-2, I8) == [False] + [True] * 7
79
+ True
80
+ """
81
+ int_type = check_type(int_type)
82
+ value = as_int(number, "number")
83
+ if not int_type.contains(value):
84
+ raise BitpackError(
85
+ f"{quote(value)} is out of range for {int_type.name} [{int_type.min}, {int_type.max}]"
86
+ )
87
+ raw = value & int_type.mask
88
+ return [bool((raw >> index) & 1) for index in range(int_type.bits)]
lvbitpack/_pack.py ADDED
@@ -0,0 +1,72 @@
1
+ """Compact bit packing of Boolean arrays: eight elements per byte, no header.
2
+
3
+ LabVIEW itself stores a Boolean as one byte; this is the dense form for wire protocols,
4
+ register maps and files that want one bit per element.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Iterable
10
+ from typing import Literal, SupportsIndex
11
+
12
+ from ._errors import BitpackError, Buffer, as_bools, as_buffer, as_int, plain_str, quote
13
+
14
+ BitOrder = Literal["msb", "lsb"]
15
+
16
+
17
+ def _check_order(bit_order: object) -> bool:
18
+ """True for least-significant-bit first."""
19
+ text = plain_str(bit_order)
20
+ if text in ("msb", "lsb"):
21
+ return text == "lsb"
22
+ raise BitpackError(f"bit_order must be 'msb' or 'lsb', not {quote(bit_order)}")
23
+
24
+
25
+ def pack_bools(values: Iterable[bool | int], *, bit_order: BitOrder = "msb") -> bytes:
26
+ """Pack Booleans eight to a byte, zero-filling the unused bits of the last byte.
27
+
28
+ With ``bit_order="msb"`` (default) element 0 is bit 7 (0x80) of byte 0. With ``"lsb"``
29
+ element 0 is bit 0 of byte 0, so each byte equals Boolean Array To Number of its eight
30
+ elements as a U8.
31
+
32
+ >>> from lvbitpack import pack_bools
33
+ >>> array = [True, True, False, True]
34
+ >>> pack_bools(array), pack_bools(array, bit_order="lsb")
35
+ (b'\\xd0', b'\\x0b')
36
+ """
37
+ lsb = _check_order(bit_order)
38
+ bits = as_bools(values)
39
+ out = bytearray((len(bits) + 7) // 8)
40
+ for index, bit in enumerate(bits):
41
+ if bit:
42
+ shift = index % 8 if lsb else 7 - index % 8
43
+ out[index // 8] |= 1 << shift
44
+ return bytes(out)
45
+
46
+
47
+ def unpack_bools(
48
+ data: Buffer, count: SupportsIndex | None = None, *, bit_order: BitOrder = "msb"
49
+ ) -> list[bool]:
50
+ """Unpack ``count`` Booleans (default: every bit) from bytes written by ``pack_bools``.
51
+
52
+ ``count`` drops the fill bits of the last byte; it must be between 0 and ``8 * len(data)``.
53
+ The fill bits are not checked.
54
+
55
+ >>> from lvbitpack import unpack_bools
56
+ >>> unpack_bools(b"\\xd0", 4)
57
+ [True, True, False, True]
58
+ >>> unpack_bools(b"\\x0b", 4, bit_order="lsb")
59
+ [True, True, False, True]
60
+ """
61
+ lsb = _check_order(bit_order)
62
+ raw = as_buffer(data)
63
+ total = len(raw) * 8
64
+ wanted = total if count is None else as_int(count, "count")
65
+ if wanted < 0:
66
+ raise BitpackError(f"count must not be negative, got {quote(wanted)}")
67
+ if wanted > total:
68
+ raise BitpackError(f"count is {quote(wanted)}, more than the {total} bits in data")
69
+ return [
70
+ bool(raw[index // 8] >> (index % 8 if lsb else 7 - index % 8) & 1)
71
+ for index in range(wanted)
72
+ ]
lvbitpack/_text.py ADDED
@@ -0,0 +1,63 @@
1
+ """Safe text for error messages: short and printable ASCII, whatever the object does."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import reprlib
6
+
7
+ # Values quoted in messages are cut to this many characters, so huge input is never echoed.
8
+ _QUOTE_LIMIT = 40
9
+
10
+
11
+ class _ShortRepr(reprlib.Repr):
12
+ """``reprlib`` with byte strings cut before their repr is built."""
13
+
14
+ def repr_bytes(self, value: bytes, level: int) -> str:
15
+ return repr(bytes(value[: self.maxstring]))
16
+
17
+ def repr_bytearray(self, value: bytearray, level: int) -> str:
18
+ return repr(bytearray(value[: self.maxstring]))
19
+
20
+
21
+ _REPR = _ShortRepr()
22
+ _REPR.maxstring = _REPR.maxother = _QUOTE_LIMIT + 10
23
+ _REPR.maxlist = _REPR.maxtuple = _REPR.maxdict = _REPR.maxset = 4
24
+
25
+
26
+ def printable(text: str) -> str:
27
+ """``text`` with control characters and non-ASCII escaped, so it cannot drive a terminal."""
28
+ return "".join(
29
+ char if " " <= char <= "~" else char.encode("unicode_escape").decode("ascii")
30
+ for char in text
31
+ )
32
+
33
+
34
+ def _cut(text: str) -> str:
35
+ text = printable(text)
36
+ return text if len(text) <= _QUOTE_LIMIT else text[:_QUOTE_LIMIT] + "..."
37
+
38
+
39
+ def type_name(value: object) -> str:
40
+ """The type name of ``value``, cut for messages; a metaclass cannot change it."""
41
+ try:
42
+ name = type.__dict__["__name__"].__get__(type(value), type)
43
+ return _cut(str.__str__(name))
44
+ except Exception:
45
+ return "object"
46
+
47
+
48
+ def quote(value: object) -> str:
49
+ """A short, safe, printable description of ``value`` for an error message."""
50
+ try:
51
+ if issubclass(type(value), int) and type(value) is not bool:
52
+ size = int.bit_length(value) # type: ignore[arg-type] # a subclass cannot lie
53
+ if size > 128:
54
+ return f"an integer of {size} bits"
55
+ return int.__repr__(value)
56
+ return _cut(str.__str__(_REPR.repr(value)))
57
+ except Exception: # a hostile object must not replace our error with its own
58
+ return f"<{type_name(value)} object>"
59
+
60
+
61
+ def plain_str(value: object) -> str | None:
62
+ """``value`` as an exact ``str`` without calling a subclass's methods, else None."""
63
+ return str.__str__(value) if issubclass(type(value), str) else None
lvbitpack/_types.py ADDED
@@ -0,0 +1,150 @@
1
+ """The integer representations and the sign extension modes of Boolean Array To Number."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from ._errors import BitpackError, as_int, is_integer, numpy_bool, plain_str, quote, type_name
8
+
9
+ _VALID_BITS = (8, 16, 32, 64)
10
+ _PREFIXES = (("uint", False), ("int", True), ("u", False), ("i", True))
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class IntType:
15
+ """A LabVIEW integer representation: 8, 16, 32 or 64 bits, signed or unsigned.
16
+
17
+ ``U8`` to ``I64`` are ready-made; equal types compare and hash equal.
18
+
19
+ >>> from lvbitpack import IntType, U16
20
+ >>> IntType(16, False) == U16
21
+ True
22
+ >>> from lvbitpack import BitpackError
23
+ >>> try:
24
+ ... IntType(12, False)
25
+ ... except BitpackError as error:
26
+ ... print(error)
27
+ bits must be 8, 16, 32 or 64, not 12
28
+ """
29
+
30
+ bits: int
31
+ signed: bool
32
+
33
+ def __post_init__(self) -> None:
34
+ bits = as_int(self.bits, "bits")
35
+ if bits not in _VALID_BITS:
36
+ raise BitpackError(f"bits must be 8, 16, 32 or 64, not {quote(bits)}")
37
+ object.__setattr__(self, "bits", bits) # a NumPy or other __index__ width is stored as int
38
+ signed = self.signed if type(self.signed) is bool else numpy_bool(self.signed)
39
+ if signed is None:
40
+ raise BitpackError(f"signed must be a bool, not {type_name(self.signed)}")
41
+ object.__setattr__(self, "signed", signed)
42
+
43
+ @property
44
+ def name(self) -> str:
45
+ """The short lower-case name.
46
+
47
+ >>> from lvbitpack import I32
48
+ >>> I32.name
49
+ 'i32'
50
+ """
51
+ return f"{'i' if self.signed else 'u'}{self.bits}"
52
+
53
+ @property
54
+ def mask(self) -> int:
55
+ """All ones over the type's width.
56
+
57
+ >>> from lvbitpack import U16
58
+ >>> hex(U16.mask)
59
+ '0xffff'
60
+ """
61
+ return (1 << self.bits) - 1
62
+
63
+ @property
64
+ def min(self) -> int:
65
+ """The smallest value the type holds.
66
+
67
+ >>> from lvbitpack import I8, U8
68
+ >>> I8.min, U8.min
69
+ (-128, 0)
70
+ """
71
+ return -(1 << (self.bits - 1)) if self.signed else 0
72
+
73
+ @property
74
+ def max(self) -> int:
75
+ """The largest value the type holds.
76
+
77
+ >>> from lvbitpack import I8, U8
78
+ >>> I8.max, U8.max
79
+ (127, 255)
80
+ """
81
+ return (1 << (self.bits - 1)) - 1 if self.signed else self.mask
82
+
83
+ def contains(self, number: object) -> bool:
84
+ """Whether ``number`` is an integer within the type's range.
85
+
86
+ Any ``__index__`` integer counts; ``bool``, floats and other types are never contained.
87
+
88
+ >>> from lvbitpack import I8
89
+ >>> I8.contains(-128), I8.contains(128), I8.contains(1.0), I8.contains(True)
90
+ (True, False, False, False)
91
+ """
92
+ if not is_integer(number):
93
+ return False
94
+ try:
95
+ value = as_int(number, "number")
96
+ except BitpackError: # an __index__ that refuses, such as a NumPy array's
97
+ return False
98
+ return self.min <= value <= self.max
99
+
100
+ @classmethod
101
+ def parse(cls, text: str) -> IntType:
102
+ """Read a name such as ``"u8"``, ``"I16"``, ``"uint32"`` or ``"int64"``.
103
+
104
+ Case and surrounding white space are ignored.
105
+
106
+ >>> from lvbitpack import IntType
107
+ >>> IntType.parse("I16"), IntType.parse(" uint8 ")
108
+ (IntType(bits=16, signed=True), IntType(bits=8, signed=False))
109
+ """
110
+ plain = plain_str(text)
111
+ if plain is None:
112
+ raise BitpackError(f"an integer type name must be str, not {type_name(text)}")
113
+ key = plain.strip().lower()
114
+ for prefix, signed in _PREFIXES:
115
+ digits = key[len(prefix) :]
116
+ if key.startswith(prefix) and digits in ("8", "16", "32", "64"):
117
+ return cls(int(digits), signed)
118
+ raise BitpackError(
119
+ f"unknown integer type {quote(text)}; use u8, u16, u32, u64, i8, i16, i32 or i64"
120
+ )
121
+
122
+
123
+ U8 = IntType(8, False)
124
+ U16 = IntType(16, False)
125
+ U32 = IntType(32, False)
126
+ U64 = IntType(64, False)
127
+ I8 = IntType(8, True)
128
+ I16 = IntType(16, True)
129
+ I32 = IntType(32, True)
130
+ I64 = IntType(64, True)
131
+
132
+
133
+ def check_type(int_type: object) -> IntType:
134
+ """``int_type`` as a plain ``IntType``; anything else is refused.
135
+
136
+ A subclass is rebuilt from its two fields, so overridden methods change nothing.
137
+ """
138
+ kind = type(int_type)
139
+ if not issubclass(kind, IntType):
140
+ raise BitpackError(f"int_type must be an IntType such as U8, not {type_name(int_type)}")
141
+ try:
142
+ bits = object.__getattribute__(int_type, "bits")
143
+ signed = object.__getattribute__(int_type, "signed")
144
+ extra = len(object.__getattribute__(int_type, "__dict__")) != 2 # a shadowed method
145
+ except Exception: # never initialised, or a subclass whose field lookup fails
146
+ raise BitpackError("int_type is an IntType that was never initialised") from None
147
+ plain = not extra and type(bits) is int and type(signed) is bool
148
+ if kind is IntType and plain and bits in (8, 16, 32, 64):
149
+ return int_type # type: ignore[return-value]
150
+ return IntType(bits, signed) # rebuilt, so a subclass or a tampered value is checked
lvbitpack/py.typed ADDED
File without changes
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.5
2
+ Name: lvbitpack
3
+ Version: 1.0.0
4
+ Summary: LabVIEW Boolean arrays in pure Python: Boolean Array To Number with its sign extension modes, Number To Boolean Array, flattening and bit packing
5
+ Project-URL: Homepage, https://lianes8studio.com
6
+ Project-URL: Source, https://github.com/JaviLianes8/lvbitpack
7
+ Project-URL: Issues, https://github.com/JaviLianes8/lvbitpack/issues
8
+ Project-URL: Changelog, https://github.com/JaviLianes8/lvbitpack/blob/main/CHANGELOG.md
9
+ Project-URL: LinkedIn, https://www.linkedin.com/in/jlianes/
10
+ Project-URL: Funding, https://buymeacoffee.com/jlianesglrs
11
+ Author-email: Javier Lianes <javier@lianes8studio.com>
12
+ License-Expression: MIT
13
+ License-File: LICENSE
14
+ Keywords: binary,bit,bitpack,boolean,flatten,labview,ni,sign-extension
15
+ Classifier: Development Status :: 5 - Production/Stable
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Intended Audience :: Science/Research
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Programming Language :: Python :: 3.14
26
+ Classifier: Topic :: Scientific/Engineering
27
+ Classifier: Topic :: Software Development :: Libraries
28
+ Classifier: Typing :: Typed
29
+ Requires-Python: >=3.10
30
+ Provides-Extra: dev
31
+ Requires-Dist: build; extra == 'dev'
32
+ Requires-Dist: editables; extra == 'dev'
33
+ Requires-Dist: hatchling; extra == 'dev'
34
+ Requires-Dist: mypy; extra == 'dev'
35
+ Requires-Dist: pytest; extra == 'dev'
36
+ Requires-Dist: ruff; extra == 'dev'
37
+ Requires-Dist: twine; extra == 'dev'
38
+ Description-Content-Type: text/markdown
39
+
40
+ # lvbitpack
41
+
42
+ LabVIEW Boolean arrays in pure Python: **Boolean Array To Number** with its three sign
43
+ extension modes, **Number To Boolean Array**, the flattened form LabVIEW writes for a Boolean
44
+ array, and compact bit packing. No dependencies, fully typed, Python 3.10 or newer.
45
+
46
+ The full reference is the [guide](https://github.com/JaviLianes8/lvbitpack/blob/main/GUIDE.md);
47
+ every release is described in the
48
+ [changelog](https://github.com/JaviLianes8/lvbitpack/blob/main/CHANGELOG.md).
49
+
50
+ ## Install
51
+
52
+ ```
53
+ pip install lvbitpack
54
+ ```
55
+
56
+ ## The problem
57
+
58
+ A LabVIEW program that reads digital lines, status registers or flag words often turns a
59
+ Boolean array into one integer with Boolean Array To Number, and back with Number To Boolean
60
+ Array. Element 0 is the least significant bit, elements past the width are dropped, and a
61
+ short array is padded by a per-node setting (Sign Extension Mode) that most people never
62
+ look at, yet it decides whether `{FALSE, TRUE}` as an I8 is 2 or -2. Getting the same
63
+ numbers in Python means reproducing those rules exactly.
64
+
65
+ ## Quick start
66
+
67
+ ```python
68
+ from lvbitpack import I8, U8, U16, SignExtension, bools_to_number, number_to_bools
69
+
70
+ # Element 0 is the least significant bit.
71
+ assert bools_to_number([False, True], U8) == 2
72
+
73
+ # LabVIEW's default mode pads a signed output with the last element: {F, T} as I8 is -2.
74
+ assert bools_to_number([False, True], I8) == -2
75
+ assert bools_to_number([False, True, False, True], I8, sign_extension=SignExtension.NEVER) == 10
76
+ assert bools_to_number([False, True, False, True], I8, sign_extension=SignExtension.ALWAYS) == -6
77
+
78
+ # Elements beyond the width are dropped.
79
+ assert bools_to_number([True] * 9, U8) == 255
80
+
81
+ # The inverse returns one element per bit of the type.
82
+ assert number_to_bools(-2, I8) == [False, True, True, True, True, True, True, True]
83
+ assert len(number_to_bools(1, U16)) == 16
84
+ ```
85
+
86
+ Bytes, LabVIEW's flattened array and dense bit packing:
87
+
88
+ ```python
89
+ from lvbitpack import U16, bools_to_bytes, pack_bools, pack_flattened, unpack_flattened
90
+
91
+ # The number's bytes, big-endian by default like LabVIEW's Flatten To String.
92
+ assert bools_to_bytes([False, True], U16) == b"\x00\x02"
93
+ assert bools_to_bytes([False, True], U16, byteorder="little") == b"\x02\x00"
94
+
95
+ # Flatten To String of a Boolean array: an I32 count, then one byte per element.
96
+ assert pack_flattened([True, False, True, True]).hex() == "0000000401000101"
97
+ assert unpack_flattened(bytes.fromhex("0000000401000101")) == [True, False, True, True]
98
+
99
+ # Eight elements per byte, first element in the high bit (or the low bit with "lsb").
100
+ assert pack_bools([True, True, False, True]) == b"\xd0"
101
+ assert pack_bools([True, True, False, True], bit_order="lsb") == b"\x0b"
102
+ ```
103
+
104
+ Anything refused (a value out of range, a string where a Boolean array belongs, a truncated
105
+ flattened array) raises `lvbitpack.BitpackError`, a `ValueError`.
106
+
107
+ ## Command line
108
+
109
+ ```
110
+ $ lvbitpack pack 01 --type i8
111
+ -2
112
+ $ lvbitpack pack 0101 --type i8 --sign-extension never
113
+ 10
114
+ $ lvbitpack unpack -2 --type i8
115
+ 01111111
116
+ $ lvbitpack flatten 1011
117
+ 0000000401000101
118
+ ```
119
+
120
+ Bit strings list element 0 first. `python -m lvbitpack` works too.
121
+
122
+ ## Why this library
123
+
124
+ - It follows LabVIEW's own rules: the three Sign Extension Mode settings, truncation of long
125
+ arrays, the 8, 16, 32 and 64-bit signed and unsigned outputs, and the flattened Boolean
126
+ array layout, checked against NI's documentation and against output of LabVIEW 2026.
127
+ - NumPy's `packbits` packs 0/1 arrays into `uint8` bytes (high or low bit first) but has no
128
+ notion of LabVIEW's integer widths or sign extension, and needs NumPy. `bitarray` (a C
129
+ extension) and `bitstring` (which depends on `tibs`) are general bit containers; neither
130
+ reproduces Boolean Array To Number. lvbitpack is small, pure Python and dependency free.
131
+
132
+ What it does not cover: Boolean Array To Number with a fixed-point output (see the guide).
133
+
134
+ ## License
135
+
136
+ MIT. See [LICENSE](https://github.com/JaviLianes8/lvbitpack/blob/main/LICENSE).
137
+
138
+ ## Links
139
+
140
+ - Website: https://lianes8studio.com
141
+ - GitHub: https://github.com/JaviLianes8
142
+ - LinkedIn: https://www.linkedin.com/in/jlianes/
@@ -0,0 +1,18 @@
1
+ lvbitpack/__init__.py,sha256=tcHX4zzK0r0cZm537mEzRa6BpAiqHYm8o9b4FA3yVwY,1557
2
+ lvbitpack/__main__.py,sha256=LorHK61MyWjMAULC9zVNqUNX5LAzvugBaz8tq_3Zl6E,153
3
+ lvbitpack/_bytes.py,sha256=R2tl08DqBzmTxam2nLauzKEDTS0PbNtl4JkoLfSMfWE,3626
4
+ lvbitpack/_cli.py,sha256=gK3SpHX9byw1RvZtOelp1mpbjriY359YGXZ4VAvzrD0,5849
5
+ lvbitpack/_cli_io.py,sha256=AbTBEBc-J713mgPT6dLjYL6tViV7bymJcwqjiPjBe50,3077
6
+ lvbitpack/_errors.py,sha256=aRtNDTXEFocZuGUC7R08DzfFAbrhiloZqXTGexmX8w8,5157
7
+ lvbitpack/_flatten.py,sha256=o3zo0e8jqKBvT8goqwkhtocDO3yBhqt8MS3P2hsKUbI,2082
8
+ lvbitpack/_modes.py,sha256=IM63e0KBteiBrqPpbwP8_mweCQan1gGbCCJYGLoFV8M,1817
9
+ lvbitpack/_number.py,sha256=soz4V21wcRHnwaCkZSPvLW45j7dQGQ-uHisGAmn-vrQ,3393
10
+ lvbitpack/_pack.py,sha256=3YUBblGr-dy33aE0bNAFG-pxFwEyS5asXTlRgaO1uNM,2608
11
+ lvbitpack/_text.py,sha256=ReycP95uHqcqw9c38mB_5nISQxhTx1krC5Hk65ACD10,2219
12
+ lvbitpack/_types.py,sha256=mbFWzyrYDq88y8_Iz-cAt2b0tmNsibBoW34ZiPww_Rc,5168
13
+ lvbitpack/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ lvbitpack-1.0.0.dist-info/METADATA,sha256=iFcA7lDqTJqRvpIiA8p-ymk80DH5QRUtydIT7dT6Drs,5900
15
+ lvbitpack-1.0.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
16
+ lvbitpack-1.0.0.dist-info/entry_points.txt,sha256=vsHCk8Ovlmpthqam4kAYJKt-Nc72ufl2FIkZQBDkLTQ,50
17
+ lvbitpack-1.0.0.dist-info/licenses/LICENSE,sha256=wirD5nCA2aQa5UusK_1YHLBogI3CGcw23xAnulp3L_U,1091
18
+ lvbitpack-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ lvbitpack = lvbitpack._cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Javier Lianes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.