lvtypedesc 0.2.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.
lvtypedesc/__init__.py ADDED
@@ -0,0 +1,43 @@
1
+ """Parse LabVIEW type-descriptor byte strings into a readable, typed schema."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ._codes import Category, TypeCode, known_codes
6
+ from ._emit import to_bytes
7
+ from ._errors import TypeDescError
8
+ from ._model import (
9
+ VARIABLE,
10
+ Array,
11
+ Boolean,
12
+ Cluster,
13
+ FixedPoint,
14
+ LVString,
15
+ Refnum,
16
+ Scalar,
17
+ TypeDesc,
18
+ describe,
19
+ )
20
+ from ._parser import parse_type_descriptor
21
+ from ._pretty import pretty
22
+
23
+ __version__ = "0.2.0"
24
+
25
+ __all__ = [
26
+ "VARIABLE",
27
+ "Array",
28
+ "Boolean",
29
+ "Category",
30
+ "Cluster",
31
+ "FixedPoint",
32
+ "LVString",
33
+ "Refnum",
34
+ "Scalar",
35
+ "TypeCode",
36
+ "TypeDesc",
37
+ "TypeDescError",
38
+ "describe",
39
+ "known_codes",
40
+ "parse_type_descriptor",
41
+ "pretty",
42
+ "to_bytes",
43
+ ]
lvtypedesc/_cli.py ADDED
@@ -0,0 +1,91 @@
1
+ """Command-line interface for lvtypedesc.
2
+
3
+ Decode a hex type descriptor into a readable type, or list the codes the library
4
+ knows. Errors are written to stderr and the process exits with status 1.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import sys
11
+ from collections.abc import Sequence
12
+
13
+ from . import __version__
14
+ from ._codes import known_codes
15
+ from ._errors import TypeDescError
16
+ from ._model import describe
17
+ from ._parser import parse_type_descriptor
18
+ from ._pretty import pretty
19
+
20
+ _EXAMPLES = """\
21
+ examples:
22
+ lvtypedesc decode 00040003 -> I32
23
+ lvtypedesc decode "0004 000A" -> DBL (spaces are ignored)
24
+ lvtypedesc decode 000E00400001FFFFFFFF0004000A -> 1D Array of DBL
25
+ lvtypedesc decode --tree 000E00500002000400030004000A
26
+ lvtypedesc codes -> list every known type code
27
+ """
28
+
29
+
30
+ def _build_parser() -> argparse.ArgumentParser:
31
+ parser = argparse.ArgumentParser(
32
+ prog="lvtypedesc",
33
+ description="Parse LabVIEW type-descriptor byte strings into a readable type.",
34
+ epilog=_EXAMPLES,
35
+ formatter_class=argparse.RawDescriptionHelpFormatter,
36
+ )
37
+ parser.add_argument("--version", action="version", version=f"lvtypedesc {__version__}")
38
+ sub = parser.add_subparsers(dest="command", required=True)
39
+
40
+ decode = sub.add_parser(
41
+ "decode",
42
+ help="decode a hex type descriptor into a readable type",
43
+ description="Decode a hex type descriptor. Whitespace in the hex is ignored.",
44
+ )
45
+ decode.add_argument("hex", help="the type descriptor as hex, for example 00040003")
46
+ decode.add_argument(
47
+ "--tree",
48
+ action="store_true",
49
+ help="print an indented tree instead of a one-line summary",
50
+ )
51
+
52
+ sub.add_parser(
53
+ "codes",
54
+ help="list every type code the library recognises",
55
+ description="List every recognised type code, with its name and category.",
56
+ )
57
+ return parser
58
+
59
+
60
+ def _run_decode(hex_text: str, tree: bool) -> int:
61
+ try:
62
+ data = bytes.fromhex(hex_text)
63
+ except ValueError as exc:
64
+ print(f"error: not valid hex: {exc}", file=sys.stderr)
65
+ return 1
66
+ try:
67
+ node = parse_type_descriptor(data)
68
+ except TypeDescError as exc:
69
+ print(f"error: {exc}", file=sys.stderr)
70
+ return 1
71
+ print(pretty(node) if tree else describe(node))
72
+ return 0
73
+
74
+
75
+ def _run_codes() -> int:
76
+ for code, name, category in known_codes():
77
+ print(f"0x{code:02X} {name:<10} {category.value}")
78
+ return 0
79
+
80
+
81
+ def main(argv: Sequence[str] | None = None) -> int:
82
+ """Entry point for the ``lvtypedesc`` console script."""
83
+ parser = _build_parser()
84
+ args = parser.parse_args(argv)
85
+ if args.command == "decode":
86
+ return _run_decode(args.hex, args.tree)
87
+ return _run_codes()
88
+
89
+
90
+ if __name__ == "__main__": # pragma: no cover
91
+ raise SystemExit(main())
lvtypedesc/_codes.py ADDED
@@ -0,0 +1,131 @@
1
+ """The type codes this library understands.
2
+
3
+ The names and values follow the community-documented LabVIEW type-descriptor
4
+ table (see the project README for provenance). These codes are not an official
5
+ NI byte specification and can change between LabVIEW versions, so they are kept
6
+ in one place rather than scattered through the parser.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from enum import Enum, IntEnum
12
+
13
+
14
+ class TypeCode(IntEnum):
15
+ """Every raw type code this library recognises, by name.
16
+
17
+ Member names are Python identifiers. The human label used in output comes
18
+ from the name tables below (for example ``BOOLEAN16`` is labelled
19
+ ``Boolean16``).
20
+ """
21
+
22
+ I8 = 0x01
23
+ I16 = 0x02
24
+ I32 = 0x03
25
+ I64 = 0x04
26
+ U8 = 0x05
27
+ U16 = 0x06
28
+ U32 = 0x07
29
+ U64 = 0x08
30
+ SGL = 0x09
31
+ DBL = 0x0A
32
+ EXT = 0x0B
33
+ CSG = 0x0C
34
+ CDB = 0x0D
35
+ CXT = 0x0E
36
+ BOOLEAN16 = 0x20
37
+ BOOLEAN = 0x21
38
+ STRING = 0x30
39
+ PATH = 0x32
40
+ PICTURE = 0x33
41
+ ARRAY = 0x40
42
+ CLUSTER = 0x50
43
+ FXP = 0x5F
44
+ REFNUM = 0x70
45
+
46
+
47
+ SCALAR_NAMES: dict[int, str] = {
48
+ 0x01: "I8",
49
+ 0x02: "I16",
50
+ 0x03: "I32",
51
+ 0x04: "I64",
52
+ 0x05: "U8",
53
+ 0x06: "U16",
54
+ 0x07: "U32",
55
+ 0x08: "U64",
56
+ 0x09: "SGL",
57
+ 0x0A: "DBL",
58
+ 0x0B: "EXT",
59
+ 0x0C: "CSG",
60
+ 0x0D: "CDB",
61
+ 0x0E: "CXT",
62
+ }
63
+
64
+ BOOLEAN_NAMES: dict[int, str] = {
65
+ 0x20: "Boolean16",
66
+ 0x21: "Boolean",
67
+ }
68
+
69
+ STRING_NAMES: dict[int, str] = {
70
+ 0x30: "String",
71
+ 0x32: "Path",
72
+ 0x33: "Picture",
73
+ }
74
+
75
+ ARRAY_CODE = 0x40
76
+ CLUSTER_CODE = 0x50
77
+ FXP_CODE = 0x5F
78
+ REFNUM_CODE = 0x70
79
+
80
+ # Recognised by LabVIEW but outside this library's scope. Their payloads are not
81
+ # documented well enough to parse honestly, so the parser refuses them with a
82
+ # named message instead of a bare "unknown code".
83
+ UNSUPPORTED_NAMES: dict[int, str] = {
84
+ 0x15: "EnumU8",
85
+ 0x16: "EnumU16",
86
+ 0x17: "EnumU32",
87
+ 0x53: "Variant",
88
+ 0x54: "Waveform",
89
+ }
90
+
91
+
92
+ class Category(Enum):
93
+ """The structural family a type code belongs to."""
94
+
95
+ SCALAR = "scalar"
96
+ BOOLEAN = "boolean"
97
+ STRING = "string"
98
+ ARRAY = "array"
99
+ CLUSTER = "cluster"
100
+ FXP = "fxp"
101
+ REFNUM = "refnum"
102
+
103
+
104
+ def classify(code: int) -> tuple[Category, str] | None:
105
+ """Map a type code to its category and name, or None if out of scope."""
106
+ if code in SCALAR_NAMES:
107
+ return Category.SCALAR, SCALAR_NAMES[code]
108
+ if code in BOOLEAN_NAMES:
109
+ return Category.BOOLEAN, BOOLEAN_NAMES[code]
110
+ if code in STRING_NAMES:
111
+ return Category.STRING, STRING_NAMES[code]
112
+ if code == ARRAY_CODE:
113
+ return Category.ARRAY, "Array"
114
+ if code == CLUSTER_CODE:
115
+ return Category.CLUSTER, "Cluster"
116
+ if code == FXP_CODE:
117
+ return Category.FXP, "FXP"
118
+ if code == REFNUM_CODE:
119
+ return Category.REFNUM, "Refnum"
120
+ return None
121
+
122
+
123
+ def known_codes() -> list[tuple[int, str, Category]]:
124
+ """Return every recognised ``(code, name, category)`` row, sorted by code."""
125
+ rows: list[tuple[int, str, Category]] = []
126
+ for code in range(0x100):
127
+ kind = classify(code)
128
+ if kind is not None:
129
+ category, name = kind
130
+ rows.append((code, name, category))
131
+ return rows
lvtypedesc/_emit.py ADDED
@@ -0,0 +1,59 @@
1
+ """Re-emit a typed tree as a canonical short-form type descriptor.
2
+
3
+ ``to_bytes`` is the inverse of :func:`parse_type_descriptor` for every node type
4
+ this library models. It writes the short-form descriptor (16-bit length) and
5
+ raises :class:`TypeDescError` when a tree is too large for that form, rather than
6
+ guessing the long-form layout.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from ._errors import TypeDescError
12
+ from ._model import (
13
+ VARIABLE,
14
+ Array,
15
+ Boolean,
16
+ Cluster,
17
+ FixedPoint,
18
+ LVString,
19
+ Refnum,
20
+ Scalar,
21
+ TypeDesc,
22
+ )
23
+
24
+ _HEADER = 4
25
+ _U32_VARIABLE = 0xFFFFFFFF
26
+ _MAX_LENGTH = 0xFFFF
27
+
28
+
29
+ def to_bytes(node: TypeDesc) -> bytes:
30
+ """Serialise ``node`` back into a canonical short-form type descriptor."""
31
+ body = _body(node)
32
+ length = _HEADER + len(body)
33
+ if length > _MAX_LENGTH:
34
+ raise TypeDescError(f"descriptor of {length} bytes is too large for the short form")
35
+ return _u16(length) + _u16(node.code & 0xFFFF) + body
36
+
37
+
38
+ def _body(node: TypeDesc) -> bytes:
39
+ if isinstance(node, (Scalar, Boolean)):
40
+ return b""
41
+ if isinstance(node, LVString):
42
+ size = _U32_VARIABLE if node.size == VARIABLE else node.size
43
+ return _u32(size)
44
+ if isinstance(node, Array):
45
+ dims = b"".join(_u32(_U32_VARIABLE if d == VARIABLE else d) for d in node.dimensions)
46
+ return _u16(node.rank) + dims + to_bytes(node.element)
47
+ if isinstance(node, Cluster):
48
+ return _u16(len(node.fields)) + b"".join(to_bytes(f) for f in node.fields)
49
+ if isinstance(node, (FixedPoint, Refnum)):
50
+ return node.payload
51
+ raise TypeDescError(f"cannot serialise node of type {type(node).__name__}")
52
+
53
+
54
+ def _u16(value: int) -> bytes:
55
+ return int(value).to_bytes(2, "big")
56
+
57
+
58
+ def _u32(value: int) -> bytes:
59
+ return int(value).to_bytes(4, "big")
lvtypedesc/_errors.py ADDED
@@ -0,0 +1,11 @@
1
+ """The single error type raised for malformed or unsupported input."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class TypeDescError(ValueError):
7
+ """Raised when a byte string is not a type descriptor this library can parse.
8
+
9
+ Covers truncated input, an impossible length word, and type codes that are
10
+ unknown or outside the supported subset.
11
+ """
lvtypedesc/_model.py ADDED
@@ -0,0 +1,96 @@
1
+ """The typed tree a parsed type descriptor becomes, and a readable rendering."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+ # A dimension or string whose size is not fixed in the descriptor. LabVIEW
8
+ # stores this as 0xFFFFFFFF; the actual size lives in the flattened data.
9
+ VARIABLE = -1
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class TypeDesc:
14
+ """Base node. ``code`` is the raw type code, ``name`` its documented label."""
15
+
16
+ code: int
17
+ name: str
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class Scalar(TypeDesc):
22
+ """A fixed-width numeric type, for example I32 or DBL."""
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class Boolean(TypeDesc):
27
+ """A LabVIEW boolean."""
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class LVString(TypeDesc):
32
+ """A variable-length string, path or picture.
33
+
34
+ ``size`` is the descriptor's 32-bit size word: ``VARIABLE`` when the size is
35
+ not fixed (``0xFFFFFFFF``), otherwise the fixed byte count.
36
+ """
37
+
38
+ size: int = VARIABLE
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class Array(TypeDesc):
43
+ """An n-dimensional array of one element type."""
44
+
45
+ element: TypeDesc
46
+ dimensions: tuple[int, ...] = field(default_factory=tuple)
47
+
48
+ @property
49
+ def rank(self) -> int:
50
+ """The number of dimensions."""
51
+ return len(self.dimensions)
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class Cluster(TypeDesc):
56
+ """An ordered group of named-by-position field types."""
57
+
58
+ fields: tuple[TypeDesc, ...] = field(default_factory=tuple)
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class FixedPoint(TypeDesc):
63
+ """A LabVIEW fixed-point number (FXP).
64
+
65
+ The word, delta and offset encoding inside an FXP descriptor is
66
+ version-specific and not fully documented, so the type-specific bytes are
67
+ preserved verbatim in ``payload`` rather than parsed into fields. This keeps
68
+ round-trip exact without inventing an interpretation.
69
+ """
70
+
71
+ payload: bytes = b""
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class Refnum(TypeDesc):
76
+ """A LabVIEW refnum (a handle to a queue, VISA resource, control, ...).
77
+
78
+ ``subtype`` is the first payload word, which identifies the refnum kind. Its
79
+ meaning is only partly documented, so it is surfaced best-effort and the full
80
+ type-specific bytes are kept in ``payload`` for exact round-trip.
81
+ """
82
+
83
+ subtype: int = 0
84
+ payload: bytes = b""
85
+
86
+
87
+ def describe(node: TypeDesc) -> str:
88
+ """Render a parsed descriptor as a compact, readable one-line string."""
89
+ if isinstance(node, Array):
90
+ return f"{node.rank}D Array of {describe(node.element)}"
91
+ if isinstance(node, Cluster):
92
+ inner = ", ".join(describe(f) for f in node.fields)
93
+ return f"Cluster {{ {inner} }}"
94
+ if isinstance(node, Refnum):
95
+ return f"Refnum(subtype=0x{node.subtype:02x})"
96
+ return node.name
lvtypedesc/_parser.py ADDED
@@ -0,0 +1,125 @@
1
+ """Recursive descent over a LabVIEW type descriptor byte string.
2
+
3
+ Layout of the common types, all big-endian:
4
+
5
+ - header: a 16-bit length (bytes in this descriptor, including the length) and a
6
+ 16-bit type-code word whose low byte is the type code.
7
+ - scalar, boolean: header only.
8
+ - string, path, picture: header plus a 32-bit size word (0xFFFFFFFF = variable).
9
+ - array: header, a 16-bit dimension count, one 32-bit size per dimension, then
10
+ the element descriptor.
11
+ - cluster: header, a 16-bit field count, then that many descriptors in order.
12
+ - fxp, refnum: header plus a type-specific payload kept as raw bytes, because its
13
+ internal layout is version-specific and not fully documented.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from ._codes import UNSUPPORTED_NAMES, Category, classify
19
+ from ._errors import TypeDescError
20
+ from ._model import (
21
+ VARIABLE,
22
+ Array,
23
+ Boolean,
24
+ Cluster,
25
+ FixedPoint,
26
+ LVString,
27
+ Refnum,
28
+ Scalar,
29
+ TypeDesc,
30
+ )
31
+
32
+ _HEADER = 4
33
+ _U32_VARIABLE = 0xFFFFFFFF
34
+
35
+
36
+ def parse_type_descriptor(data: bytes) -> TypeDesc:
37
+ """Parse the leading type descriptor in ``data`` into a typed tree.
38
+
39
+ Trailing bytes (such as the flattened data that follows a descriptor) are
40
+ ignored. Raises :class:`TypeDescError` on truncated or unsupported input.
41
+ """
42
+ if not isinstance(data, (bytes, bytearray)):
43
+ raise TypeDescError("expected bytes")
44
+ node, _ = _parse(bytes(data), 0)
45
+ return node
46
+
47
+
48
+ def _u16(data: bytes, offset: int) -> int:
49
+ if offset + 2 > len(data):
50
+ raise TypeDescError(f"truncated: need 2 bytes at offset {offset}")
51
+ return int.from_bytes(data[offset : offset + 2], "big")
52
+
53
+
54
+ def _u32(data: bytes, offset: int) -> int:
55
+ if offset + 4 > len(data):
56
+ raise TypeDescError(f"truncated: need 4 bytes at offset {offset}")
57
+ return int.from_bytes(data[offset : offset + 4], "big")
58
+
59
+
60
+ def _parse(data: bytes, offset: int) -> tuple[TypeDesc, int]:
61
+ length = _u16(data, offset)
62
+ code = _u16(data, offset + 2) & 0xFF
63
+ end = offset + length
64
+ if length < _HEADER:
65
+ raise TypeDescError(f"length {length} too small at offset {offset}")
66
+ if end > len(data):
67
+ raise TypeDescError(f"descriptor at offset {offset} runs past the input")
68
+
69
+ kind = classify(code)
70
+ if kind is None:
71
+ name = UNSUPPORTED_NAMES.get(code)
72
+ detail = f"unsupported type {name}" if name else "unknown type code"
73
+ raise TypeDescError(f"{detail} 0x{code:02x} at offset {offset}")
74
+
75
+ category, name = kind
76
+ if category is Category.SCALAR:
77
+ return Scalar(code, name), end
78
+ if category is Category.BOOLEAN:
79
+ return Boolean(code, name), end
80
+ if category is Category.STRING:
81
+ return _parse_string(data, offset, end, code, name), end
82
+ if category is Category.ARRAY:
83
+ return _parse_array(data, offset, code, name), end
84
+ if category is Category.CLUSTER:
85
+ return _parse_cluster(data, offset, code, name), end
86
+ if category is Category.FXP:
87
+ return FixedPoint(code, name, payload=data[offset + _HEADER : end]), end
88
+ return _parse_refnum(data, offset, end, code, name), end
89
+
90
+
91
+ def _parse_string(data: bytes, offset: int, end: int, code: int, name: str) -> LVString:
92
+ if end - offset >= _HEADER + 4:
93
+ raw = _u32(data, offset + _HEADER)
94
+ size = VARIABLE if raw == _U32_VARIABLE else raw
95
+ else:
96
+ size = VARIABLE
97
+ return LVString(code, name, size=size)
98
+
99
+
100
+ def _parse_array(data: bytes, offset: int, code: int, name: str) -> Array:
101
+ rank = _u16(data, offset + _HEADER)
102
+ pos = offset + _HEADER + 2
103
+ dims: list[int] = []
104
+ for _ in range(rank):
105
+ size = _u32(data, pos)
106
+ dims.append(VARIABLE if size == _U32_VARIABLE else size)
107
+ pos += 4
108
+ element, _ = _parse(data, pos)
109
+ return Array(code, name, element=element, dimensions=tuple(dims))
110
+
111
+
112
+ def _parse_cluster(data: bytes, offset: int, code: int, name: str) -> Cluster:
113
+ count = _u16(data, offset + _HEADER)
114
+ pos = offset + _HEADER + 2
115
+ fields: list[TypeDesc] = []
116
+ for _ in range(count):
117
+ child, pos = _parse(data, pos)
118
+ fields.append(child)
119
+ return Cluster(code, name, fields=tuple(fields))
120
+
121
+
122
+ def _parse_refnum(data: bytes, offset: int, end: int, code: int, name: str) -> Refnum:
123
+ payload = data[offset + _HEADER : end]
124
+ subtype = _u16(data, offset + _HEADER) if end - offset >= _HEADER + 2 else 0
125
+ return Refnum(code, name, subtype=subtype, payload=payload)
lvtypedesc/_pretty.py ADDED
@@ -0,0 +1,54 @@
1
+ """Render a parsed type descriptor as an indented, multi-line tree.
2
+
3
+ This complements the one-line :func:`describe`. It is meant for reading a nested
4
+ descriptor at a glance, so it spells out ranks, dimensions and field positions.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from ._model import (
10
+ VARIABLE,
11
+ Array,
12
+ Cluster,
13
+ FixedPoint,
14
+ LVString,
15
+ Refnum,
16
+ TypeDesc,
17
+ )
18
+
19
+ _STEP = " "
20
+
21
+
22
+ def pretty(node: TypeDesc) -> str:
23
+ """Return a multi-line, indented rendering of ``node`` and its children."""
24
+ return "\n".join(_lines(node, 0))
25
+
26
+
27
+ def _dim(value: int) -> str:
28
+ return "variable" if value == VARIABLE else str(value)
29
+
30
+
31
+ def _lines(node: TypeDesc, depth: int) -> list[str]:
32
+ pad = _STEP * depth
33
+ if isinstance(node, Array):
34
+ dims = ", ".join(_dim(d) for d in node.dimensions)
35
+ head = f"{pad}Array (rank {node.rank}, dims [{dims}])"
36
+ return [head, *_lines(node.element, depth + 1)]
37
+ if isinstance(node, Cluster):
38
+ head = f"{pad}Cluster ({len(node.fields)} fields)"
39
+ out = [head]
40
+ for index, child in enumerate(node.fields):
41
+ label = f"{_STEP * (depth + 1)}[{index}]"
42
+ child_lines = _lines(child, depth + 2)
43
+ first = child_lines[0].lstrip()
44
+ out.append(f"{label} {first}")
45
+ out.extend(child_lines[1:])
46
+ return out
47
+ if isinstance(node, LVString):
48
+ size = "variable" if node.size == VARIABLE else f"{node.size} bytes"
49
+ return [f"{pad}{node.name} (size {size})"]
50
+ if isinstance(node, Refnum):
51
+ return [f"{pad}Refnum (subtype 0x{node.subtype:02x})"]
52
+ if isinstance(node, FixedPoint):
53
+ return [f"{pad}FXP ({len(node.payload)}-byte payload)"]
54
+ return [f"{pad}{node.name}"]
lvtypedesc/py.typed ADDED
File without changes
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.5
2
+ Name: lvtypedesc
3
+ Version: 0.2.0
4
+ Summary: Parse LabVIEW type-descriptor byte strings into a readable, typed schema
5
+ Project-URL: Homepage, https://lianes8studio.com
6
+ Project-URL: Repository, https://github.com/JaviLianes8/lvtypedesc
7
+ Project-URL: Issues, https://github.com/JaviLianes8/lvtypedesc/issues
8
+ Project-URL: Changelog, https://github.com/JaviLianes8/lvtypedesc/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
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.10
19
+ Provides-Extra: dev
20
+ Requires-Dist: build; extra == 'dev'
21
+ Requires-Dist: mypy; extra == 'dev'
22
+ Requires-Dist: pytest; extra == 'dev'
23
+ Requires-Dist: ruff; extra == 'dev'
24
+ Requires-Dist: twine; extra == 'dev'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # lvtypedesc
28
+
29
+ Parse LabVIEW type-descriptor byte strings into a readable, typed schema
30
+
31
+ ## Install
32
+
33
+ ```
34
+ pip install lvtypedesc
35
+ ```
36
+
37
+ A LabVIEW type descriptor is the small, length-prefixed block of type codes that
38
+ precedes flattened LabVIEW data. This library turns that block into a typed tree
39
+ you can inspect, which is the schema a generic unflatten needs.
40
+
41
+ ## Usage
42
+
43
+ ```python
44
+ from lvtypedesc import parse_type_descriptor, describe, pretty, to_bytes
45
+
46
+ # A 1D array of clusters, each holding an I32 and a string.
47
+ data = bytes.fromhex(
48
+ "001C 0040 0001 FFFFFFFF 0012 0050 0002 0004 0003 0008 0030 FFFFFFFF".replace(" ", "")
49
+ )
50
+
51
+ node = parse_type_descriptor(data)
52
+ print(describe(node)) # 1D Array of Cluster { I32, String }
53
+ print(node.rank) # 1
54
+ print(node.element.fields[0].name) # I32
55
+ print(to_bytes(node) == data) # True, round-trips exactly
56
+ print(pretty(node))
57
+ # Array (rank 1, dims [variable])
58
+ # Cluster (2 fields)
59
+ # [0] I32
60
+ # [1] String (size variable)
61
+ ```
62
+
63
+ The tree is built from frozen dataclasses: `Scalar`, `Boolean`, `LVString` (with
64
+ `size`), `Array` (with `element` and `dimensions`), `Cluster` (with `fields`),
65
+ `FixedPoint` and `Refnum`. A dimension or string whose size is not fixed in the
66
+ descriptor uses the sentinel `VARIABLE`. Trailing bytes, such as the flattened
67
+ data after the descriptor, are ignored. Truncated, unknown or out-of-scope input
68
+ raises `TypeDescError`.
69
+
70
+ `to_bytes` re-emits the canonical short-form descriptor for any node the library
71
+ models, so `parse_type_descriptor` and `to_bytes` are inverses for the supported
72
+ subset. `TypeCode` exposes the recognised codes by name and `known_codes()`
73
+ returns the full table.
74
+
75
+ ## Command line
76
+
77
+ Installing the package also installs a `lvtypedesc` console script:
78
+
79
+ ```
80
+ $ lvtypedesc decode 00040003
81
+ I32
82
+ $ lvtypedesc decode 000E00400001FFFFFFFF0004000A
83
+ 1D Array of DBL
84
+ $ lvtypedesc decode --tree 000E00500002000400030004000A
85
+ Cluster (2 fields)
86
+ [0] I32
87
+ [1] DBL
88
+ $ lvtypedesc codes # list every recognised type code
89
+ $ lvtypedesc --version
90
+ ```
91
+
92
+ Whitespace in the hex is ignored. Errors go to stderr with exit code 1. Run
93
+ `lvtypedesc --help` for the full list of examples.
94
+
95
+ ## Supported subset
96
+
97
+ Fully parsed: the numeric scalars (`I8` to `U64`, `SGL`, `DBL`, `EXT` and the
98
+ complex variants), booleans, strings, paths, pictures, n-dimensional arrays and
99
+ clusters, nested to any depth. Fixed-point (`FXP`) and refnum descriptors are
100
+ recognised and their type-specific bytes preserved, but their internal layout is
101
+ not interpreted (see provenance below).
102
+
103
+ Not covered, and reported with a clear error: enums, variants and waveforms,
104
+ whose payloads are not documented well enough to parse honestly.
105
+
106
+ ## Provenance (please read)
107
+
108
+ The type-descriptor code table used here is **community reverse-engineering**,
109
+ documented at the [LabVIEW wiki](https://labviewwiki.org/wiki/Type_descriptor).
110
+ It is consistent with the fragments NI has published but it is **not** an
111
+ official, byte-exact NI specification. Two further caveats:
112
+
113
+ - Codes can change between LabVIEW versions, so treat them as the common
114
+ documented set rather than a guarantee for every build.
115
+ - The in-memory 16-bit-word form and the flattened byte form can differ. This
116
+ library parses the flattened byte form.
117
+
118
+ Because of this, anything that cannot be verified is treated as provisional:
119
+ unknown codes raise an honest error instead of a guess, and the fixed-point and
120
+ refnum payloads are kept as raw bytes rather than invented into fields. The
121
+ refnum `subtype` word is surfaced best-effort and is itself provisional.
122
+
123
+ ## Why
124
+
125
+ LabVIEW ships flattened data with a leading type descriptor, and decoding it in
126
+ Python needs that descriptor parsed into a schema first. OpenG's Variant Data
127
+ Library parses descriptors inside LabVIEW, but it is LabVIEW VIs, not Python. The
128
+ existing PyPI packages read LabVIEW source files instead: `pylabview` and `lvkit`
129
+ parse the `.vi` binary, `pylavi` validates VI files. None of them exposes a parser
130
+ for the raw runtime type-descriptor byte string that precedes flattened data or
131
+ sits inside a variant. This library fills that gap with a zero-dependency, fully
132
+ typed reader for the common documented types.
133
+
134
+ ## License
135
+
136
+ MIT. See [LICENSE](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,14 @@
1
+ lvtypedesc/__init__.py,sha256=qWMcxYKoxepoPwbrYag-v30l7xmObb-Ic0SYWWQ7PEE,782
2
+ lvtypedesc/_cli.py,sha256=vTPtSt8g2WKzwf4YMFKKpq5VpsTxnQ19eVDHOqMxfM8,2919
3
+ lvtypedesc/_codes.py,sha256=6M72TfbblERaL0XAMKfFw7UmuicOpBzbQSFWUozbR_4,3186
4
+ lvtypedesc/_emit.py,sha256=MIHK0Lg1HXBL4QjXcy_ltQ2mk8thwqjsyaoA6LODJL8,1815
5
+ lvtypedesc/_errors.py,sha256=yFS7B0CyC7wIUHQ4B_L3cORJFMqKhW0LDmqyX5x06HA,357
6
+ lvtypedesc/_model.py,sha256=szDUs52GuCazK6BozpHHuJcaPHc6ghvkTe8PMMTMvRw,2723
7
+ lvtypedesc/_parser.py,sha256=NEpe39QQwnBZGWI2uFptBLeBAJ8f3Lbs5q1no9QN3cw,4459
8
+ lvtypedesc/_pretty.py,sha256=40EmR7CpMeWAIiYcxoocRHGa_8M4RhxnSJ7H_plxS5k,1747
9
+ lvtypedesc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ lvtypedesc-0.2.0.dist-info/METADATA,sha256=nztOqEr8I3KyUIILg5CA-IRoJKJJLhHaV1q0yp6Q0lE,5433
11
+ lvtypedesc-0.2.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
12
+ lvtypedesc-0.2.0.dist-info/entry_points.txt,sha256=cFKuuV3WevGGmQy_2KFN801j22pVqEwTkiLsWJhb0uk,52
13
+ lvtypedesc-0.2.0.dist-info/licenses/LICENSE,sha256=wirD5nCA2aQa5UusK_1YHLBogI3CGcw23xAnulp3L_U,1091
14
+ lvtypedesc-0.2.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
+ lvtypedesc = lvtypedesc._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.