lvpath 0.3.1__tar.gz → 1.0.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.
Files changed (51) hide show
  1. {lvpath-0.3.1 → lvpath-1.0.0}/.gitignore +2 -0
  2. lvpath-1.0.0/CHANGELOG.md +140 -0
  3. {lvpath-0.3.1 → lvpath-1.0.0}/CONTRIBUTING.md +19 -19
  4. lvpath-1.0.0/GUIDE.md +290 -0
  5. {lvpath-0.3.1 → lvpath-1.0.0}/LICENSE +21 -21
  6. lvpath-1.0.0/PKG-INFO +147 -0
  7. lvpath-1.0.0/README.md +110 -0
  8. {lvpath-0.3.1 → lvpath-1.0.0}/pyproject.toml +16 -5
  9. {lvpath-0.3.1 → lvpath-1.0.0}/src/lvpath/__init__.py +9 -9
  10. lvpath-1.0.0/src/lvpath/__main__.py +10 -0
  11. lvpath-1.0.0/src/lvpath/_cli.py +177 -0
  12. lvpath-1.0.0/src/lvpath/_coerce.py +34 -0
  13. lvpath-1.0.0/src/lvpath/_convert.py +86 -0
  14. lvpath-1.0.0/src/lvpath/_fields.py +71 -0
  15. lvpath-1.0.0/src/lvpath/_flatbridge.py +125 -0
  16. lvpath-1.0.0/src/lvpath/_flatten.py +173 -0
  17. {lvpath-0.3.1 → lvpath-1.0.0}/src/lvpath/_kinds.py +20 -1
  18. {lvpath-0.3.1 → lvpath-1.0.0}/src/lvpath/_platform.py +26 -6
  19. {lvpath-0.3.1 → lvpath-1.0.0}/src/lvpath/_portable.py +56 -31
  20. lvpath-1.0.0/src/lvpath/_text.py +29 -0
  21. lvpath-1.0.0/tests/conftest.py +59 -0
  22. lvpath-1.0.0/tests/fixtures/labview2026_flat.json +98 -0
  23. lvpath-1.0.0/tests/fixtures/sources/MANIFEST.json +13 -0
  24. {lvpath-0.3.1 → lvpath-1.0.0}/tests/test_cli.py +13 -3
  25. {lvpath-0.3.1 → lvpath-1.0.0}/tests/test_cli_e2e.py +12 -24
  26. lvpath-1.0.0/tests/test_cli_e2e_more.py +60 -0
  27. lvpath-1.0.0/tests/test_contract_one.py +156 -0
  28. lvpath-1.0.0/tests/test_contract_review.py +50 -0
  29. lvpath-1.0.0/tests/test_contract_strength.py +63 -0
  30. lvpath-1.0.0/tests/test_contract_three.py +100 -0
  31. lvpath-1.0.0/tests/test_contract_two.py +140 -0
  32. lvpath-1.0.0/tests/test_docs.py +114 -0
  33. {lvpath-0.3.1 → lvpath-1.0.0}/tests/test_flatten.py +1 -1
  34. lvpath-1.0.0/tests/test_fuzz.py +134 -0
  35. lvpath-1.0.0/tests/test_packaging.py +64 -0
  36. lvpath-1.0.0/tests/test_real_labview_flat.py +149 -0
  37. lvpath-1.0.0/tests/test_sources_truth.py +60 -0
  38. lvpath-0.3.1/CHANGELOG.md +0 -58
  39. lvpath-0.3.1/PKG-INFO +0 -271
  40. lvpath-0.3.1/README.md +0 -244
  41. lvpath-0.3.1/src/lvpath/_cli.py +0 -134
  42. lvpath-0.3.1/src/lvpath/_convert.py +0 -48
  43. lvpath-0.3.1/src/lvpath/_flatbridge.py +0 -69
  44. lvpath-0.3.1/src/lvpath/_flatten.py +0 -139
  45. {lvpath-0.3.1 → lvpath-1.0.0}/src/lvpath/py.typed +0 -0
  46. {lvpath-0.3.1 → lvpath-1.0.0}/tests/fixtures/labview2026_paths.json +0 -0
  47. {lvpath-0.3.1 → lvpath-1.0.0}/tests/test_contract.py +0 -0
  48. {lvpath-0.3.1 → lvpath-1.0.0}/tests/test_convert.py +0 -0
  49. {lvpath-0.3.1 → lvpath-1.0.0}/tests/test_flatten_contract.py +0 -0
  50. {lvpath-0.3.1 → lvpath-1.0.0}/tests/test_portable.py +0 -0
  51. {lvpath-0.3.1 → lvpath-1.0.0}/tests/test_portable_contract.py +0 -0
@@ -10,3 +10,5 @@ dist/
10
10
  .ruff_cache/
11
11
  .coverage
12
12
  htmlcov/
