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 +60 -0
- lvbitpack/__main__.py +7 -0
- lvbitpack/_bytes.py +96 -0
- lvbitpack/_cli.py +154 -0
- lvbitpack/_cli_io.py +83 -0
- lvbitpack/_errors.py +138 -0
- lvbitpack/_flatten.py +56 -0
- lvbitpack/_modes.py +49 -0
- lvbitpack/_number.py +88 -0
- lvbitpack/_pack.py +72 -0
- lvbitpack/_text.py +63 -0
- lvbitpack/_types.py +150 -0
- lvbitpack/py.typed +0 -0
- lvbitpack-1.0.0.dist-info/METADATA +142 -0
- lvbitpack-1.0.0.dist-info/RECORD +18 -0
- lvbitpack-1.0.0.dist-info/WHEEL +4 -0
- lvbitpack-1.0.0.dist-info/entry_points.txt +2 -0
- lvbitpack-1.0.0.dist-info/licenses/LICENSE +21 -0
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
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,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.
|