lvtypedesc 0.2.0__tar.gz
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-0.2.0/.gitattributes +1 -0
- lvtypedesc-0.2.0/.github/FUNDING.yml +1 -0
- lvtypedesc-0.2.0/.gitignore +12 -0
- lvtypedesc-0.2.0/CHANGELOG.md +35 -0
- lvtypedesc-0.2.0/CONTRIBUTING.md +19 -0
- lvtypedesc-0.2.0/IMPROVEMENTS-round.md +63 -0
- lvtypedesc-0.2.0/LICENSE +21 -0
- lvtypedesc-0.2.0/PKG-INFO +142 -0
- lvtypedesc-0.2.0/README.md +116 -0
- lvtypedesc-0.2.0/pyproject.toml +54 -0
- lvtypedesc-0.2.0/requirements-dev.txt +3 -0
- lvtypedesc-0.2.0/scripts/check.ps1 +16 -0
- lvtypedesc-0.2.0/scripts/release.ps1 +28 -0
- lvtypedesc-0.2.0/src/lvtypedesc/__init__.py +43 -0
- lvtypedesc-0.2.0/src/lvtypedesc/_cli.py +91 -0
- lvtypedesc-0.2.0/src/lvtypedesc/_codes.py +131 -0
- lvtypedesc-0.2.0/src/lvtypedesc/_emit.py +59 -0
- lvtypedesc-0.2.0/src/lvtypedesc/_errors.py +11 -0
- lvtypedesc-0.2.0/src/lvtypedesc/_model.py +96 -0
- lvtypedesc-0.2.0/src/lvtypedesc/_parser.py +125 -0
- lvtypedesc-0.2.0/src/lvtypedesc/_pretty.py +54 -0
- lvtypedesc-0.2.0/src/lvtypedesc/py.typed +0 -0
- lvtypedesc-0.2.0/tests/test_cli.py +49 -0
- lvtypedesc-0.2.0/tests/test_contract.py +84 -0
- lvtypedesc-0.2.0/tests/test_e2e.py +83 -0
- lvtypedesc-0.2.0/tests/test_parse.py +128 -0
- lvtypedesc-0.2.0/tests/test_pretty.py +25 -0
- lvtypedesc-0.2.0/tests/test_roundtrip.py +55 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* text=auto eol=lf
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
buy_me_a_coffee: jlianesglrs
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.2.0]
|
|
10
|
+
|
|
11
|
+
- Command-line interface: a `lvtypedesc` console script with `decode` (optional
|
|
12
|
+
`--tree`), `codes`, `--version` and runnable `--help` examples. Errors go to
|
|
13
|
+
stderr with exit code 1.
|
|
14
|
+
- `to_bytes` re-emits the canonical short-form descriptor for every modelled
|
|
15
|
+
node, so parsing and emission are inverses for the supported subset.
|
|
16
|
+
- New public API: `TypeCode` enum, `known_codes()`, `pretty()`, `to_bytes()`,
|
|
17
|
+
and the `FixedPoint` and `Refnum` node types.
|
|
18
|
+
- Fixed-point (`FXP`) and refnum descriptors are now recognised. Their
|
|
19
|
+
type-specific bytes are preserved verbatim rather than interpreted, because
|
|
20
|
+
their internal layout is version-specific and not fully documented.
|
|
21
|
+
- `LVString` now carries the descriptor's 32-bit `size` word (`VARIABLE` when not
|
|
22
|
+
fixed), so fixed-size strings round-trip exactly.
|
|
23
|
+
- README now states the provenance plainly: the code table is community
|
|
24
|
+
reverse-engineering from the LabVIEW wiki, not an official NI byte spec.
|
|
25
|
+
|
|
26
|
+
## [0.1.0]
|
|
27
|
+
|
|
28
|
+
- First release.
|
|
29
|
+
- `parse_type_descriptor` reads a LabVIEW type-descriptor byte string into a
|
|
30
|
+
typed tree of frozen dataclasses, and `describe` renders it as a readable line.
|
|
31
|
+
- Supported subset: numeric scalars (I8 to U64, SGL, DBL, EXT, complex),
|
|
32
|
+
booleans, strings, paths, pictures, n-dimensional arrays and nested clusters.
|
|
33
|
+
- Out-of-scope codes (enums, variants, waveforms, refnums) and truncated input
|
|
34
|
+
raise `TypeDescError`. Fills the gap left by VI-file parsers, which do not
|
|
35
|
+
decode the runtime type-descriptor byte string.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for taking the time. This project keeps a small, predictable shape.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
pip install -r requirements-dev.txt
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Before you open a pull request
|
|
12
|
+
|
|
13
|
+
- Keep public API in `src/lvtypedesc/__init__.py`; internals live in `_`-prefixed modules.
|
|
14
|
+
- Add a test for the behaviour you change. Tests inject time, disk and network; they
|
|
15
|
+
never touch the real thing.
|
|
16
|
+
- Run `scripts/check.ps1` (format, lint, types, tests) and make sure it passes.
|
|
17
|
+
- One logical change per commit, imperative subject line.
|
|
18
|
+
|
|
19
|
+
Open an issue first if the change is large or shifts the public API.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Improvements round
|
|
2
|
+
|
|
3
|
+
Ranked, substantial improvements for this round. Each is additive: the existing
|
|
4
|
+
`parse_type_descriptor` / `describe` API keeps working unchanged.
|
|
5
|
+
|
|
6
|
+
## Provenance note (read first)
|
|
7
|
+
|
|
8
|
+
The type-descriptor code table this library uses is the community
|
|
9
|
+
reverse-engineering published at the LabVIEW wiki
|
|
10
|
+
(https://labviewwiki.org/wiki/Type_descriptor). It is consistent with the
|
|
11
|
+
scattered fragments NI has published, but it is **not** an official, byte-exact
|
|
12
|
+
NI specification. Codes can also differ between LabVIEW versions and between the
|
|
13
|
+
in-memory 16-bit-word form and the flattened byte form. So every code and layout
|
|
14
|
+
below is treated as the common documented set, not a guarantee, and the parser
|
|
15
|
+
reports honest errors rather than guessing. Where a payload cannot be verified
|
|
16
|
+
(fixed-point and refnum internals), the raw bytes are preserved instead of being
|
|
17
|
+
invented into fields.
|
|
18
|
+
|
|
19
|
+
## Ranked improvements
|
|
20
|
+
|
|
21
|
+
1. **Command-line interface.** A real `lvtypedesc` console script (argparse,
|
|
22
|
+
`[project.scripts]`): `decode` a hex descriptor into a readable type, a
|
|
23
|
+
`codes` listing of every code the library knows, `--version`, and a `--help`
|
|
24
|
+
with runnable examples. Errors go to stderr with exit code 1. This is the
|
|
25
|
+
single biggest reach improvement: it makes the library usable without writing
|
|
26
|
+
Python.
|
|
27
|
+
|
|
28
|
+
2. **Expose the type-code enum.** A public `TypeCode` `IntEnum` so callers can
|
|
29
|
+
refer to `TypeCode.DBL` instead of a bare `0x0A`, plus `known_codes()` that
|
|
30
|
+
returns the full `(code, name, category)` table that backs the CLI listing.
|
|
31
|
+
|
|
32
|
+
3. **Round-trip emission.** `to_bytes(node)` re-emits the canonical descriptor
|
|
33
|
+
for every node type the library models, so `parse` and `to_bytes` are
|
|
34
|
+
inverses for the supported subset. This is round-trip "where defined": it
|
|
35
|
+
emits the short-form descriptor and refuses (honest error) when the length
|
|
36
|
+
would overflow the 16-bit short-form length word.
|
|
37
|
+
|
|
38
|
+
4. **Recognise fixed-point and refnum.** FXP (`0x5F`) and Refnum (`0x70`) move
|
|
39
|
+
from "unsupported, raises" to recognised nodes. Because their internal
|
|
40
|
+
payload layout is version-specific and not fully verifiable, the parser keeps
|
|
41
|
+
the raw payload bytes rather than inventing fields. The refnum subtype word is
|
|
42
|
+
surfaced as a best-effort `subtype` and documented as provisional.
|
|
43
|
+
|
|
44
|
+
5. **Faithful string sizing.** `LVString` now carries the descriptor's 32-bit
|
|
45
|
+
size word (`VARIABLE` for `0xFFFFFFFF`), so fixed-size strings, paths and
|
|
46
|
+
pictures round-trip exactly and the size is inspectable.
|
|
47
|
+
|
|
48
|
+
6. **Multi-line pretty-print.** `pretty(node)` renders a parsed descriptor as an
|
|
49
|
+
indented tree, complementing the existing one-line `describe`. The CLI uses it
|
|
50
|
+
for `decode --tree`.
|
|
51
|
+
|
|
52
|
+
7. **Stronger tests.** A contract test that parses representative descriptors for
|
|
53
|
+
every structural family (scalar, string, array of DBL, cluster) and asserts
|
|
54
|
+
the whole typed tree; round-trip property tests; a CLI unit test; and a real
|
|
55
|
+
end-to-end test that pip-installs the package editable and drives the actual
|
|
56
|
+
installed `lvtypedesc` console script through a subprocess.
|
|
57
|
+
|
|
58
|
+
## Deliberately out of scope
|
|
59
|
+
|
|
60
|
+
Enums, variants and waveforms stay unsupported with a clear error: their payloads
|
|
61
|
+
are not documented well enough to parse honestly. No codes are invented. The
|
|
62
|
+
long-form (32-bit length) descriptor variant is not emitted; `to_bytes` reports
|
|
63
|
+
when a tree is too large for the short form instead of guessing the long form.
|
lvtypedesc-0.2.0/LICENSE
ADDED
|
@@ -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.
|
|
@@ -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,116 @@
|
|
|
1
|
+
# lvtypedesc
|
|
2
|
+
|
|
3
|
+
Parse LabVIEW type-descriptor byte strings into a readable, typed schema
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
pip install lvtypedesc
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
A LabVIEW type descriptor is the small, length-prefixed block of type codes that
|
|
12
|
+
precedes flattened LabVIEW data. This library turns that block into a typed tree
|
|
13
|
+
you can inspect, which is the schema a generic unflatten needs.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from lvtypedesc import parse_type_descriptor, describe, pretty, to_bytes
|
|
19
|
+
|
|
20
|
+
# A 1D array of clusters, each holding an I32 and a string.
|
|
21
|
+
data = bytes.fromhex(
|
|
22
|
+
"001C 0040 0001 FFFFFFFF 0012 0050 0002 0004 0003 0008 0030 FFFFFFFF".replace(" ", "")
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
node = parse_type_descriptor(data)
|
|
26
|
+
print(describe(node)) # 1D Array of Cluster { I32, String }
|
|
27
|
+
print(node.rank) # 1
|
|
28
|
+
print(node.element.fields[0].name) # I32
|
|
29
|
+
print(to_bytes(node) == data) # True, round-trips exactly
|
|
30
|
+
print(pretty(node))
|
|
31
|
+
# Array (rank 1, dims [variable])
|
|
32
|
+
# Cluster (2 fields)
|
|
33
|
+
# [0] I32
|
|
34
|
+
# [1] String (size variable)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The tree is built from frozen dataclasses: `Scalar`, `Boolean`, `LVString` (with
|
|
38
|
+
`size`), `Array` (with `element` and `dimensions`), `Cluster` (with `fields`),
|
|
39
|
+
`FixedPoint` and `Refnum`. A dimension or string whose size is not fixed in the
|
|
40
|
+
descriptor uses the sentinel `VARIABLE`. Trailing bytes, such as the flattened
|
|
41
|
+
data after the descriptor, are ignored. Truncated, unknown or out-of-scope input
|
|
42
|
+
raises `TypeDescError`.
|
|
43
|
+
|
|
44
|
+
`to_bytes` re-emits the canonical short-form descriptor for any node the library
|
|
45
|
+
models, so `parse_type_descriptor` and `to_bytes` are inverses for the supported
|
|
46
|
+
subset. `TypeCode` exposes the recognised codes by name and `known_codes()`
|
|
47
|
+
returns the full table.
|
|
48
|
+
|
|
49
|
+
## Command line
|
|
50
|
+
|
|
51
|
+
Installing the package also installs a `lvtypedesc` console script:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
$ lvtypedesc decode 00040003
|
|
55
|
+
I32
|
|
56
|
+
$ lvtypedesc decode 000E00400001FFFFFFFF0004000A
|
|
57
|
+
1D Array of DBL
|
|
58
|
+
$ lvtypedesc decode --tree 000E00500002000400030004000A
|
|
59
|
+
Cluster (2 fields)
|
|
60
|
+
[0] I32
|
|
61
|
+
[1] DBL
|
|
62
|
+
$ lvtypedesc codes # list every recognised type code
|
|
63
|
+
$ lvtypedesc --version
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Whitespace in the hex is ignored. Errors go to stderr with exit code 1. Run
|
|
67
|
+
`lvtypedesc --help` for the full list of examples.
|
|
68
|
+
|
|
69
|
+
## Supported subset
|
|
70
|
+
|
|
71
|
+
Fully parsed: the numeric scalars (`I8` to `U64`, `SGL`, `DBL`, `EXT` and the
|
|
72
|
+
complex variants), booleans, strings, paths, pictures, n-dimensional arrays and
|
|
73
|
+
clusters, nested to any depth. Fixed-point (`FXP`) and refnum descriptors are
|
|
74
|
+
recognised and their type-specific bytes preserved, but their internal layout is
|
|
75
|
+
not interpreted (see provenance below).
|
|
76
|
+
|
|
77
|
+
Not covered, and reported with a clear error: enums, variants and waveforms,
|
|
78
|
+
whose payloads are not documented well enough to parse honestly.
|
|
79
|
+
|
|
80
|
+
## Provenance (please read)
|
|
81
|
+
|
|
82
|
+
The type-descriptor code table used here is **community reverse-engineering**,
|
|
83
|
+
documented at the [LabVIEW wiki](https://labviewwiki.org/wiki/Type_descriptor).
|
|
84
|
+
It is consistent with the fragments NI has published but it is **not** an
|
|
85
|
+
official, byte-exact NI specification. Two further caveats:
|
|
86
|
+
|
|
87
|
+
- Codes can change between LabVIEW versions, so treat them as the common
|
|
88
|
+
documented set rather than a guarantee for every build.
|
|
89
|
+
- The in-memory 16-bit-word form and the flattened byte form can differ. This
|
|
90
|
+
library parses the flattened byte form.
|
|
91
|
+
|
|
92
|
+
Because of this, anything that cannot be verified is treated as provisional:
|
|
93
|
+
unknown codes raise an honest error instead of a guess, and the fixed-point and
|
|
94
|
+
refnum payloads are kept as raw bytes rather than invented into fields. The
|
|
95
|
+
refnum `subtype` word is surfaced best-effort and is itself provisional.
|
|
96
|
+
|
|
97
|
+
## Why
|
|
98
|
+
|
|
99
|
+
LabVIEW ships flattened data with a leading type descriptor, and decoding it in
|
|
100
|
+
Python needs that descriptor parsed into a schema first. OpenG's Variant Data
|
|
101
|
+
Library parses descriptors inside LabVIEW, but it is LabVIEW VIs, not Python. The
|
|
102
|
+
existing PyPI packages read LabVIEW source files instead: `pylabview` and `lvkit`
|
|
103
|
+
parse the `.vi` binary, `pylavi` validates VI files. None of them exposes a parser
|
|
104
|
+
for the raw runtime type-descriptor byte string that precedes flattened data or
|
|
105
|
+
sits inside a variant. This library fills that gap with a zero-dependency, fully
|
|
106
|
+
typed reader for the common documented types.
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT. See [LICENSE](LICENSE).
|
|
111
|
+
|
|
112
|
+
## Links
|
|
113
|
+
|
|
114
|
+
- Website: https://lianes8studio.com
|
|
115
|
+
- GitHub: https://github.com/JaviLianes8
|
|
116
|
+
- LinkedIn: https://www.linkedin.com/in/jlianes/
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "lvtypedesc"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Parse LabVIEW type-descriptor byte strings into a readable, typed schema"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Javier Lianes", email = "javier@lianes8studio.com" }]
|
|
14
|
+
keywords = []
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
]
|
|
21
|
+
dependencies = []
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = ["ruff", "mypy", "pytest", "build", "twine"]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
lvtypedesc = "lvtypedesc._cli:main"
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://lianes8studio.com"
|
|
31
|
+
Repository = "https://github.com/JaviLianes8/lvtypedesc"
|
|
32
|
+
Issues = "https://github.com/JaviLianes8/lvtypedesc/issues"
|
|
33
|
+
Changelog = "https://github.com/JaviLianes8/lvtypedesc/blob/main/CHANGELOG.md"
|
|
34
|
+
LinkedIn = "https://www.linkedin.com/in/jlianes/"
|
|
35
|
+
Funding = "https://buymeacoffee.com/jlianesglrs"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/lvtypedesc"]
|
|
39
|
+
|
|
40
|
+
[tool.ruff]
|
|
41
|
+
line-length = 100
|
|
42
|
+
target-version = "py310"
|
|
43
|
+
|
|
44
|
+
[tool.ruff.lint]
|
|
45
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
|
|
46
|
+
|
|
47
|
+
[tool.mypy]
|
|
48
|
+
python_version = "3.10"
|
|
49
|
+
strict = true
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
testpaths = ["tests"]
|
|
53
|
+
pythonpath = ["src"]
|
|
54
|
+
addopts = "-q"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Local quality gate. Run before every commit. No CI runs in the cloud.
|
|
2
|
+
# $ErrorActionPreference does NOT stop on a native command's non-zero exit, so each tool's
|
|
3
|
+
# exit code is checked explicitly: a red gate must fail, never pass silently.
|
|
4
|
+
$ErrorActionPreference = "Stop"
|
|
5
|
+
|
|
6
|
+
function Invoke-Step($label, [scriptblock]$cmd) {
|
|
7
|
+
Write-Host "== $label =="
|
|
8
|
+
& $cmd
|
|
9
|
+
if ($LASTEXITCODE -ne 0) { Write-Host "FAILED: $label (exit $LASTEXITCODE)"; exit $LASTEXITCODE }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
Invoke-Step "ruff format" { ruff format --check src tests }
|
|
13
|
+
Invoke-Step "ruff lint" { ruff check src tests }
|
|
14
|
+
Invoke-Step "mypy" { mypy src tests }
|
|
15
|
+
Invoke-Step "pytest" { pytest }
|
|
16
|
+
Write-Host "gate green"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Build and upload to PyPI. Run scripts/check.ps1 first.
|
|
2
|
+
# The name must not already exist on PyPI; the token is read from a secret store, never git.
|
|
3
|
+
$ErrorActionPreference = "Stop"
|
|
4
|
+
|
|
5
|
+
# Works on both Windows PowerShell 5.1 and pwsh 7 (generic catch, no PS7-only types).
|
|
6
|
+
$taken = $true
|
|
7
|
+
try {
|
|
8
|
+
Invoke-WebRequest -UseBasicParsing "https://pypi.org/pypi/lvtypedesc/json" | Out-Null
|
|
9
|
+
} catch {
|
|
10
|
+
if ($_.Exception.Response.StatusCode.value__ -eq 404) { $taken = $false } else { throw }
|
|
11
|
+
}
|
|
12
|
+
if ($taken) { throw "The name 'lvtypedesc' already exists on PyPI. Choose another before publishing." }
|
|
13
|
+
|
|
14
|
+
$token = $env:PYPI_TOKEN
|
|
15
|
+
if (-not $token -and (Test-Path "C:\secrets\pypi-token.txt")) {
|
|
16
|
+
$token = (Get-Content "C:\secrets\pypi-token.txt" -Raw).Trim()
|
|
17
|
+
}
|
|
18
|
+
if (-not $token) {
|
|
19
|
+
throw "No PyPI token. Set `$env:PYPI_TOKEN or save it to C:\secrets\pypi-token.txt (never commit it)."
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
Remove-Item -Recurse -Force dist -ErrorAction SilentlyContinue
|
|
23
|
+
python -m build
|
|
24
|
+
python -m twine check dist/*
|
|
25
|
+
|
|
26
|
+
$env:TWINE_USERNAME = "__token__"
|
|
27
|
+
$env:TWINE_PASSWORD = $token
|
|
28
|
+
try { python -m twine upload dist/* } finally { Remove-Item Env:\TWINE_PASSWORD -ErrorAction SilentlyContinue }
|
|
@@ -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
|
+
]
|
|
@@ -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())
|