13
+ tests/fixtures/sources/*
14
+ !tests/fixtures/sources/MANIFEST.json
@@ -0,0 +1,140 @@
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
+ ## [1.0.0] - 2026-10-15
10
+
11
+ The public surface is now stable: the seventeen names of 0.3.1 (three of them exception classes) with the same
12
+ parameters and defaults (some annotations widened, listed below), the `lvpath` command and `python -m lvpath`.
13
+ The flattened form is checked against NI's published example and against the
14
+ 349,566 flattened paths LabVIEW 2026 stores in its own installed files.
15
+
16
+ ### Changed
17
+
18
+ - An empty flattened component now decodes as `..` and `..` encodes as an empty
19
+ component, which is how LabVIEW 2026 stores relative links (21,765 links in its own
20
+ files resolve that way). 0.3.1 dropped the empty components, so
21
+ `('', '', 'NI_MDF.lvlib')` decoded as `NI_MDF.lvlib`, and wrote `..` as two dots.
22
+ - `<Not A Path>` in the flattened form is path type 2 with no component, the code
23
+ LabVIEW's `cintools/extcode.h` gives it and the record LabVIEW 2026 stores in
24
+ `resource/labview.rsc`. `to_flat_path("")` and `to_flat_path(None)` now write it (0.3.1
25
+ wrote `""` as an empty relative path, which decodes as `.`, and raised
26
+ `AttributeError` for `None`), and `from_flat_path` raises `NotAPathError` for it (0.3.1:
27
+ `FlatPathError`). `FlatPathType` gains `NOT_A_PATH` (2) and `UNC` (3); a UNC flattened
28
+ path is still refused with `FlatPathError`.
29
+ - `from_flat_path` refuses with `FlatPathError` what 0.3.1 misread: a symbolic
30
+ `<vilib>`-style first component of an absolute path, a first Windows component that is
31
+ not one drive letter (0.3.1 produced `usr:\lib`), literal `.` and `..` components, and
32
+ components holding `/` (or `\` or `:` on Windows).
33
+ - `to_flat_path` refuses a POSIX path starting with `//` (0.3.1 dropped one slash) and a
34
+ rooted Windows path without a drive such as `\usr\x` (0.3.1 wrote it as a relative path
35
+ whose first component was a backslash).
36
+ - One flavor rule for `PurePath` arguments in every function: a `PurePath` keeps its own
37
+ flavor unless `platform` names the other one, and then only a relative path may cross,
38
+ with no backslash or colon inside a component on the way to Windows; anything else
39
+ raises `PortablePathError` (`FlatPathError` in `to_flat_path`). 0.3.1 re-read a
40
+ `PurePath` with the flavor of `platform` (or of the running system) in `to_lv_path`,
41
+ `to_flat_path`, `to_os_path`, `path_kind` and, on POSIX, `from_portable_path`, producing a different path for anchored
42
+ ones, and `from_portable_path` raised `TypeError` for a `PurePath` on Windows. The
43
+ annotations of `to_os_path`, `path_kind` and `from_portable_path` now include
44
+ `PurePath`, and that of `to_flat_path` includes `None`.
45
+ - `platform` is checked before anything else in every function: an unknown name raises
46
+ `ValueError` and a value that is not a `str`, `Platform` or `None` raises `TypeError`
47
+ (0.3.1 raised `AttributeError`), even when the path is empty or the bytes are malformed.
48
+ In 0.3.1, `path_kind("", platform="bogus")` returned `NOT_A_PATH`, `to_lv_path` and
49
+ `to_portable_path` returned `""`, `to_os_path` and `from_portable_path` raised
50
+ `NotAPathError` and `from_flat_path` raised `FlatPathError` for bad bytes.
51
+ - Arguments of the wrong type raise `TypeError` with a short message. 0.3.1 returned
52
+ `'5'` for `to_portable_path(5)` and `"b'x'"` for `to_portable_path(b"x")`, raised
53
+ `AttributeError` for `to_lv_path(5)` and `to_flat_path(5)`, and raised `FlatPathError`
54
+ for `parse_flat_path` given a short `str`.
55
+ - `FlatPath` checks its fields when built: a `bool` or non-integer path type and a
56
+ non-`str` component raise `TypeError` (0.3.1 accepted `FlatPath(0, "ab")`); a non-ASCII
57
+ or over-long component raises `FlatPathError` at once (0.3.1 raised it later, in
58
+ `to_bytes`). The path type is stored as a plain `int` (0.3.1 kept an `IntEnum` member as
59
+ given) and the components as a tuple. `build_flat_path` refuses a `str` as its
60
+ components with `TypeError` (0.3.1 split it into one-character components). A pickle
61
+ written by 0.3.1 loads with its fields normalised the same way.
62
+ - Non-ASCII flattened bytes raise `FlatPathError` instead of `UnicodeDecodeError`.
63
+ `parse_flat_path`, `FlatPath.from_bytes` and `from_flat_path` read the raw bytes of any
64
+ object with the buffer protocol (`bytes`, `bytearray`, `memoryview`, `array`, `mmap`)
65
+ and are typed for `bytes | bytearray | memoryview`. 0.3.1 read `bytes` and
66
+ `bytearray` only: a `memoryview` or `array` holding a component raised
67
+ `AttributeError`, and a wide `array` was measured in items.
68
+ - Error messages quote at most 40 characters of a value and several are reworded; the
69
+ exception types are unchanged except where listed here. The public classes
70
+ (`FlatPath`, `FlatPathType`, `PathKind`, `Platform` and the three exceptions) report
71
+ their module as `lvpath`, so pickles name `lvpath.<class>`; pickles made by 0.3.1 still
72
+ load.
73
+ - Command line: a usage mistake still exits 2, now with lines of at most 200 characters;
74
+ a refused value prints one bounded line and exits 1; a closed output pipe exits 1
75
+ quietly; invalid hex no longer echoes the parser's detail; `lvpath encode ''` prints the
76
+ not-a-path bytes and `lvpath decode` of them is refused. A POSIX argument that is not
77
+ valid UTF-8 is written back as its original bytes (0.3.1 stopped with a
78
+ `UnicodeEncodeError` traceback); a lone surrogate UTF-8 cannot write at all (possible
79
+ in Windows arguments) is refused in one line with status 1; a missing standard stream
80
+ no longer raises.
81
+ - A `str` subclass is read through its plain text: an overridden `__eq__` or `__len__`
82
+ no longer changes a result or slips past the component limits.
83
+
84
+ ### Added
85
+
86
+ - `python -m lvpath`.
87
+ - `FlatPathType.NOT_A_PATH` and `FlatPathType.UNC`.
88
+ - `GUIDE.md` with the rules, the errors table, the ground truth and upgrade notes.
89
+ - Docstring examples for every public name, run as doctests.
90
+
91
+ ## [0.3.1]
92
+
93
+ - The LabVIEW 2026 path fixture no longer carries source lines copied from NI files: each
94
+ row keeps only the path value, its expected renderings, and where it was found (file and
95
+ setting). The sdist is limited to the package, the tests and the docs.
96
+
97
+ ## [0.3.0]
98
+
99
+ - Added the portable path form LabVIEW writes into configuration and project
100
+ files. `from_portable_path` reads `/C/DIR1/DIR2` as `C:\DIR1\DIR2`,
101
+ `//server/share/dir` as a UNC path and `../a/b` as a relative path;
102
+ `to_portable_path` writes them back, keeping the drive letter case. The empty value is `<Not A Path>`.
103
+ - `PortablePathError` reports a value with no faithful counterpart instead of
104
+ rewriting it into another path: a POSIX absolute path read as Windows, a
105
+ drive-relative `C:file`, a rooted Windows path without a drive, a UNC path
106
+ without a share and a `\\?\` device path.
107
+ - The `lvpath` command line gains `to-native` and `to-labview`, and its help lists
108
+ runnable examples for every subcommand.
109
+ - The test suite carries a verbatim excerpt of path values from files shipped with
110
+ LabVIEW 2026 and checks every value in both directions and through a round trip.
111
+ - The README no longer implies that `to_os_path` translates `/C/foo`; it documents
112
+ the portable form, its rules and its one ambiguity.
113
+
114
+ ## [0.2.0]
115
+
116
+ - Added the binary flattened path form (`Flatten To String` `PTH0`). `FlatPath` is
117
+ a typed, frozen result with `to_bytes` and `from_bytes`; `parse_flat_path` and
118
+ `build_flat_path` are the codec entry points. The NI `C:\File` example round-trips
119
+ byte for byte in both directions.
120
+ - `to_flat_path` and `from_flat_path` bridge the flattened form to and from
121
+ `pathlib.PurePath` on a chosen platform flavor.
122
+ - `FlatPathType` exposes the two documented path type codes (absolute `0`, relative
123
+ `1`). UNC and not-a-path codes are undocumented, so they are not guessed: the
124
+ codec still round-trips their raw bytes, while the PurePath bridge refuses an
125
+ un-interpretable type code with `FlatPathError`.
126
+ - Added an `lvpath` command line with `decode` and `encode` subcommands and
127
+ `--version`.
128
+
129
+ ## [0.1.0]
130
+
131
+ - First release.
132
+ - `to_os_path` parses a LabVIEW path string into a `PurePath` of the Windows or
133
+ POSIX flavor; `to_lv_path` serializes an OS path back into a LabVIEW string.
134
+ - `path_kind` and `PathKind` detect absolute, relative and not-a-path values;
135
+ the empty string is the not-a-path value on every platform.
136
+ - `NotAPathError` is raised when the not-a-path value is converted to an OS path.
137
+ - Covers the textual `Path To String` / `String To Path` conventions: Windows
138
+ drive and UNC anchors, POSIX roots, lenient forward slashes, and clean
139
+ cross-platform conversion of relative path objects. Classic Mac OS colon paths
140
+ and the binary flatten path type are out of scope.
@@ -1,19 +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/lvpath/__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.
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/lvpath/__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.
lvpath-1.0.0/GUIDE.md ADDED
@@ -0,0 +1,290 @@
1
+ # lvpath guide
2
+
3
+ LabVIEW keeps a path in three forms. lvpath converts each of them to and from
4
+ `pathlib.PurePath` without touching the file system:
5
+
6
+ | Form | Where LabVIEW uses it | lvpath |
7
+ | --- | --- | --- |
8
+ | Portable text, `/C/DIR1/DIR2` | path values in `.ini` and `.lvproj` files | `from_portable_path`, `to_portable_path` |
9
+ | Native text, `C:\DIR1\DIR2` | `Path To String`, `String To Path` | `to_os_path`, `to_lv_path`, `path_kind` |
10
+ | Flattened bytes, `PTH0...` | `Flatten To String`, VI files, datalogs | `from_flat_path`, `to_flat_path`, `FlatPath` |
11
+
12
+ ## The platform argument
13
+
14
+ Every conversion takes `platform`: `"windows"` or `"posix"` in any letter case, a
15
+ `Platform` member, or `None` for the system running the code. A `str` argument is
16
+ parsed with that flavor. A `PurePath` argument keeps its own flavor; when `platform`
17
+ names the other flavor, only a relative path may cross, and a component holding a
18
+ backslash or a colon may not cross to Windows.
19
+
20
+ ```pycon
21
+ >>> from pathlib import PurePosixPath, PureWindowsPath
22
+ >>> from lvpath import Platform, to_lv_path
23
+ >>> to_lv_path(PureWindowsPath("reports\\q1.txt"), platform="posix")
24
+ 'reports/q1.txt'
25
+ >>> to_lv_path(PureWindowsPath("C:/x"), platform=Platform.POSIX)
26
+ Traceback (most recent call last):
27
+ ...
28
+ lvpath.PortablePathError: cannot carry the anchored path 'C:\\x' to the PurePosixPath flavor
29
+
30
+ ```
31
+
32
+ ## Portable text form
33
+
34
+ When LabVIEW writes a path value into a configuration or project file it uses a
35
+ platform-independent text: a Windows drive becomes a leading one-letter component, every
36
+ separator is `/`, a UNC share keeps its leading `//` and the empty string is
37
+ `<Not A Path>`.
38
+
39
+ | Stored value | `platform="windows"` | `platform="posix"` |
40
+ | --- | --- | --- |
41
+ | `/C/Program Files (x86)/Arduino` | `C:\Program Files (x86)\Arduino` | unchanged |
42
+ | `/c/ni-rt/startup` | `c:\ni-rt\startup` | unchanged |
43
+ | `/D` | `D:\` | unchanged |
44
+ | `//server/share/dir` | `\\server\share\dir` | unchanged |
45
+ | `../builds/Main Application` | `..\builds\Main Application` | unchanged |
46
+ | `/home/lvuser/natinst/bin` | `PortablePathError` | unchanged |
47
+ | empty | `NotAPathError` | `NotAPathError` |
48
+
49
+ ```pycon
50
+ >>> from lvpath import from_portable_path, to_portable_path
51
+ >>> from_portable_path("/D", platform="windows")
52
+ PureWindowsPath('D:/')
53
+ >>> to_portable_path("\\\\server\\share\\dir", platform="windows")
54
+ '//server/share/dir'
55
+ >>> to_portable_path("C:file", platform="windows")
56
+ Traceback (most recent call last):
57
+ ...
58
+ lvpath.PortablePathError: 'C:file' is drive-relative and has no portable form
59
+
60
+ ```
61
+
62
+ - The drive letter keeps its case, so a value in the normal form LabVIEW writes (no
63
+ trailing slash, repeated separator or `.` part) round-trips exactly.
64
+ - A value already in native Windows form (`C:\NI Installers`), which some NI files
65
+ store, is accepted as is when reading on Windows.
66
+ - On POSIX the stored form is already native and is parsed unchanged, so `/C/x` is a
67
+ drive on Windows and a directory on POSIX. Pick `platform` for the system that will
68
+ use the path.
69
+ - Refused with `PortablePathError`: a POSIX absolute path read as Windows, a malformed
70
+ drive (`/C:/x`), a drive-relative `C:file`, a rooted path without a drive (`\dir`),
71
+ a UNC path without a share and the `\\?\` and `\\.\` device forms.
72
+ - `pathlib` normalises the text: repeated separators collapse, a trailing slash and
73
+ `.` parts are dropped, and a backslash read on Windows is a separator.
74
+ - An empty `PurePath` is pathlib's current directory and writes as `"."`; `None` and
75
+ `""` write as `""`.
76
+
77
+ The table rows are checked against values found in files shipped with LabVIEW 2026
78
+ (`tests/fixtures/labview2026_paths.json` records each value with its file and setting,
79
+ not the file text). No shipped file holds a bare drive or a UNC value; those rows are
80
+ covered by tests written by hand that follow the same convention.
81
+
82
+ ## Native text form
83
+
84
+ `to_os_path` and `path_kind` read the text of a host platform, `to_lv_path` writes it.
85
+ On Windows forward slashes are accepted, and only a drive with a root or a UNC share is
86
+ absolute, so `C:file` and `\dir` count as relative, as `pathlib` decides.
87
+
88
+ ```pycon
89
+ >>> from lvpath import NotAPathError, PathKind, path_kind, to_os_path
90
+ >>> path_kind("C:file", platform="windows")
91
+ <PathKind.RELATIVE: 'relative'>
92
+ >>> to_os_path("/C/foo", platform="windows")
93
+ PureWindowsPath('/C/foo')
94
+ >>> try:
95
+ ... to_os_path("", platform="posix")
96
+ ... except NotAPathError as error:
97
+ ... print(error)
98
+ LabVIEW not-a-path has no OS path
99
+
100
+ ```
101
+
102
+ Read stored values with `from_portable_path`: `to_os_path` keeps `/C/foo` as the rooted
103
+ Windows path `\C\foo`.
104
+
105
+ ## Flattened form
106
+
107
+ `Flatten To String` writes a path as `PTH0`, a u32 byte count of the rest, a u16 path
108
+ type, a u16 component count and one length-prefixed ASCII string per component, all
109
+ big-endian. `FlatPath`, `parse_flat_path` and `build_flat_path` read and write that
110
+ container exactly and keep any path type as a number.
111
+
112
+ ```pycon
113
+ >>> from lvpath import FlatPath, build_flat_path, from_flat_path, parse_flat_path, to_flat_path
114
+ >>> parse_flat_path(build_flat_path(1, ["", "", "NI_MDF.lvlib"]))
115
+ FlatPath(path_type=1, components=('', '', 'NI_MDF.lvlib'))
116
+ >>> from_flat_path(build_flat_path(1, ["", "", "NI_MDF.lvlib"]), platform="windows")
117
+ PureWindowsPath('../../NI_MDF.lvlib')
118
+ >>> from_flat_path(build_flat_path(0, []), platform="posix")
119
+ PurePosixPath('/')
120
+ >>> from_flat_path(build_flat_path(0, ["<vilib>", "Utility"]), platform="posix")
121
+ Traceback (most recent call last):
122
+ ...
123
+ lvpath.FlatPathError: symbolic path prefix '<vilib>' has no PurePath meaning
124
+
125
+ ```
126
+
127
+ The path type codes are those LabVIEW's `cintools/extcode.h` lists in order (`fAbsPath`,
128
+ `fRelPath`, `fNotAPath`, `fUNCPath`), exposed as `FlatPathType`, and
129
+ `from_flat_path` and `to_flat_path` interpret three of them:
130
+
131
+ - `0`, absolute (`ABSOLUTE`). On Windows the first component is the drive letter
132
+ without its colon; on POSIX the components follow the root, and no component at all
133
+ is `/`. That empty absolute path (LabVIEW's empty path, common in VI files) has no
134
+ Windows `PurePath` and raises `FlatPathError` there.
135
+ - `1`, relative (`RELATIVE`). No component at all is `.`.
136
+ - In both, an empty component means one level up, `..` in text.
137
+ - `2`, `<Not A Path>` (`NOT_A_PATH`), with no component. `to_flat_path("")` and
138
+ `to_flat_path(None)` write it; `from_flat_path` raises `NotAPathError` for it.
139
+
140
+ Everything else raises `FlatPathError` in the bridge: `3`, UNC (`UNC`; no UNC record
141
+ exists in the LabVIEW install to confirm its component layout, so it is not guessed),
142
+ any other code, a symbolic first component such as `<vilib>`, a first Windows component
143
+ that is not one letter, a literal `.` or `..` component, a component holding `/` (or `\`
144
+ or `:` on Windows), UNC and driveless rooted Windows paths, drive-relative paths and
145
+ POSIX paths starting with `//`. Components are ASCII, at most 255 characters, and at
146
+ most 65535 of them.
147
+
148
+ ### Ground truth for the flattened form
149
+
150
+ - NI's flattened-data page gives `C:\File` as
151
+ `5054 4830 0000 000B 0000 0002 0143 0466 696C 65` and names type `0` only. Its bytes
152
+ spell `file` in lower case. `tests/test_sources_truth.py` reads the example from a
153
+ copy fetched by `scripts/fetch_sources.py` (not committed; `MANIFEST.json` keeps the
154
+ URL, date and SHA-256).
155
+ - `scripts/scan_labview_paths.py` walked a LabVIEW 2026 install on 2026-10-15, skipping
156
+ files over 50 MB (`LabVIEW.exe`, `resource/libcef.dll` and the object cache
157
+ `VIObjCache/26.0.0r0/objFileDB.vidb`, whose records include split, corrupted
158
+ fragments), and found
159
+ 321,686 records of type `0`, 27,879 of type `1` and one of type `2`. Of the 24,024
160
+ relative links that start with empty components, 21,765 name an existing file when
161
+ each empty component is read as one level up from the file holding the link; no
162
+ record holds a literal `..`. The one type `2` record, `PTH0 00000004 0002 0000`, is
163
+ exactly what `to_flat_path("")` writes. Eleven chosen records, with file and offset,
164
+ are in `tests/fixtures/labview2026_flat.json`.
165
+ - `cintools/extcode.h` of LabVIEW 2026 lists the path type codes in order: absolute,
166
+ relative, not-a-path, UNC.
167
+
168
+ ## Errors
169
+
170
+ | Exception | Base | Raised for |
171
+ | --- | --- | --- |
172
+ | `NotAPathError` | `ValueError` | `<Not A Path>` (an empty string, or flattened type `2`) read as an OS path |
173
+ | `PortablePathError` | `ValueError` | a value with no faithful counterpart in the portable or native form |
174
+ | `FlatPathError` | `ValueError` | malformed `PTH0` bytes, bad components, a path the bridge cannot represent |
175
+ | `ValueError` | | an unknown `platform` name |
176
+ | `TypeError` | | an argument of the wrong type (a path that is not `str` or `PurePath`, bytes that are not bytes-like, a non-integer path type) |
177
+
178
+ A message quotes at most 40 characters of any value and states the full length of a
179
+ longer one. The public classes report their module as `lvpath`.
180
+
181
+ ```pycon
182
+ >>> from lvpath import FlatPathError, PortablePathError
183
+ >>> try:
184
+ ... to_portable_path("C:" + "x" * 1000, platform="windows")
185
+ ... except PortablePathError as error:
186
+ ... print(error)
187
+ 'C:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' (first 40 of 1002 characters) is drive-relative and has no portable form
188
+ >>> issubclass(FlatPathError, ValueError)
189
+ True
190
+
191
+ ```
192
+
193
+ ## Command line
194
+
195
+ ```console
196
+ $ lvpath to-native --platform windows //server/share/dir
197
+ \\server\share\dir
198
+ $ lvpath to-labview --platform posix ''
199
+
200
+ $ lvpath decode --platform posix '50544830 00000007 0001 0002 00 0161'
201
+ ../a
202
+ $ lvpath to-native --platform windows /home/lvuser
203
+ lvpath: '/home/lvuser' is a POSIX absolute path with no Windows equivalent
204
+ ```
205
+
206
+ Output is UTF-8, and a POSIX argument that is not valid UTF-8 is written back as its
207
+ original bytes. A refused value prints one line on standard error and exits with
208
+ status 1; a usage mistake prints the usage and one line and exits with status 2; a
209
+ closed output pipe exits with status 1 quietly. A value starting with `-` goes after
210
+ `--`. Under Git Bash on Windows set `MSYS_NO_PATHCONV=1` so `/C/x` reaches lvpath
211
+ unchanged.
212
+
213
+ ## Upgrading from 0.3
214
+
215
+ The public names and parameters are the same; `python -m lvpath`,
216
+ `FlatPathType.NOT_A_PATH` and `FlatPathType.UNC` are new. Behaviour that changed (the
217
+ same list as CHANGELOG 1.0.0):
218
+
219
+ - An empty flattened component now decodes as `..` and `..` encodes as an empty
220
+ component, which is how LabVIEW 2026 stores relative links (21,765 links in its own
221
+ files resolve that way). 0.3.1 dropped the empty components, so
222
+ `('', '', 'NI_MDF.lvlib')` decoded as `NI_MDF.lvlib`, and wrote `..` as two dots.
223
+ - `<Not A Path>` in the flattened form is path type 2 with no component, the code
224
+ LabVIEW's `cintools/extcode.h` gives it and the record LabVIEW 2026 stores in
225
+ `resource/labview.rsc`. `to_flat_path("")` and `to_flat_path(None)` now write it (0.3.1
226
+ wrote `""` as an empty relative path, which decodes as `.`, and raised
227
+ `AttributeError` for `None`), and `from_flat_path` raises `NotAPathError` for it (0.3.1:
228
+ `FlatPathError`). `FlatPathType` gains `NOT_A_PATH` (2) and `UNC` (3); a UNC flattened
229
+ path is still refused with `FlatPathError`.
230
+ - `from_flat_path` refuses with `FlatPathError` what 0.3.1 misread: a symbolic
231
+ `<vilib>`-style first component of an absolute path, a first Windows component that is
232
+ not one drive letter (0.3.1 produced `usr:\lib`), literal `.` and `..` components, and
233
+ components holding `/` (or `\` or `:` on Windows).
234
+ - `to_flat_path` refuses a POSIX path starting with `//` (0.3.1 dropped one slash) and a
235
+ rooted Windows path without a drive such as `\usr\x` (0.3.1 wrote it as a relative path
236
+ whose first component was a backslash).
237
+ - One flavor rule for `PurePath` arguments in every function: a `PurePath` keeps its own
238
+ flavor unless `platform` names the other one, and then only a relative path may cross,
239
+ with no backslash or colon inside a component on the way to Windows; anything else
240
+ raises `PortablePathError` (`FlatPathError` in `to_flat_path`). 0.3.1 re-read a
241
+ `PurePath` with the flavor of `platform` (or of the running system) in `to_lv_path`,
242
+ `to_flat_path`, `to_os_path`, `path_kind` and, on POSIX, `from_portable_path`, producing a different path for anchored
243
+ ones, and `from_portable_path` raised `TypeError` for a `PurePath` on Windows. The
244
+ annotations of `to_os_path`, `path_kind` and `from_portable_path` now include
245
+ `PurePath`, and that of `to_flat_path` includes `None`.
246
+ - `platform` is checked before anything else in every function: an unknown name raises
247
+ `ValueError` and a value that is not a `str`, `Platform` or `None` raises `TypeError`
248
+ (0.3.1 raised `AttributeError`), even when the path is empty or the bytes are malformed.
249
+ In 0.3.1, `path_kind("", platform="bogus")` returned `NOT_A_PATH`, `to_lv_path` and
250
+ `to_portable_path` returned `""`, `to_os_path` and `from_portable_path` raised
251
+ `NotAPathError` and `from_flat_path` raised `FlatPathError` for bad bytes.
252
+ - Arguments of the wrong type raise `TypeError` with a short message. 0.3.1 returned
253
+ `'5'` for `to_portable_path(5)` and `"b'x'"` for `to_portable_path(b"x")`, raised
254
+ `AttributeError` for `to_lv_path(5)` and `to_flat_path(5)`, and raised `FlatPathError`
255
+ for `parse_flat_path` given a short `str`.
256
+ - `FlatPath` checks its fields when built: a `bool` or non-integer path type and a
257
+ non-`str` component raise `TypeError` (0.3.1 accepted `FlatPath(0, "ab")`); a non-ASCII
258
+ or over-long component raises `FlatPathError` at once (0.3.1 raised it later, in
259
+ `to_bytes`). The path type is stored as a plain `int` (0.3.1 kept an `IntEnum` member as
260
+ given) and the components as a tuple. `build_flat_path` refuses a `str` as its
261
+ components with `TypeError` (0.3.1 split it into one-character components). A pickle
262
+ written by 0.3.1 loads with its fields normalised the same way.
263
+ - Non-ASCII flattened bytes raise `FlatPathError` instead of `UnicodeDecodeError`.
264
+ `parse_flat_path`, `FlatPath.from_bytes` and `from_flat_path` read the raw bytes of any
265
+ object with the buffer protocol (`bytes`, `bytearray`, `memoryview`, `array`, `mmap`)
266
+ and are typed for `bytes | bytearray | memoryview`. 0.3.1 read `bytes` and
267
+ `bytearray` only: a `memoryview` or `array` holding a component raised
268
+ `AttributeError`, and a wide `array` was measured in items.
269
+ - Error messages quote at most 40 characters of a value and several are reworded; the
270
+ exception types are unchanged except where listed here. The public classes
271
+ (`FlatPath`, `FlatPathType`, `PathKind`, `Platform` and the three exceptions) report
272
+ their module as `lvpath`, so pickles name `lvpath.<class>`; pickles made by 0.3.1 still
273
+ load.
274
+ - Command line: a usage mistake still exits 2, now with lines of at most 200 characters;
275
+ a refused value prints one bounded line and exits 1; a closed output pipe exits 1
276
+ quietly; invalid hex no longer echoes the parser's detail; `lvpath encode ''` prints the
277
+ not-a-path bytes and `lvpath decode` of them is refused. A POSIX argument that is not
278
+ valid UTF-8 is written back as its original bytes (0.3.1 stopped with a
279
+ `UnicodeEncodeError` traceback); a lone surrogate UTF-8 cannot write at all (possible
280
+ in Windows arguments) is refused in one line with status 1; a missing standard stream
281
+ no longer raises.
282
+ - A `str` subclass is read through its plain text: an overridden `__eq__` or `__len__`
283
+ no longer changes a result or slips past the component limits.
284
+
285
+ ## What it does not do
286
+
287
+ - No file system access, no symbolic links, no `.`/`..` resolution beyond `pathlib`.
288
+ - No classic Mac OS colon paths.
289
+ - No expansion of symbolic prefixes such as `<vilib>` and no resolution of a relative
290
+ value against the file it came from.
@@ -1,21 +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.
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.
lvpath-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.5
2
+ Name: lvpath
3
+ Version: 1.0.0
4
+ Summary: Convert LabVIEW path strings, the portable /C/DIR form of config and project files, and the flattened PTH0 form to and from native paths.
5
+ Project-URL: Homepage, https://lianes8studio.com
6
+ Project-URL: Source, https://github.com/JaviLianes8/lvpath
7
+ Project-URL: Issues, https://github.com/JaviLianes8/lvpath/issues
8
+ Project-URL: Changelog, https://github.com/JaviLianes8/lvpath/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: flatten,ini,labview,lvproj,ni,path,pathlib,portable,pth0
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: mypy; extra == 'dev'
33
+ Requires-Dist: pytest; extra == 'dev'
34
+ Requires-Dist: ruff; extra == 'dev'
35
+ Requires-Dist: twine; extra == 'dev'
36
+ Description-Content-Type: text/markdown
37
+
38
+ # lvpath
39
+
40
+ Convert LabVIEW path values to and from Python `pathlib` paths, in all three forms
41
+ LabVIEW uses: the portable text it writes into configuration and project files
42
+ (`/C/DIR1/DIR2`), the native text of `Path To String`, and the flattened `PTH0` bytes
43
+ of `Flatten To String`. Typed, no dependencies, Python 3.10 to 3.14.
44
+
45
+ ## Install
46
+
47
+ ```
48
+ pip install lvpath
49
+ ```
50
+
51
+ ## Quick start
52
+
53
+ Read a path value stored in an `.ini` or `.lvproj` file, and write one back:
54
+
55
+ ```pycon
56
+ >>> from lvpath import from_portable_path, to_portable_path
57
+ >>> from_portable_path("/C/Program Files (x86)/Arduino", platform="windows")
58
+ PureWindowsPath('C:/Program Files (x86)/Arduino')
59
+ >>> from_portable_path("../builds/Main Application", platform="windows")
60
+ PureWindowsPath('../builds/Main Application')
61
+ >>> to_portable_path("C:/DIR1/DIR2", platform="windows")
62
+ '/C/DIR1/DIR2'
63
+ >>> to_portable_path(None)
64
+ ''
65
+
66
+ ```
67
+
68
+ Decode the flattened bytes of a LabVIEW path, such as NI's `C:\File` example:
69
+
70
+ ```pycon
71
+ >>> from lvpath import from_flat_path, parse_flat_path, to_flat_path
72
+ >>> golden = bytes.fromhex("505448300000000b0000000201430466696c65")
73
+ >>> parse_flat_path(golden)
74
+ FlatPath(path_type=0, components=('C', 'file'))
75
+ >>> from_flat_path(golden, platform="windows")
76
+ PureWindowsPath('C:/file')
77
+ >>> to_flat_path("C:/file", platform="windows") == golden
78
+ True
79
+ >>> from_flat_path(to_flat_path("../lib/x.vi", platform="posix"), platform="posix")
80
+ PurePosixPath('../lib/x.vi')
81
+
82
+ ```
83
+
84
+ Work with the native text form and LabVIEW's `<Not A Path>` (the empty string):
85
+
86
+ ```pycon
87
+ >>> from lvpath import path_kind, to_lv_path, to_os_path
88
+ >>> to_os_path("C:\\Users\\test\\data.txt", platform="windows")
89
+ PureWindowsPath('C:/Users/test/data.txt')
90
+ >>> to_lv_path("logs/run.txt", platform="windows")
91
+ 'logs\\run.txt'
92
+ >>> path_kind("", platform="posix")
93
+ <PathKind.NOT_A_PATH: 'not_a_path'>
94
+
95
+ ```
96
+
97
+ Every `platform` argument takes `"windows"`, `"posix"`, a `Platform` member, or `None`
98
+ for the system running the code. Values with no faithful counterpart raise a typed
99
+ error (`PortablePathError`, `FlatPathError`, `NotAPathError`) instead of being turned
100
+ into a different path.
101
+
102
+ ## Command line
103
+
104
+ ```console
105
+ $ lvpath to-native --platform windows '/C/Program Files (x86)/Arduino'
106
+ C:\Program Files (x86)\Arduino
107
+ $ lvpath to-labview --platform windows 'C:\DIR1\DIR2'
108
+ /C/DIR1/DIR2
109
+ $ lvpath decode --platform windows 505448300000000b0000000201430466696c65
110
+ C:\file
111
+ $ lvpath encode --platform posix ../lib/x.vi
112
+ 505448300000000e0001000300036c696204782e7669
113
+ ```
114
+
115
+ `python -m lvpath` runs the same command. Refused values exit with status 1, usage
116
+ mistakes with status 2. Run `lvpath --help` for every option.
117
+
118
+ ## Why
119
+
120
+ LabVIEW writes one path in different forms depending on where it goes, and none of
121
+ them reads correctly with plain string handling: `/C/DIR1/DIR2` names the wrong file on
122
+ Windows, a native string means something only with the platform it was written on,
123
+ and the flattened form needs the `PTH0` layout. `pathlib`'s `PureWindowsPath` and
124
+ `PurePosixPath` already handle foreign-platform paths with no file access, so lvpath is
125
+ a thin layer on top of them that adds only the LabVIEW part: the portable drive and UNC
126
+ form, `<Not A Path>`, and the flattened container, including the rule that an empty
127
+ flattened component means one level up.
128
+
129
+ The conversions are checked against path values in files shipped with LabVIEW 2026,
130
+ against flattened paths LabVIEW 2026 stored in its own installed files, and against
131
+ the example on NI's public flattened-data page.
132
+
133
+ ## Documentation
134
+
135
+ - [Guide](https://github.com/JaviLianes8/lvpath/blob/main/GUIDE.md): every rule, the
136
+ errors, the command line and upgrading from 0.3.
137
+ - [Changelog](https://github.com/JaviLianes8/lvpath/blob/main/CHANGELOG.md)
138
+
139
+ ## License
140
+
141
+ MIT. See [LICENSE](https://github.com/JaviLianes8/lvpath/blob/main/LICENSE).
142
+
143
+ ## Links
144
+
145
+ - Website: https://lianes8studio.com
146
+ - GitHub: https://github.com/JaviLianes8
147
+ - LinkedIn: https://www.linkedin.com/in/jlianes/