lvpath 0.3.1__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.
- lvpath/__init__.py +48 -0
- lvpath/_cli.py +134 -0
- lvpath/_convert.py +48 -0
- lvpath/_flatbridge.py +69 -0
- lvpath/_flatten.py +139 -0
- lvpath/_kinds.py +21 -0
- lvpath/_platform.py +36 -0
- lvpath/_portable.py +139 -0
- lvpath/py.typed +0 -0
- lvpath-0.3.1.dist-info/METADATA +271 -0
- lvpath-0.3.1.dist-info/RECORD +14 -0
- lvpath-0.3.1.dist-info/WHEEL +4 -0
- lvpath-0.3.1.dist-info/entry_points.txt +2 -0
- lvpath-0.3.1.dist-info/licenses/LICENSE +21 -0
lvpath/__init__.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Convert between LabVIEW path strings and native OS paths.
|
|
2
|
+
|
|
3
|
+
The textual surface (:func:`to_os_path`, :func:`to_lv_path`, :func:`path_kind`)
|
|
4
|
+
models LabVIEW's ``Path To String`` / ``String To Path``. The flattened surface
|
|
5
|
+
(:class:`FlatPath`, :func:`to_flat_path`, :func:`from_flat_path` and friends)
|
|
6
|
+
models the binary ``Flatten To String`` ``PTH0`` form. The portable surface
|
|
7
|
+
(:func:`from_portable_path`, :func:`to_portable_path`) models the
|
|
8
|
+
platform-independent form LabVIEW writes into configuration and project files,
|
|
9
|
+
such as ``/C/DIR1/DIR2`` for ``C:\\DIR1\\DIR2``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from ._convert import path_kind, to_lv_path, to_os_path
|
|
15
|
+
from ._flatbridge import from_flat_path, to_flat_path
|
|
16
|
+
from ._flatten import (
|
|
17
|
+
SIGNATURE,
|
|
18
|
+
FlatPath,
|
|
19
|
+
FlatPathError,
|
|
20
|
+
FlatPathType,
|
|
21
|
+
build_flat_path,
|
|
22
|
+
parse_flat_path,
|
|
23
|
+
)
|
|
24
|
+
from ._kinds import NotAPathError, PathKind
|
|
25
|
+
from ._platform import Platform
|
|
26
|
+
from ._portable import PortablePathError, from_portable_path, to_portable_path
|
|
27
|
+
|
|
28
|
+
__version__ = "0.3.1"
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"SIGNATURE",
|
|
32
|
+
"FlatPath",
|
|
33
|
+
"FlatPathError",
|
|
34
|
+
"FlatPathType",
|
|
35
|
+
"NotAPathError",
|
|
36
|
+
"PathKind",
|
|
37
|
+
"Platform",
|
|
38
|
+
"PortablePathError",
|
|
39
|
+
"build_flat_path",
|
|
40
|
+
"from_flat_path",
|
|
41
|
+
"from_portable_path",
|
|
42
|
+
"parse_flat_path",
|
|
43
|
+
"path_kind",
|
|
44
|
+
"to_flat_path",
|
|
45
|
+
"to_lv_path",
|
|
46
|
+
"to_os_path",
|
|
47
|
+
"to_portable_path",
|
|
48
|
+
]
|
lvpath/_cli.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Command-line interface for LabVIEW path strings and the flattened path form.
|
|
2
|
+
|
|
3
|
+
``lvpath to-native`` turns LabVIEW's portable path string (``/C/DIR1/DIR2``) into
|
|
4
|
+
a native path; ``lvpath to-labview`` does the reverse. ``lvpath decode`` turns the
|
|
5
|
+
hex of a flattened ``PTH0`` byte string into a native path; ``lvpath encode``
|
|
6
|
+
turns a native path into that hex. Errors are written to standard error and the
|
|
7
|
+
process exits with status ``1``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import io
|
|
14
|
+
import sys
|
|
15
|
+
from collections.abc import Sequence
|
|
16
|
+
|
|
17
|
+
from . import __version__
|
|
18
|
+
from ._flatbridge import from_flat_path, to_flat_path
|
|
19
|
+
from ._portable import from_portable_path, to_portable_path
|
|
20
|
+
|
|
21
|
+
_EXAMPLES = """\
|
|
22
|
+
examples:
|
|
23
|
+
# LabVIEW's portable path string, as written in .ini and .lvproj files, to Windows.
|
|
24
|
+
lvpath to-native --platform windows "/C/Program Files (x86)/Arduino"
|
|
25
|
+
# prints C:\\Program Files (x86)\\Arduino
|
|
26
|
+
|
|
27
|
+
# A UNC share and a relative path.
|
|
28
|
+
lvpath to-native --platform windows //server/share/dir
|
|
29
|
+
lvpath to-native --platform windows "../builds/Main Application"
|
|
30
|
+
|
|
31
|
+
# A native Windows path to the portable string LabVIEW writes.
|
|
32
|
+
lvpath to-labview --platform windows "C:\\DIR1\\DIR2"
|
|
33
|
+
# prints /C/DIR1/DIR2
|
|
34
|
+
|
|
35
|
+
# Decode the NI C:\\File flattened example into a path.
|
|
36
|
+
lvpath decode --platform windows 505448300000000b0000000201430466696c65
|
|
37
|
+
|
|
38
|
+
# Encode a path into its flattened hex form (spaces in the hex are accepted on decode).
|
|
39
|
+
lvpath encode --platform posix /usr/local/bin
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
44
|
+
parser = argparse.ArgumentParser(
|
|
45
|
+
prog="lvpath",
|
|
46
|
+
description=(
|
|
47
|
+
"Convert LabVIEW's portable path strings to and from native paths, and decode "
|
|
48
|
+
"or encode the LabVIEW PTH0 flattened path form."
|
|
49
|
+
),
|
|
50
|
+
epilog=_EXAMPLES,
|
|
51
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
52
|
+
)
|
|
53
|
+
parser.add_argument("--version", action="version", version=f"lvpath {__version__}")
|
|
54
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
55
|
+
commands = [
|
|
56
|
+
(
|
|
57
|
+
"to-native",
|
|
58
|
+
"text",
|
|
59
|
+
"a LabVIEW portable path string to a native path",
|
|
60
|
+
"Convert a LabVIEW portable path string (/C/DIR1/DIR2, //server/share, a/b) "
|
|
61
|
+
"into a native path. An empty string is <Not A Path> and is an error.",
|
|
62
|
+
),
|
|
63
|
+
(
|
|
64
|
+
"to-labview",
|
|
65
|
+
"path",
|
|
66
|
+
"a native path to a LabVIEW portable path string",
|
|
67
|
+
"Convert a native path into the portable path string LabVIEW writes into "
|
|
68
|
+
"configuration and project files. An empty path prints an empty line, "
|
|
69
|
+
"LabVIEW's <Not A Path>.",
|
|
70
|
+
),
|
|
71
|
+
(
|
|
72
|
+
"decode",
|
|
73
|
+
"hex",
|
|
74
|
+
"hex of flattened PTH0 bytes to a native path",
|
|
75
|
+
"Decode the hex of a flattened PTH0 byte string into a native path.",
|
|
76
|
+
),
|
|
77
|
+
(
|
|
78
|
+
"encode",
|
|
79
|
+
"path",
|
|
80
|
+
"a native path to flattened PTH0 hex",
|
|
81
|
+
"Encode a native path into the hex of its flattened PTH0 byte string.",
|
|
82
|
+
),
|
|
83
|
+
]
|
|
84
|
+
for name, argument, short, long in commands:
|
|
85
|
+
sub = subparsers.add_parser(name, help=short, description=long)
|
|
86
|
+
sub.add_argument(argument, help=f"the {argument} to convert")
|
|
87
|
+
_add_platform(sub)
|
|
88
|
+
return parser
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _add_platform(parser: argparse.ArgumentParser) -> None:
|
|
92
|
+
parser.add_argument(
|
|
93
|
+
"--platform",
|
|
94
|
+
choices=["windows", "posix"],
|
|
95
|
+
default=None,
|
|
96
|
+
help="native path flavor to use (default: the current operating system)",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
101
|
+
"""Run the command line interface. Returns the process exit status."""
|
|
102
|
+
args = _build_parser().parse_args(argv)
|
|
103
|
+
# Paths may hold any Unicode character; a pipe on Windows defaults to a
|
|
104
|
+
# legacy code page, so write UTF-8 to standard output and standard error.
|
|
105
|
+
for stream in (sys.stdout, sys.stderr):
|
|
106
|
+
if isinstance(stream, io.TextIOWrapper):
|
|
107
|
+
stream.reconfigure(encoding="utf-8")
|
|
108
|
+
try:
|
|
109
|
+
print(_run(args.command, args, args.platform))
|
|
110
|
+
except ValueError as error:
|
|
111
|
+
print(f"lvpath: {error}", file=sys.stderr)
|
|
112
|
+
return 1
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _run(command: str, args: argparse.Namespace, platform: str | None) -> str:
|
|
117
|
+
if command == "to-native":
|
|
118
|
+
return str(from_portable_path(args.text, platform=platform))
|
|
119
|
+
if command == "to-labview":
|
|
120
|
+
return to_portable_path(args.path, platform=platform)
|
|
121
|
+
if command == "decode":
|
|
122
|
+
return str(from_flat_path(_hex_bytes(args.hex), platform=platform))
|
|
123
|
+
return to_flat_path(args.path, platform=platform).hex()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _hex_bytes(raw_hex: str) -> bytes:
|
|
127
|
+
try:
|
|
128
|
+
return bytes.fromhex(raw_hex.replace(" ", ""))
|
|
129
|
+
except ValueError as error:
|
|
130
|
+
raise ValueError(f"not valid hex: {error}") from error
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if __name__ == "__main__":
|
|
134
|
+
sys.exit(main())
|
lvpath/_convert.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Conversion between LabVIEW path strings and native OS paths."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import PurePath
|
|
6
|
+
|
|
7
|
+
from ._kinds import NotAPathError, PathKind
|
|
8
|
+
from ._platform import Platform, flavor, resolve
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def path_kind(lv_path: str, *, platform: Platform | str | None = None) -> PathKind:
|
|
12
|
+
"""Classify a LabVIEW path string as absolute, relative or not-a-path.
|
|
13
|
+
|
|
14
|
+
An empty string is LabVIEW's ``<Not A Path>`` value on every platform.
|
|
15
|
+
"""
|
|
16
|
+
if lv_path == "":
|
|
17
|
+
return PathKind.NOT_A_PATH
|
|
18
|
+
pure = flavor(resolve(platform))(lv_path)
|
|
19
|
+
return PathKind.ABSOLUTE if pure.is_absolute() else PathKind.RELATIVE
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def to_os_path(lv_path: str, *, platform: Platform | str | None = None) -> PurePath:
|
|
23
|
+
"""Parse a LabVIEW path string into a ``PurePath`` of the platform flavor.
|
|
24
|
+
|
|
25
|
+
Raises :class:`NotAPathError` for the empty not-a-path value, which has no
|
|
26
|
+
operating-system path. Forward slashes are accepted when parsing a Windows
|
|
27
|
+
path, matching LabVIEW's lenient reader. This reads the host's native text
|
|
28
|
+
form; for the portable form LabVIEW writes into configuration and project
|
|
29
|
+
files (``/C/DIR1/DIR2``) use :func:`lvpath.from_portable_path`.
|
|
30
|
+
"""
|
|
31
|
+
if lv_path == "":
|
|
32
|
+
raise NotAPathError("LabVIEW not-a-path has no OS path")
|
|
33
|
+
return flavor(resolve(platform))(lv_path)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def to_lv_path(os_path: PurePath | str | None, *, platform: Platform | str | None = None) -> str:
|
|
37
|
+
"""Serialize an OS path into a LabVIEW path string for ``platform``.
|
|
38
|
+
|
|
39
|
+
``None`` or an empty string becomes LabVIEW's not-a-path value (``""``).
|
|
40
|
+
The components of ``os_path`` are rendered with the separators of the
|
|
41
|
+
target platform, so a relative path crosses platforms cleanly.
|
|
42
|
+
"""
|
|
43
|
+
if os_path is None or os_path == "":
|
|
44
|
+
return ""
|
|
45
|
+
target = flavor(resolve(platform))
|
|
46
|
+
if isinstance(os_path, str):
|
|
47
|
+
return str(target(os_path))
|
|
48
|
+
return str(target(*os_path.parts))
|
lvpath/_flatbridge.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Bridge the PTH0 flattened form to and from ``pathlib.PurePath``.
|
|
2
|
+
|
|
3
|
+
Only the two documented path type codes are interpreted here: absolute (``0``)
|
|
4
|
+
and relative (``1``). UNC, driveless and empty / not-a-path forms carry type
|
|
5
|
+
codes the NI documentation does not state, so they are refused with a clear
|
|
6
|
+
error rather than guessed. The low-level :mod:`lvpath._flatten` codec still
|
|
7
|
+
round-trips their raw bytes faithfully.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pathlib import PurePath, PurePosixPath, PureWindowsPath
|
|
13
|
+
|
|
14
|
+
from ._flatten import FlatPath, FlatPathError, FlatPathType, parse_flat_path
|
|
15
|
+
from ._platform import Platform, flavor, resolve
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def to_flat_path(os_path: PurePath | str, *, platform: Platform | str | None = None) -> bytes:
|
|
19
|
+
"""Encode an OS path into its PTH0 flattened byte string.
|
|
20
|
+
|
|
21
|
+
Absolute Windows paths store the drive letter without its colon as the first
|
|
22
|
+
component (matching NI's ``C:\\File`` example); absolute POSIX paths store the
|
|
23
|
+
components after the leading root. UNC, driveless and not-a-path forms are
|
|
24
|
+
refused because their flattened type codes are not documented.
|
|
25
|
+
"""
|
|
26
|
+
target = flavor(resolve(platform))
|
|
27
|
+
pure = target(os_path) if isinstance(os_path, str) else target(*os_path.parts)
|
|
28
|
+
if pure.is_absolute():
|
|
29
|
+
return _encode_absolute(pure, target).to_bytes()
|
|
30
|
+
if pure.drive:
|
|
31
|
+
raise FlatPathError(f"drive-relative paths have no documented flattened form: {pure}")
|
|
32
|
+
return FlatPath(path_type=int(FlatPathType.RELATIVE), components=pure.parts).to_bytes()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def from_flat_path(data: bytes, *, platform: Platform | str | None = None) -> PurePath:
|
|
36
|
+
"""Decode PTH0 flattened bytes into a ``PurePath`` of the platform flavor.
|
|
37
|
+
|
|
38
|
+
Raises :class:`FlatPathError` for a path type with no documented PurePath
|
|
39
|
+
meaning, so an un-interpretable flattened path is reported rather than
|
|
40
|
+
silently reconstructed. Use :func:`lvpath.parse_flat_path` to inspect its raw
|
|
41
|
+
type and components instead.
|
|
42
|
+
"""
|
|
43
|
+
flat = parse_flat_path(data)
|
|
44
|
+
target = flavor(resolve(platform))
|
|
45
|
+
if flat.is_relative:
|
|
46
|
+
return target(*flat.components) if flat.components else target(".")
|
|
47
|
+
if flat.is_absolute:
|
|
48
|
+
return _decode_absolute(flat, target)
|
|
49
|
+
raise FlatPathError(f"path type {flat.path_type} has no documented PurePath meaning")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _encode_absolute(pure: PurePath, target: type[PurePath]) -> FlatPath:
|
|
53
|
+
if target is PureWindowsPath:
|
|
54
|
+
drive = pure.drive
|
|
55
|
+
if drive.startswith("\\\\") or not drive:
|
|
56
|
+
raise FlatPathError(f"UNC and driveless absolute paths are not supported: {pure}")
|
|
57
|
+
components = (drive.rstrip(":"), *pure.parts[1:])
|
|
58
|
+
else:
|
|
59
|
+
components = pure.parts[1:]
|
|
60
|
+
return FlatPath(path_type=int(FlatPathType.ABSOLUTE), components=components)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _decode_absolute(flat: FlatPath, target: type[PurePath]) -> PurePath:
|
|
64
|
+
if target is PureWindowsPath:
|
|
65
|
+
if not flat.components:
|
|
66
|
+
raise FlatPathError("absolute Windows flattened path has no drive component")
|
|
67
|
+
drive, *rest = flat.components
|
|
68
|
+
return PureWindowsPath(f"{drive}:\\", *rest)
|
|
69
|
+
return PurePosixPath("/", *flat.components)
|
lvpath/_flatten.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""The PTH0 binary flattened form of a LabVIEW path.
|
|
2
|
+
|
|
3
|
+
LabVIEW's ``Flatten To String`` renders a Path value as a small big-endian
|
|
4
|
+
container: the ASCII signature ``PTH0``, a u32 total length, a u16 path type, a
|
|
5
|
+
u16 component count and then one length-prefixed (Pascal) string per component.
|
|
6
|
+
The absolute path documented by NI as ``C:\\File`` flattens to::
|
|
7
|
+
|
|
8
|
+
50 54 48 30 00 00 00 0B 00 00 00 02 01 43 04 66 69 6C 65
|
|
9
|
+
|
|
10
|
+
This module is a faithful codec for that container. It keeps the numeric path
|
|
11
|
+
type as read so any flattened path round-trips byte for byte, and it never
|
|
12
|
+
invents a meaning for a type code the NI documentation does not state.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import struct
|
|
18
|
+
from collections.abc import Iterable
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from enum import IntEnum
|
|
21
|
+
|
|
22
|
+
SIGNATURE: bytes = b"PTH0"
|
|
23
|
+
"""The four ASCII bytes that open every flattened LabVIEW path."""
|
|
24
|
+
|
|
25
|
+
_HEADER = struct.Struct(">4sIHH") # signature, total length, path type, count
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FlatPathType(IntEnum):
|
|
29
|
+
"""The path type codes documented and verifiable from the NI example.
|
|
30
|
+
|
|
31
|
+
Only these two are stated by the public documentation. UNC and the empty /
|
|
32
|
+
not-a-path forms carry their own codes that the documentation does not give,
|
|
33
|
+
so they are deliberately absent here rather than guessed.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
ABSOLUTE = 0
|
|
37
|
+
RELATIVE = 1
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class FlatPathError(ValueError):
|
|
41
|
+
"""Raised when flattened path bytes are malformed or cannot be interpreted.
|
|
42
|
+
|
|
43
|
+
The message names the problem: a wrong signature, a truncated stream, a
|
|
44
|
+
declared length that disagrees with the body, a component that overruns the
|
|
45
|
+
buffer, or a path type with no documented meaning.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class FlatPath:
|
|
51
|
+
"""A parsed PTH0 flattened path: the raw path type and its components.
|
|
52
|
+
|
|
53
|
+
``path_type`` is kept as the integer read from the stream, so a flattened
|
|
54
|
+
path with an un-interpreted type code still round-trips through
|
|
55
|
+
:meth:`to_bytes` byte for byte. ``components`` are the decoded Pascal-string
|
|
56
|
+
parts in order.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
path_type: int
|
|
60
|
+
components: tuple[str, ...]
|
|
61
|
+
|
|
62
|
+
def __post_init__(self) -> None:
|
|
63
|
+
if not 0 <= self.path_type <= 0xFFFF:
|
|
64
|
+
raise FlatPathError(f"path type out of u16 range: {self.path_type}")
|
|
65
|
+
if len(self.components) > 0xFFFF:
|
|
66
|
+
raise FlatPathError(f"too many components to flatten: {len(self.components)}")
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def is_absolute(self) -> bool:
|
|
70
|
+
"""Whether the path type is the documented absolute code (``0``)."""
|
|
71
|
+
return self.path_type == FlatPathType.ABSOLUTE
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def is_relative(self) -> bool:
|
|
75
|
+
"""Whether the path type is the documented relative code (``1``)."""
|
|
76
|
+
return self.path_type == FlatPathType.RELATIVE
|
|
77
|
+
|
|
78
|
+
def to_bytes(self) -> bytes:
|
|
79
|
+
"""Serialize this path to its PTH0 flattened byte string."""
|
|
80
|
+
body = bytearray(struct.pack(">HH", self.path_type, len(self.components)))
|
|
81
|
+
for component in self.components:
|
|
82
|
+
encoded = _encode_component(component)
|
|
83
|
+
body.append(len(encoded))
|
|
84
|
+
body += encoded
|
|
85
|
+
return struct.pack(">4sI", SIGNATURE, len(body)) + bytes(body)
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def from_bytes(cls, data: bytes) -> FlatPath:
|
|
89
|
+
"""Parse a PTH0 flattened byte string into a :class:`FlatPath`."""
|
|
90
|
+
if len(data) < _HEADER.size:
|
|
91
|
+
raise FlatPathError(
|
|
92
|
+
f"flattened path is too short: {len(data)} bytes, need at least {_HEADER.size}"
|
|
93
|
+
)
|
|
94
|
+
signature, total, path_type, count = _HEADER.unpack_from(data)
|
|
95
|
+
if signature != SIGNATURE:
|
|
96
|
+
raise FlatPathError(f"bad signature: expected {SIGNATURE!r}, got {signature!r}")
|
|
97
|
+
body = data[8:]
|
|
98
|
+
if len(body) != total:
|
|
99
|
+
raise FlatPathError(f"declared length {total} does not match body of {len(body)} bytes")
|
|
100
|
+
components, offset = _read_components(body, count)
|
|
101
|
+
if offset != len(body):
|
|
102
|
+
raise FlatPathError(f"{len(body) - offset} trailing byte(s) after {count} component(s)")
|
|
103
|
+
return cls(path_type=path_type, components=tuple(components))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def build_flat_path(path_type: int, components: Iterable[str]) -> bytes:
|
|
107
|
+
"""Build PTH0 bytes from a numeric path type and its components."""
|
|
108
|
+
return FlatPath(path_type=path_type, components=tuple(components)).to_bytes()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def parse_flat_path(data: bytes) -> FlatPath:
|
|
112
|
+
"""Parse PTH0 bytes into a typed :class:`FlatPath` result."""
|
|
113
|
+
return FlatPath.from_bytes(data)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _encode_component(component: str) -> bytes:
|
|
117
|
+
try:
|
|
118
|
+
encoded = component.encode("ascii")
|
|
119
|
+
except UnicodeEncodeError as error:
|
|
120
|
+
raise FlatPathError(f"component is not ASCII: {component!r}") from error
|
|
121
|
+
if len(encoded) > 0xFF:
|
|
122
|
+
raise FlatPathError(f"component too long to flatten: {len(encoded)} bytes")
|
|
123
|
+
return encoded
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _read_components(body: bytes, count: int) -> tuple[list[str], int]:
|
|
127
|
+
components: list[str] = []
|
|
128
|
+
offset = 4 # past the path type and count already read from the header
|
|
129
|
+
for index in range(count):
|
|
130
|
+
if offset >= len(body):
|
|
131
|
+
raise FlatPathError(f"stream ends before component {index} length byte")
|
|
132
|
+
length = body[offset]
|
|
133
|
+
offset += 1
|
|
134
|
+
end = offset + length
|
|
135
|
+
if end > len(body):
|
|
136
|
+
raise FlatPathError(f"component {index} of {length} bytes overruns the buffer")
|
|
137
|
+
components.append(body[offset:end].decode("ascii"))
|
|
138
|
+
offset = end
|
|
139
|
+
return components, offset
|
lvpath/_kinds.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""The three LabVIEW path kinds and the not-a-path error."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PathKind(Enum):
|
|
9
|
+
"""Whether a LabVIEW path is absolute, relative or the not-a-path value."""
|
|
10
|
+
|
|
11
|
+
ABSOLUTE = "absolute"
|
|
12
|
+
RELATIVE = "relative"
|
|
13
|
+
NOT_A_PATH = "not_a_path"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NotAPathError(ValueError):
|
|
17
|
+
"""Raised when a LabVIEW not-a-path value is converted to an OS path.
|
|
18
|
+
|
|
19
|
+
LabVIEW's ``<Not A Path>`` has no operating-system equivalent, so there is
|
|
20
|
+
no ``PurePath`` to return. Check :func:`lvpath.path_kind` first to avoid it.
|
|
21
|
+
"""
|
lvpath/_platform.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Target platform selection and the matching pure-path flavor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from pathlib import PurePath, PurePosixPath, PureWindowsPath
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Platform(Enum):
|
|
11
|
+
"""A LabVIEW host platform whose path convention a string follows."""
|
|
12
|
+
|
|
13
|
+
WINDOWS = "windows"
|
|
14
|
+
POSIX = "posix"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def current_platform() -> Platform:
|
|
18
|
+
"""The platform convention of the operating system running this code."""
|
|
19
|
+
return Platform.WINDOWS if os.name == "nt" else Platform.POSIX
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def resolve(platform: Platform | str | None) -> Platform:
|
|
23
|
+
"""Normalise the ``platform`` argument, defaulting to the current OS."""
|
|
24
|
+
if platform is None:
|
|
25
|
+
return current_platform()
|
|
26
|
+
if isinstance(platform, Platform):
|
|
27
|
+
return platform
|
|
28
|
+
try:
|
|
29
|
+
return Platform(platform.lower())
|
|
30
|
+
except ValueError:
|
|
31
|
+
raise ValueError(f"unknown platform: {platform!r}") from None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def flavor(platform: Platform) -> type[PurePath]:
|
|
35
|
+
"""The ``PurePath`` subclass that parses and renders ``platform`` paths."""
|
|
36
|
+
return PureWindowsPath if platform is Platform.WINDOWS else PurePosixPath
|
lvpath/_portable.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""LabVIEW's portable path string form and native ``pathlib`` paths.
|
|
2
|
+
|
|
3
|
+
When LabVIEW writes a path value into a configuration file or a project file it
|
|
4
|
+
uses a platform-independent text form instead of the host's native one:
|
|
5
|
+
|
|
6
|
+
- a Windows drive becomes a leading single-letter component, so ``C:\\DIR1\\DIR2``
|
|
7
|
+
is stored as ``/C/DIR1/DIR2`` and a bare drive ``C:\\`` as ``/C``;
|
|
8
|
+
- a UNC share keeps a leading double slash, ``//server/share/dir``;
|
|
9
|
+
- every separator is a forward slash, so relative paths read ``../builds/app``;
|
|
10
|
+
- a POSIX absolute path (Linux, macOS) is already in this form;
|
|
11
|
+
- the empty string is ``<Not A Path>``.
|
|
12
|
+
|
|
13
|
+
This module converts that form to and from ``PureWindowsPath`` and
|
|
14
|
+
``PurePosixPath``. Values with no faithful counterpart on the target platform
|
|
15
|
+
(a POSIX absolute path read on Windows, a drive-relative ``C:file``, a rooted
|
|
16
|
+
path with no drive, a ``\\\\?\\`` device path) raise :class:`PortablePathError`
|
|
17
|
+
instead of being turned into a different path.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
from pathlib import PurePath, PurePosixPath, PureWindowsPath
|
|
24
|
+
|
|
25
|
+
from ._kinds import NotAPathError
|
|
26
|
+
from ._platform import Platform, flavor, resolve
|
|
27
|
+
|
|
28
|
+
_PORTABLE_DRIVE = re.compile(r"^/([A-Za-z])(?:/(.*))?$", re.DOTALL)
|
|
29
|
+
_MALFORMED_DRIVE = re.compile(r"^/[A-Za-z][:\\]")
|
|
30
|
+
_DEVICE_PREFIXES = ("\\\\?\\", "\\\\.\\")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PortablePathError(ValueError):
|
|
34
|
+
"""Raised when a path has no faithful equivalent in the requested form.
|
|
35
|
+
|
|
36
|
+
The message names the value and why it cannot be converted, for example a
|
|
37
|
+
POSIX absolute path read as a Windows path or a drive-relative ``C:file``.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def from_portable_path(text: str, *, platform: Platform | str | None = None) -> PurePath:
|
|
42
|
+
"""Parse LabVIEW's portable path string into a ``PurePath`` for ``platform``.
|
|
43
|
+
|
|
44
|
+
On Windows ``/C/DIR1/DIR2`` becomes ``PureWindowsPath('C:/DIR1/DIR2')``,
|
|
45
|
+
``//server/share/dir`` becomes a UNC path and ``../a/b`` a relative path.
|
|
46
|
+
A value already written in native Windows form (``C:\\DIR1``) is accepted
|
|
47
|
+
as is. On POSIX the portable form is already native and is parsed directly.
|
|
48
|
+
|
|
49
|
+
Raises :class:`NotAPathError` for the empty ``<Not A Path>`` value and
|
|
50
|
+
:class:`PortablePathError` for a value with no Windows equivalent.
|
|
51
|
+
"""
|
|
52
|
+
if text == "":
|
|
53
|
+
raise NotAPathError("LabVIEW not-a-path has no OS path")
|
|
54
|
+
if resolve(platform) is Platform.POSIX:
|
|
55
|
+
return PurePosixPath(text)
|
|
56
|
+
match = _PORTABLE_DRIVE.fullmatch(text)
|
|
57
|
+
if match:
|
|
58
|
+
drive, rest = match.groups()
|
|
59
|
+
return PureWindowsPath(f"{drive}:\\{rest or ''}")
|
|
60
|
+
if _MALFORMED_DRIVE.match(text):
|
|
61
|
+
raise PortablePathError(f"{text!r} has a malformed drive component, expected /C/...")
|
|
62
|
+
if text.startswith("/") and not text.startswith(("//", "/\\")):
|
|
63
|
+
raise PortablePathError(f"{text!r} is a POSIX absolute path with no Windows equivalent")
|
|
64
|
+
pure = PureWindowsPath(text)
|
|
65
|
+
_check_windows(pure, text)
|
|
66
|
+
return pure
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def to_portable_path(
|
|
70
|
+
os_path: PurePath | str | None, *, platform: Platform | str | None = None
|
|
71
|
+
) -> str:
|
|
72
|
+
"""Serialize a native path into LabVIEW's portable path string.
|
|
73
|
+
|
|
74
|
+
``C:\\DIR1\\DIR2`` becomes ``/C/DIR1/DIR2``, ``\\\\server\\share\\dir``
|
|
75
|
+
becomes ``//server/share/dir`` and a relative path joins its parts with
|
|
76
|
+
forward slashes. ``None`` or ``""`` becomes the ``<Not A Path>`` value ``""``;
|
|
77
|
+
an empty ``PurePath`` is pathlib's current directory and becomes ``"."``.
|
|
78
|
+
|
|
79
|
+
A ``str`` is parsed with the flavor of ``platform`` (default: the running
|
|
80
|
+
operating system). A ``PurePath`` keeps its own flavor; when ``platform``
|
|
81
|
+
names the other flavor, only a relative path may be carried across.
|
|
82
|
+
|
|
83
|
+
Raises :class:`PortablePathError` for a Windows path with no portable form.
|
|
84
|
+
"""
|
|
85
|
+
if os_path is None or os_path == "":
|
|
86
|
+
return ""
|
|
87
|
+
original = os_path if isinstance(os_path, str) else str(os_path)
|
|
88
|
+
pure = _as_pure(os_path, platform)
|
|
89
|
+
if isinstance(pure, PureWindowsPath):
|
|
90
|
+
return _windows_to_portable(pure, original)
|
|
91
|
+
return str(pure)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _as_pure(os_path: PurePath | str, platform: Platform | str | None) -> PurePath:
|
|
95
|
+
if isinstance(os_path, str):
|
|
96
|
+
return flavor(resolve(platform))(os_path)
|
|
97
|
+
if platform is None:
|
|
98
|
+
return os_path
|
|
99
|
+
target = flavor(resolve(platform))
|
|
100
|
+
if isinstance(os_path, target):
|
|
101
|
+
return os_path
|
|
102
|
+
if os_path.anchor:
|
|
103
|
+
raise PortablePathError(
|
|
104
|
+
f"cannot carry the anchored path {str(os_path)!r} to the {target.__name__} flavor"
|
|
105
|
+
)
|
|
106
|
+
if target is PureWindowsPath and any("\\" in part for part in os_path.parts):
|
|
107
|
+
raise PortablePathError(
|
|
108
|
+
f"a component of {str(os_path)!r} contains a backslash, a Windows separator"
|
|
109
|
+
)
|
|
110
|
+
return target(*os_path.parts)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _windows_to_portable(pure: PureWindowsPath, original: str) -> str:
|
|
114
|
+
_check_windows(pure, original)
|
|
115
|
+
if not pure.drive:
|
|
116
|
+
return "/".join(pure.parts) or "."
|
|
117
|
+
rest = "/".join(pure.parts[1:])
|
|
118
|
+
if pure.drive.startswith("\\\\"):
|
|
119
|
+
head = "//" + pure.drive[2:].replace("\\", "/")
|
|
120
|
+
else:
|
|
121
|
+
head = "/" + pure.drive[0]
|
|
122
|
+
return f"{head}/{rest}" if rest else head
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _check_windows(pure: PureWindowsPath, original: str) -> None:
|
|
126
|
+
# The UNC and device forms are judged on the text itself: pathlib parses an
|
|
127
|
+
# incomplete UNC prefix differently across Python versions.
|
|
128
|
+
text = original.replace("/", "\\")
|
|
129
|
+
if text.startswith(_DEVICE_PREFIXES):
|
|
130
|
+
raise PortablePathError(f"{original!r} is a device path with no portable form")
|
|
131
|
+
if text.startswith("\\\\"):
|
|
132
|
+
server, _, rest = text[2:].partition("\\")
|
|
133
|
+
if not server or not rest.partition("\\")[0] or not pure.drive.startswith("\\\\"):
|
|
134
|
+
raise PortablePathError(f"{original!r} is not a //server/share UNC path")
|
|
135
|
+
return
|
|
136
|
+
if pure.drive and not pure.root:
|
|
137
|
+
raise PortablePathError(f"{original!r} is drive-relative and has no portable form")
|
|
138
|
+
if pure.root and not pure.drive:
|
|
139
|
+
raise PortablePathError(f"{original!r} is rooted without a drive or UNC share")
|
lvpath/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: lvpath
|
|
3
|
+
Version: 0.3.1
|
|
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: Repository, 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
|
|
15
|
+
Classifier: Development Status :: 4 - Beta
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: build; extra == 'dev'
|
|
22
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
24
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
25
|
+
Requires-Dist: twine; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# lvpath
|
|
29
|
+
|
|
30
|
+
Convert between LabVIEW path strings and native OS paths: the host text form, the
|
|
31
|
+
portable form LabVIEW writes into config and project files (`/C/DIR1/DIR2`), and the
|
|
32
|
+
flattened `PTH0` binary form, with absolute, relative and not-a-path detection.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
pip install lvpath
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from lvpath import to_os_path, to_lv_path, path_kind, PathKind, NotAPathError
|
|
44
|
+
|
|
45
|
+
# Parse a LabVIEW path string into a PurePath of the chosen platform flavor.
|
|
46
|
+
to_os_path("C:\\Users\\test\\data.txt", platform="windows")
|
|
47
|
+
# PureWindowsPath('C:/Users/test/data.txt')
|
|
48
|
+
|
|
49
|
+
to_os_path("/usr/local/bin", platform="posix")
|
|
50
|
+
# PurePosixPath('/usr/local/bin')
|
|
51
|
+
|
|
52
|
+
# Serialize an OS path back into a LabVIEW path string.
|
|
53
|
+
to_lv_path("logs/run.txt", platform="windows")
|
|
54
|
+
# 'logs\\run.txt'
|
|
55
|
+
|
|
56
|
+
# A relative path object crosses platforms cleanly.
|
|
57
|
+
win = to_os_path("reports\\q1\\data.txt", platform="windows")
|
|
58
|
+
to_lv_path(win, platform="posix")
|
|
59
|
+
# 'reports/q1/data.txt'
|
|
60
|
+
|
|
61
|
+
# Detect the kind. The empty string is LabVIEW's <Not A Path> on every platform.
|
|
62
|
+
path_kind("", platform="posix") # PathKind.NOT_A_PATH
|
|
63
|
+
path_kind("sub\\file", platform="windows") # PathKind.RELATIVE
|
|
64
|
+
path_kind("C:\\x", platform="windows") # PathKind.ABSOLUTE
|
|
65
|
+
|
|
66
|
+
# Not-a-path has no OS equivalent, so to_os_path refuses it.
|
|
67
|
+
try:
|
|
68
|
+
to_os_path("", platform="posix")
|
|
69
|
+
except NotAPathError:
|
|
70
|
+
...
|
|
71
|
+
|
|
72
|
+
# None (or "") serializes back to the not-a-path value.
|
|
73
|
+
to_lv_path(None, platform="windows") # ''
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`platform` accepts `"windows"`, `"posix"`, a `Platform` member, or `None` to use
|
|
77
|
+
the convention of the operating system running the code.
|
|
78
|
+
|
|
79
|
+
## Portable form (config and project files)
|
|
80
|
+
|
|
81
|
+
When LabVIEW writes a path value into a configuration (`.ini`) file or a project
|
|
82
|
+
(`.lvproj`) file, it does not use the native Windows text. It uses a
|
|
83
|
+
platform-independent form: the drive letter becomes a leading component, every
|
|
84
|
+
separator is a forward slash, a UNC share keeps a leading double slash and the empty
|
|
85
|
+
string is `<Not A Path>`. `C:\DIR1\DIR2` is stored as `/C/DIR1/DIR2`.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from lvpath import from_portable_path, to_portable_path, PortablePathError
|
|
89
|
+
|
|
90
|
+
# Read a stored value into a native path.
|
|
91
|
+
from_portable_path("/C/Program Files (x86)/Arduino", platform="windows")
|
|
92
|
+
# PureWindowsPath('C:/Program Files (x86)/Arduino')
|
|
93
|
+
from_portable_path("//server/share/dir", platform="windows")
|
|
94
|
+
# PureWindowsPath('//server/share/dir')
|
|
95
|
+
from_portable_path("../builds/Main Application", platform="windows")
|
|
96
|
+
# PureWindowsPath('../builds/Main Application')
|
|
97
|
+
from_portable_path("/home/lvuser/natinst/bin", platform="posix")
|
|
98
|
+
# PurePosixPath('/home/lvuser/natinst/bin')
|
|
99
|
+
|
|
100
|
+
# Write a native path back in the stored form.
|
|
101
|
+
to_portable_path("C:\\DIR1\\DIR2", platform="windows") # '/C/DIR1/DIR2'
|
|
102
|
+
to_portable_path("D:\\", platform="windows") # '/D'
|
|
103
|
+
to_portable_path("\\\\server\\share\\dir", platform="windows") # '//server/share/dir'
|
|
104
|
+
to_portable_path(None) # '' (<Not A Path>)
|
|
105
|
+
|
|
106
|
+
# Values with no faithful counterpart are refused, never rewritten into another path.
|
|
107
|
+
try:
|
|
108
|
+
from_portable_path("/home/lvuser", platform="windows")
|
|
109
|
+
except PortablePathError:
|
|
110
|
+
...
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The rules below are checked against path values in files shipped with LabVIEW 2026
|
|
114
|
+
(`tests/fixtures/labview2026_paths.json` records each value with the file and setting it
|
|
115
|
+
was found in; the NI files themselves are not copied).
|
|
116
|
+
No shipped file holds a bare drive or a UNC value, so the `/D` and `//server/share/dir`
|
|
117
|
+
rows are covered by synthetic tests that follow the same convention:
|
|
118
|
+
|
|
119
|
+
| Stored value | Windows | POSIX |
|
|
120
|
+
| -------------------------------- | ------------------------------ | -------------------------------- |
|
|
121
|
+
| `/C/Program Files (x86)/Arduino` | `C:\Program Files (x86)\Arduino` | `/C/Program Files (x86)/Arduino` |
|
|
122
|
+
| `/c/ni-rt/startup` | `c:\ni-rt\startup` | `/c/ni-rt/startup` |
|
|
123
|
+
| `/D` | `D:\` | `/D` |
|
|
124
|
+
| `//server/share/dir` | `\\server\share\dir` | `//server/share/dir` |
|
|
125
|
+
| `../builds/Main Application` | `..\builds\Main Application` | `../builds/Main Application` |
|
|
126
|
+
| `/home/lvuser/natinst/bin` | `PortablePathError` | `/home/lvuser/natinst/bin` |
|
|
127
|
+
| empty | `NotAPathError` | `NotAPathError` |
|
|
128
|
+
|
|
129
|
+
- The drive letter keeps its case, so a value in normal form (no trailing slash,
|
|
130
|
+
repeated separator or `.` component, as LabVIEW writes it) round-trips exactly.
|
|
131
|
+
- A value already in native Windows form (`C:\NI Installers`), which some NI files
|
|
132
|
+
store, is accepted as is when reading on Windows.
|
|
133
|
+
- On POSIX the stored form is already native and is parsed unchanged. A one-letter
|
|
134
|
+
first directory is therefore ambiguous: `/C/x` reads as the drive `C:` on Windows
|
|
135
|
+
and as the directory `/C/x` on POSIX. Pick `platform` for the system that will
|
|
136
|
+
use the path.
|
|
137
|
+
- `PortablePathError` is raised for a POSIX absolute path read as Windows, a
|
|
138
|
+
drive-relative `C:file`, a rooted Windows path without a drive (`\dir`), a UNC
|
|
139
|
+
path without a share and a `\\?\` device path.
|
|
140
|
+
- A `str` is parsed with the flavor of `platform`; a `PurePath` keeps its own
|
|
141
|
+
flavor, and only a relative one may be carried to the other flavor.
|
|
142
|
+
- Text is normalised by `pathlib`: repeated separators collapse, a trailing slash and
|
|
143
|
+
`.` components are dropped, so `/C/x/` writes back as `/C/x`. A backslash inside a
|
|
144
|
+
value read on Windows is a separator (`/C/x\y` reads as `C:\x\y`).
|
|
145
|
+
- The sibling `lvconfig` library (0.3.0 and later) gives the same results on drive,
|
|
146
|
+
UNC and relative values in normal form, and refuses the same values lvpath refuses
|
|
147
|
+
(`C:file`, `D:`, `\dir`, `//server`, a POSIX absolute path on Windows). The one
|
|
148
|
+
difference left: lvpath normalises text through `pathlib`, where lvconfig only
|
|
149
|
+
swaps separators.
|
|
150
|
+
|
|
151
|
+
## Flattened form
|
|
152
|
+
|
|
153
|
+
`Flatten To String` renders a LabVIEW Path as a small big-endian binary
|
|
154
|
+
container: the ASCII signature `PTH0`, a `u32` total length, a `u16` path type, a
|
|
155
|
+
`u16` component count and then one length-prefixed (Pascal) string per component.
|
|
156
|
+
NI documents the absolute path `C:\File` flattening to the bytes
|
|
157
|
+
`50 54 48 30 00 00 00 0B 00 00 00 02 01 43 04 66 69 6C 65`.
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from lvpath import FlatPath, parse_flat_path, from_flat_path, to_flat_path
|
|
161
|
+
|
|
162
|
+
golden = bytes.fromhex("505448300000000b0000000201430466696c65")
|
|
163
|
+
|
|
164
|
+
# Parse the flattened bytes into a typed result.
|
|
165
|
+
parse_flat_path(golden)
|
|
166
|
+
# FlatPath(path_type=0, components=('C', 'file'))
|
|
167
|
+
|
|
168
|
+
# Round-trips byte for byte.
|
|
169
|
+
parse_flat_path(golden).to_bytes() == golden # True
|
|
170
|
+
|
|
171
|
+
# Bridge to and from pathlib on a chosen platform flavor.
|
|
172
|
+
from_flat_path(golden, platform="windows") # PureWindowsPath('C:/file')
|
|
173
|
+
to_flat_path("C:/file", platform="windows") # the golden bytes
|
|
174
|
+
to_flat_path("/usr/local/bin", platform="posix") # a POSIX absolute flattened path
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Only the two documented path type codes are interpreted: `0` absolute and `1`
|
|
178
|
+
relative, exposed as `FlatPathType`. UNC and empty / not-a-path forms carry type
|
|
179
|
+
codes the public NI documentation does not state, so they are not guessed: the
|
|
180
|
+
low-level codec still round-trips their raw bytes faithfully through `FlatPath`,
|
|
181
|
+
while `to_flat_path` and `from_flat_path` refuse an un-interpretable type code
|
|
182
|
+
with a clear `FlatPathError` instead of inventing a meaning.
|
|
183
|
+
|
|
184
|
+
## Command line
|
|
185
|
+
|
|
186
|
+
Installing the package provides an `lvpath` command for the portable string form and
|
|
187
|
+
the flattened form.
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
# A stored LabVIEW path string to a native path, and back.
|
|
191
|
+
lvpath to-native --platform windows "/C/Program Files (x86)/Arduino"
|
|
192
|
+
# C:\Program Files (x86)\Arduino
|
|
193
|
+
lvpath to-labview --platform windows "C:\DIR1\DIR2"
|
|
194
|
+
# /C/DIR1/DIR2
|
|
195
|
+
|
|
196
|
+
# Decode the NI C:\File flattened example into a path.
|
|
197
|
+
lvpath decode --platform windows 505448300000000b0000000201430466696c65
|
|
198
|
+
# C:\file
|
|
199
|
+
|
|
200
|
+
# Encode a path into its flattened hex form.
|
|
201
|
+
lvpath encode --platform windows "C:\file"
|
|
202
|
+
# 505448300000000b0000000201430466696c65
|
|
203
|
+
|
|
204
|
+
lvpath --version
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`lvpath decode` accepts hex with or without spaces. `lvpath to-labview ""` prints an
|
|
208
|
+
empty line, the `<Not A Path>` value, while `lvpath to-native ""` is an error. Errors
|
|
209
|
+
are written to standard error and the command exits with status 1. Output is UTF-8.
|
|
210
|
+
A value that starts with `-` goes after `--`. Under Git Bash on Windows, set
|
|
211
|
+
`MSYS_NO_PATHCONV=1` so the shell passes POSIX-looking arguments such as `/C/x`
|
|
212
|
+
unchanged. Run `lvpath --help` for the full usage.
|
|
213
|
+
|
|
214
|
+
## Covered conventions
|
|
215
|
+
|
|
216
|
+
LabVIEW's `Path To String` and `String To Path` render and parse a path in the
|
|
217
|
+
standard textual form of a host platform. `to_os_path`, `to_lv_path` and `path_kind`
|
|
218
|
+
model that native textual form (so `to_os_path("/C/foo", platform="windows")` is the
|
|
219
|
+
rooted Windows path `\C\foo`; read stored values with `from_portable_path` instead):
|
|
220
|
+
|
|
221
|
+
- **Separators:** `\` on Windows, `/` on POSIX. Forward slashes are also accepted
|
|
222
|
+
when parsing a Windows path, matching LabVIEW's lenient reader.
|
|
223
|
+
- **Absolute, Windows:** drive-letter paths (`C:\dir\file`) and UNC paths
|
|
224
|
+
(`\\server\share\dir`).
|
|
225
|
+
- **Absolute, POSIX:** a leading `/` (modern LabVIEW on macOS and Linux).
|
|
226
|
+
- **Relative:** any other non-empty string, including up-level (`..\up`) and
|
|
227
|
+
drive-relative (`C:file`) forms.
|
|
228
|
+
- **Not-a-path:** the empty string, LabVIEW's `<Not A Path>` value, on every
|
|
229
|
+
platform. `None` serializes back to it.
|
|
230
|
+
|
|
231
|
+
### What it does not do
|
|
232
|
+
|
|
233
|
+
- It does not touch the filesystem, resolve symbolic links, or normalise `.`/`..`
|
|
234
|
+
beyond what `pathlib` does.
|
|
235
|
+
- It does not handle classic Mac OS colon-separated paths (pre-OS X). Modern
|
|
236
|
+
LabVIEW on macOS uses POSIX `/`.
|
|
237
|
+
- The flattened codec interprets only the documented absolute and relative path
|
|
238
|
+
type codes. It does not guess the undocumented UNC or not-a-path type codes, and
|
|
239
|
+
it does not touch the textual-form behaviour above.
|
|
240
|
+
- It does not expand LabVIEW symbolic path prefixes such as `<vilib>` or
|
|
241
|
+
`<instrlib>`, and it does not resolve a relative stored value against the file
|
|
242
|
+
it came from.
|
|
243
|
+
|
|
244
|
+
## Why
|
|
245
|
+
|
|
246
|
+
LabVIEW uses two text forms for a path: the native form of its host platform for
|
|
247
|
+
`Path To String` / `String To Path`, and a platform-independent form (`/C/DIR1/DIR2`
|
|
248
|
+
for `C:\DIR1\DIR2`) when it writes a path into configuration and project files. Read
|
|
249
|
+
with plain string handling, the stored form names the wrong file on Windows, and a
|
|
250
|
+
native string only means something together with the platform convention it was
|
|
251
|
+
written in, so round-tripping LabVIEW path data in Python needs explicit,
|
|
252
|
+
platform-aware handling.
|
|
253
|
+
|
|
254
|
+
The standard library already does most of this. `pathlib`'s `PureWindowsPath` and
|
|
255
|
+
`PurePosixPath` are built to manipulate foreign-platform paths with no I/O, so they
|
|
256
|
+
cover the bulk of the separator and anchor handling on their own, and this library is
|
|
257
|
+
a thin wrapper over them rather than a reimplementation. What they do not know is the
|
|
258
|
+
genuinely LabVIEW-specific residue: the stored drive and UNC form (`/C/foo` to
|
|
259
|
+
`C:\foo` and back, through `from_portable_path` and `to_portable_path`), the empty
|
|
260
|
+
string standing for `<Not A Path>`, and LabVIEW's exact serialization rules. No PyPI package targets those conventions. `lvpath` adds exactly
|
|
261
|
+
that layer on top of `pathlib`, with a small, typed, zero-dependency surface.
|
|
262
|
+
|
|
263
|
+
## License
|
|
264
|
+
|
|
265
|
+
MIT. See [LICENSE](LICENSE).
|
|
266
|
+
|
|
267
|
+
## Links
|
|
268
|
+
|
|
269
|
+
- Website: https://lianes8studio.com
|
|
270
|
+
- GitHub: https://github.com/JaviLianes8
|
|
271
|
+
- LinkedIn: https://www.linkedin.com/in/jlianes/
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
lvpath/__init__.py,sha256=vu-gM9D1hAdkm8ne5gN7766ZQub-LSceU63gJw3Mg58,1391
|
|
2
|
+
lvpath/_cli.py,sha256=dXCnsBD6YCIEbX7qBOExAFSUzjoCs2KUl5bBV6hbqms,4952
|
|
3
|
+
lvpath/_convert.py,sha256=DyxgMwQYMIoxrq_oSTB9HWM8Amcbjm076WO_cbAolmw,1946
|
|
4
|
+
lvpath/_flatbridge.py,sha256=gDnFhyE0fcSaDPYTyLGraGUU94PaNRkt5YppQ2OqJ7o,3155
|
|
5
|
+
lvpath/_flatten.py,sha256=t87NqCOhkVLbdyIUWL5JX2g-M4dbyVrwjJzPa_0PsSM,5496
|
|
6
|
+
lvpath/_kinds.py,sha256=-EUYDTFl8K2bjouBGMwFF6QGK91J3-sqmnaB2VX15V4,585
|
|
7
|
+
lvpath/_platform.py,sha256=QudH7SeeHYMVVNf35wEfo6F9Ha0FxBcRt6rLfqA9jdE,1135
|
|
8
|
+
lvpath/_portable.py,sha256=hHeYICGEXnhO0nBQ_KOYzW82VrGGqOYf9HT2U3L5a9s,6074
|
|
9
|
+
lvpath/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
lvpath-0.3.1.dist-info/METADATA,sha256=L6EcoRRWnV7HQjrh-UrVFDFUv85NiQ-R8W7KfkzL0og,12678
|
|
11
|
+
lvpath-0.3.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
12
|
+
lvpath-0.3.1.dist-info/entry_points.txt,sha256=G1ZaJMn30SluLurjE2tZROWJ1tK4TcDtss1SGDfQkKA,44
|
|
13
|
+
lvpath-0.3.1.dist-info/licenses/LICENSE,sha256=wirD5nCA2aQa5UusK_1YHLBogI3CGcw23xAnulp3L_U,1091
|
|
14
|
+
lvpath-0.3.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Javier Lianes
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|