vcti-datanode 1.0.1__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.
@@ -0,0 +1,8 @@
1
+ Copyright (c) 2018-2026 Visual Collaboration Technologies Inc.
2
+ All Rights Reserved.
3
+
4
+ This software is proprietary and confidential. Unauthorized copying,
5
+ distribution, or use of this software, via any medium, is strictly
6
+ prohibited. Access is granted only to authorized VCollab developers
7
+ and individuals explicitly authorized by Visual Collaboration
8
+ Technologies Inc.
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.4
2
+ Name: vcti-datanode
3
+ Version: 1.0.1
4
+ Summary: Lightweight data-plus-attributes node containers, with an optional lazily-loaded variant for out-of-core data.
5
+ Author: Visual Collaboration Technologies Inc.
6
+ Requires-Python: <3.15,>=3.12
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: numpy>=1.26
10
+ Provides-Extra: test
11
+ Requires-Dist: pytest; extra == "test"
12
+ Requires-Dist: pytest-cov; extra == "test"
13
+ Provides-Extra: lint
14
+ Requires-Dist: ruff; extra == "lint"
15
+ Provides-Extra: typecheck
16
+ Requires-Dist: mypy; extra == "typecheck"
17
+ Dynamic: license-file
18
+
19
+ # Data Node
20
+
21
+ Lightweight data-plus-attributes node containers, with an optional
22
+ lazily-loaded variant for out-of-core data.
23
+
24
+ ## Overview
25
+
26
+ `vcti-datanode` provides two small, tree-agnostic value types:
27
+
28
+ - **`DataNode`** — pairs `data` (an `np.ndarray`, any array-like, or an
29
+ arbitrary object) with an `attributes` metadata dict. Value equality and
30
+ readable `repr`/`str`. That's it.
31
+ - **`LazyDataNode`** — a `DataNode` whose data is fetched on demand through a
32
+ loader callback (`load()`) and can be freed (`release()`) and reloaded
33
+ later. Attributes stay resident; only the data comes and goes. Built for
34
+ large datasets (e.g. CAE models) where holding everything in memory at once
35
+ is impractical.
36
+
37
+ They are commonly used as **node payloads** in a tree, but depend on nothing
38
+ but numpy and can be used anywhere a "data + metadata" unit is useful.
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install vcti-datanode
44
+ ```
45
+
46
+ ### In `requirements.txt`
47
+
48
+ ```
49
+ vcti-datanode>=1.0.1
50
+ ```
51
+
52
+ ### In `pyproject.toml` dependencies
53
+
54
+ ```toml
55
+ dependencies = [
56
+ "vcti-datanode>=1.0.1",
57
+ ]
58
+ ```
59
+
60
+ ---
61
+
62
+ ## Quick Start
63
+
64
+ ```python
65
+ import numpy as np
66
+ from vcti.datanode import DataNode, LazyDataNode
67
+
68
+ # Eager: data held directly.
69
+ node = DataNode(data=np.array([1.0, 2.0, 3.0]), attributes={"units": "mm"})
70
+
71
+ # Lazy: data fetched on demand, freed when not needed.
72
+ lazy = LazyDataNode(loader=lambda: np.load("displacement.npy"),
73
+ attributes={"name": "displacement"})
74
+ lazy.is_loaded # False
75
+ arr = lazy.load() # loader runs once; cached thereafter
76
+ lazy.release() # frees the data; attributes remain
77
+ ```
78
+
79
+ ---
80
+
81
+ ## Dependencies
82
+
83
+ - `numpy>=1.26` — used for array-aware value equality and repr.
@@ -0,0 +1,65 @@
1
+ # Data Node
2
+
3
+ Lightweight data-plus-attributes node containers, with an optional
4
+ lazily-loaded variant for out-of-core data.
5
+
6
+ ## Overview
7
+
8
+ `vcti-datanode` provides two small, tree-agnostic value types:
9
+
10
+ - **`DataNode`** — pairs `data` (an `np.ndarray`, any array-like, or an
11
+ arbitrary object) with an `attributes` metadata dict. Value equality and
12
+ readable `repr`/`str`. That's it.
13
+ - **`LazyDataNode`** — a `DataNode` whose data is fetched on demand through a
14
+ loader callback (`load()`) and can be freed (`release()`) and reloaded
15
+ later. Attributes stay resident; only the data comes and goes. Built for
16
+ large datasets (e.g. CAE models) where holding everything in memory at once
17
+ is impractical.
18
+
19
+ They are commonly used as **node payloads** in a tree, but depend on nothing
20
+ but numpy and can be used anywhere a "data + metadata" unit is useful.
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ pip install vcti-datanode
26
+ ```
27
+
28
+ ### In `requirements.txt`
29
+
30
+ ```
31
+ vcti-datanode>=1.0.1
32
+ ```
33
+
34
+ ### In `pyproject.toml` dependencies
35
+
36
+ ```toml
37
+ dependencies = [
38
+ "vcti-datanode>=1.0.1",
39
+ ]
40
+ ```
41
+
42
+ ---
43
+
44
+ ## Quick Start
45
+
46
+ ```python
47
+ import numpy as np
48
+ from vcti.datanode import DataNode, LazyDataNode
49
+
50
+ # Eager: data held directly.
51
+ node = DataNode(data=np.array([1.0, 2.0, 3.0]), attributes={"units": "mm"})
52
+
53
+ # Lazy: data fetched on demand, freed when not needed.
54
+ lazy = LazyDataNode(loader=lambda: np.load("displacement.npy"),
55
+ attributes={"name": "displacement"})
56
+ lazy.is_loaded # False
57
+ arr = lazy.load() # loader runs once; cached thereafter
58
+ lazy.release() # frees the data; attributes remain
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Dependencies
64
+
65
+ - `numpy>=1.26` — used for array-aware value equality and repr.
@@ -0,0 +1,63 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "vcti-datanode"
7
+ version = "1.0.1"
8
+ description = "Lightweight data-plus-attributes node containers, with an optional lazily-loaded variant for out-of-core data."
9
+ readme = "README.md"
10
+ authors = [
11
+ {name = "Visual Collaboration Technologies Inc."}
12
+ ]
13
+ requires-python = ">=3.12,<3.15"
14
+ dependencies = [
15
+ "numpy>=1.26",
16
+ ]
17
+
18
+ [tool.setuptools.packages.find]
19
+ where = ["src"]
20
+ include = ["vcti.datanode", "vcti.datanode.*"]
21
+
22
+ [tool.setuptools.package-data]
23
+ "vcti.datanode" = ["py.typed"]
24
+
25
+ [project.optional-dependencies]
26
+ test = ["pytest", "pytest-cov"]
27
+ lint = ["ruff"]
28
+ typecheck = ["mypy"]
29
+
30
+ # zip-safe must be false for a py.typed package: type checkers (PEP 561)
31
+ # cannot read the marker or stubs from a zip-imported distribution.
32
+ [tool.setuptools]
33
+ zip-safe = false
34
+
35
+ [tool.pytest.ini_options]
36
+ addopts = "--cov=vcti.datanode --cov-report=term-missing --cov-fail-under=95"
37
+
38
+ [tool.mypy]
39
+ python_version = "3.12"
40
+ strict = true
41
+ files = ["src"]
42
+ namespace_packages = true
43
+ explicit_package_bases = true
44
+ mypy_path = ["src"]
45
+
46
+ [tool.coverage.run]
47
+ branch = true
48
+
49
+ [tool.coverage.report]
50
+ exclude_also = [
51
+ "raise NotImplementedError",
52
+ "if TYPE_CHECKING:",
53
+ "if __name__ == .__main__.:",
54
+ "@(abc\\.)?abstractmethod",
55
+ "\\.\\.\\.",
56
+ ]
57
+
58
+ [tool.ruff]
59
+ target-version = "py312"
60
+ line-length = 99
61
+
62
+ [tool.ruff.lint]
63
+ select = ["E", "F", "W", "I", "UP"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,22 @@
1
+ # Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
2
+ # See LICENSE for details.
3
+ """vcti.datanode — lightweight data-plus-attributes node containers.
4
+
5
+ ``DataNode`` pairs data with a metadata/attributes dict; ``LazyDataNode``
6
+ adds on-demand ``load()`` / ``release()`` over a loader callback for
7
+ out-of-core data. Both are tree-agnostic value types usable as node
8
+ payloads or anywhere a data + metadata unit is useful.
9
+ """
10
+
11
+ from importlib.metadata import version
12
+
13
+ from .lazy_node import LazyDataNode
14
+ from .node import DataNode
15
+
16
+ __version__ = version("vcti-datanode")
17
+
18
+ __all__ = [
19
+ "__version__",
20
+ "DataNode",
21
+ "LazyDataNode",
22
+ ]
@@ -0,0 +1,139 @@
1
+ # Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
2
+ # See LICENSE for details.
3
+ """LazyDataNode — DataNode with on-demand loading and release of data.
4
+
5
+ Designed for large datasets (e.g. CAE models) where keeping all data in
6
+ memory at once is impractical. Attributes stay resident; data is loaded via
7
+ a caller-supplied callback and can be released to free memory.
8
+ """
9
+
10
+ from collections.abc import Callable
11
+ from typing import Any
12
+
13
+ from .node import DataNode
14
+
15
+
16
+ class LazyDataNode(DataNode):
17
+ """DataNode with lazy loading and release of data.
18
+
19
+ Extends :class:`DataNode` with a loader callback for on-demand data
20
+ loading. Attributes remain in memory at all times (for filtering and
21
+ bookkeeping); data starts unloaded and is fetched via ``load()`` when
22
+ needed, then can be freed via ``release()`` and reloaded later.
23
+
24
+ The loader is a closure — it captures whatever file handle, offset,
25
+ dataset path, or other context it needs. ``LazyDataNode`` does not know
26
+ or care how the data is stored externally.
27
+
28
+ Equality is the inherited :class:`DataNode` semantics — data plus
29
+ attributes only. The loader and the load-state are **not** part of
30
+ identity, so two unloaded nodes with equal attributes compare equal even
31
+ if their loaders would produce different data.
32
+
33
+ Attributes:
34
+ data: The data (None when unloaded, populated after ``load()``).
35
+ attributes: Dictionary of metadata attributes (always resident).
36
+
37
+ Example:
38
+ >>> import numpy as np
39
+ >>> def make_loader():
40
+ ... return lambda: np.array([1.0, 2.0, 3.0])
41
+ >>> node = LazyDataNode(
42
+ ... loader=make_loader(),
43
+ ... attributes={'units': 'mm', 'name': 'displacement'},
44
+ ... )
45
+ >>> node.is_loaded
46
+ False
47
+ >>> node.load()
48
+ array([1., 2., 3.])
49
+ >>> node.is_loaded
50
+ True
51
+ >>> node.release()
52
+ >>> node.is_loaded
53
+ False
54
+ """
55
+
56
+ __slots__ = ("_loader",)
57
+
58
+ def __init__(
59
+ self,
60
+ loader: Callable[[], Any],
61
+ attributes: dict[str, Any] | None = None,
62
+ *,
63
+ data: Any = None,
64
+ ) -> None:
65
+ """Initialize a LazyDataNode.
66
+
67
+ Args:
68
+ loader: Callable that returns the data when invoked.
69
+ Must be idempotent — multiple calls should return
70
+ equivalent data.
71
+ attributes: Optional metadata dictionary. Defaults to empty dict.
72
+ data: Optional pre-loaded data. When provided, the node
73
+ starts in the loaded state.
74
+ """
75
+ super().__init__(data=data, attributes=attributes)
76
+ self._loader = loader
77
+
78
+ @property
79
+ def is_loaded(self) -> bool:
80
+ """Return True if data is currently in memory."""
81
+ return self.data is not None
82
+
83
+ def load(self) -> Any:
84
+ """Load data via the loader callback.
85
+
86
+ If data is already loaded, returns it without calling the loader
87
+ again. To force a reload, call ``release()`` first.
88
+
89
+ Returns:
90
+ The data.
91
+
92
+ Raises:
93
+ RuntimeError: If the loader returns None.
94
+ Exception: Any exception raised by the loader propagates
95
+ unchanged, preserving its type (e.g. ``FileNotFoundError``)
96
+ so callers can handle distinct failures distinctly.
97
+
98
+ Example:
99
+ >>> import numpy as np
100
+ >>> node = LazyDataNode(loader=lambda: np.array([1, 2, 3]))
101
+ >>> node.is_loaded
102
+ False
103
+ >>> data = node.load()
104
+ >>> node.is_loaded
105
+ True
106
+ """
107
+ if self.data is None:
108
+ result = self._loader()
109
+ if result is None:
110
+ raise RuntimeError("Loader returned None")
111
+ self.data = result
112
+ return self.data
113
+
114
+ def release(self) -> None:
115
+ """Free data from memory.
116
+
117
+ The node keeps its attributes intact; data can be reloaded later via
118
+ ``load()``.
119
+
120
+ Example:
121
+ >>> import numpy as np
122
+ >>> node = LazyDataNode(loader=lambda: np.array([1, 2, 3]))
123
+ >>> node.load() # doctest: +ELLIPSIS
124
+ array(...)
125
+ >>> node.release()
126
+ >>> node.is_loaded
127
+ False
128
+ """
129
+ self.data = None
130
+
131
+ def _data_status(self) -> str:
132
+ if self.data is None:
133
+ return "unloaded"
134
+ return super()._data_status()
135
+
136
+ def _data_detail_lines(self) -> list[str]:
137
+ if self.data is None:
138
+ return [" Data: unloaded"]
139
+ return super()._data_detail_lines()
@@ -0,0 +1,142 @@
1
+ # Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
2
+ # See LICENSE for details.
3
+ """DataNode — a container pairing data with a metadata/attributes dict.
4
+
5
+ A small, dependency-light value type for "a unit of data plus its
6
+ attributes." It is commonly used as a node payload in a tree, but is not
7
+ tied to any tree or container — use it anywhere a data + metadata pair is
8
+ useful.
9
+ """
10
+
11
+ from typing import Any
12
+
13
+ import numpy as np
14
+
15
+
16
+ class DataNode:
17
+ """Container for data and a metadata/attributes dictionary.
18
+
19
+ Each instance can carry:
20
+
21
+ - **Data** — an ``np.ndarray``, a masked array, or any array-like /
22
+ arbitrary object (the field is untyped so it integrates by duck typing
23
+ with other array containers without a hard dependency on them).
24
+ - **Attributes** — a metadata dictionary.
25
+ - Both, or neither.
26
+
27
+ It is intentionally minimal: just ``data`` + ``attributes``, with value
28
+ equality and readable ``repr``/``str``.
29
+
30
+ Attributes:
31
+ data: The data (``np.ndarray``, array-like, arbitrary object, or
32
+ ``None``).
33
+ attributes: Dictionary of metadata attributes.
34
+
35
+ Example:
36
+ >>> import numpy as np
37
+ >>> node = DataNode(
38
+ ... data=np.array([1.0, 2.0, 3.0]),
39
+ ... attributes={'units': 'mm', 'name': 'displacement'},
40
+ ... )
41
+ >>> group = DataNode(attributes={'type': 'results'})
42
+ """
43
+
44
+ __slots__ = ("attributes", "data")
45
+
46
+ def __eq__(self, other: object) -> bool:
47
+ if not isinstance(other, DataNode):
48
+ return NotImplemented
49
+
50
+ # Compare the cheap attributes dict first so an early mismatch
51
+ # short-circuits before any (potentially large) data comparison.
52
+ if self.attributes != other.attributes:
53
+ return False
54
+ return self._data_equals(other.data)
55
+
56
+ def _data_equals(self, other_data: Any) -> bool:
57
+ """Return whether this node's data equals ``other_data``.
58
+
59
+ For ordinary data this does not raise: any pairing of None, ndarrays,
60
+ other array-likes, or plain objects resolves to a bool. (A custom
61
+ object whose own ``__eq__`` raises is the one exception — that
62
+ propagates.) ndarrays compare by shape, dtype, and ``np.array_equal``
63
+ (note: NaN != NaN, so arrays containing NaN are unequal; for masked
64
+ arrays the mask is not consulted — the underlying data is compared).
65
+ A mismatch in kind — one side an ndarray, the other not — is treated
66
+ as unequal rather than coerced.
67
+ """
68
+ a, b = self.data, other_data
69
+ if a is None or b is None:
70
+ return a is None and b is None
71
+
72
+ a_is_array = isinstance(a, np.ndarray)
73
+ b_is_array = isinstance(b, np.ndarray)
74
+ if a_is_array and b_is_array:
75
+ return bool(a.shape == b.shape and a.dtype == b.dtype and np.array_equal(a, b))
76
+ if a_is_array or b_is_array:
77
+ # One ndarray, one not: different kinds, treat as unequal rather
78
+ # than letting an array-valued ``==`` blow up the bool reduction.
79
+ return False
80
+
81
+ result = a == b
82
+ if isinstance(result, bool):
83
+ return result
84
+ # Non-ndarray array-likes whose ``==`` yields a non-scalar: reduce
85
+ # element-wise so we still return a plain, non-raising bool.
86
+ return bool(np.all(result))
87
+
88
+ def __init__(
89
+ self,
90
+ data: Any = None,
91
+ attributes: dict[str, Any] | None = None,
92
+ ):
93
+ """Initialize a DataNode.
94
+
95
+ Args:
96
+ data: Optional data (any array-like object or None).
97
+ attributes: Optional metadata dictionary. Defaults to empty dict.
98
+ """
99
+ self.data = data
100
+ self.attributes = attributes if attributes is not None else {}
101
+
102
+ def _data_status(self) -> str:
103
+ """Return a short description of the data state for repr/str."""
104
+ if self.data is None:
105
+ return "None"
106
+ if isinstance(self.data, np.ndarray):
107
+ return f"shape={self.data.shape}, dtype={self.data.dtype}"
108
+ return f"type={type(self.data).__name__}"
109
+
110
+ def _data_detail_lines(self) -> list[str]:
111
+ """Return detail lines about the data for __str__."""
112
+ if self.data is None:
113
+ return [" Data: None"]
114
+ if isinstance(self.data, np.ndarray):
115
+ return [
116
+ f" Data shape: {self.data.shape}",
117
+ f" Data dtype: {self.data.dtype}",
118
+ ]
119
+ return [f" Data type: {type(self.data).__name__}"]
120
+
121
+ def _attributes_detail_lines(self) -> list[str]:
122
+ """Return detail lines about attributes for __str__."""
123
+ if not self.attributes:
124
+ return [" Attributes: None"]
125
+ lines = [f" Attributes ({len(self.attributes)}):"]
126
+ for key, value in list(self.attributes.items())[:5]:
127
+ value_str = str(value)
128
+ if len(value_str) > 50:
129
+ value_str = value_str[:47] + "..."
130
+ lines.append(f" {key}: {value_str}")
131
+ if len(self.attributes) > 5:
132
+ lines.append(f" ... and {len(self.attributes) - 5} more")
133
+ return lines
134
+
135
+ def __repr__(self) -> str:
136
+ return f"{type(self).__name__}(data={self._data_status()}, attrs={len(self.attributes)})"
137
+
138
+ def __str__(self) -> str:
139
+ parts = [f"{type(self).__name__}:"]
140
+ parts.extend(self._data_detail_lines())
141
+ parts.extend(self._attributes_detail_lines())
142
+ return "\n".join(parts)
File without changes
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.4
2
+ Name: vcti-datanode
3
+ Version: 1.0.1
4
+ Summary: Lightweight data-plus-attributes node containers, with an optional lazily-loaded variant for out-of-core data.
5
+ Author: Visual Collaboration Technologies Inc.
6
+ Requires-Python: <3.15,>=3.12
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: numpy>=1.26
10
+ Provides-Extra: test
11
+ Requires-Dist: pytest; extra == "test"
12
+ Requires-Dist: pytest-cov; extra == "test"
13
+ Provides-Extra: lint
14
+ Requires-Dist: ruff; extra == "lint"
15
+ Provides-Extra: typecheck
16
+ Requires-Dist: mypy; extra == "typecheck"
17
+ Dynamic: license-file
18
+
19
+ # Data Node
20
+
21
+ Lightweight data-plus-attributes node containers, with an optional
22
+ lazily-loaded variant for out-of-core data.
23
+
24
+ ## Overview
25
+
26
+ `vcti-datanode` provides two small, tree-agnostic value types:
27
+
28
+ - **`DataNode`** — pairs `data` (an `np.ndarray`, any array-like, or an
29
+ arbitrary object) with an `attributes` metadata dict. Value equality and
30
+ readable `repr`/`str`. That's it.
31
+ - **`LazyDataNode`** — a `DataNode` whose data is fetched on demand through a
32
+ loader callback (`load()`) and can be freed (`release()`) and reloaded
33
+ later. Attributes stay resident; only the data comes and goes. Built for
34
+ large datasets (e.g. CAE models) where holding everything in memory at once
35
+ is impractical.
36
+
37
+ They are commonly used as **node payloads** in a tree, but depend on nothing
38
+ but numpy and can be used anywhere a "data + metadata" unit is useful.
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install vcti-datanode
44
+ ```
45
+
46
+ ### In `requirements.txt`
47
+
48
+ ```
49
+ vcti-datanode>=1.0.1
50
+ ```
51
+
52
+ ### In `pyproject.toml` dependencies
53
+
54
+ ```toml
55
+ dependencies = [
56
+ "vcti-datanode>=1.0.1",
57
+ ]
58
+ ```
59
+
60
+ ---
61
+
62
+ ## Quick Start
63
+
64
+ ```python
65
+ import numpy as np
66
+ from vcti.datanode import DataNode, LazyDataNode
67
+
68
+ # Eager: data held directly.
69
+ node = DataNode(data=np.array([1.0, 2.0, 3.0]), attributes={"units": "mm"})
70
+
71
+ # Lazy: data fetched on demand, freed when not needed.
72
+ lazy = LazyDataNode(loader=lambda: np.load("displacement.npy"),
73
+ attributes={"name": "displacement"})
74
+ lazy.is_loaded # False
75
+ arr = lazy.load() # loader runs once; cached thereafter
76
+ lazy.release() # frees the data; attributes remain
77
+ ```
78
+
79
+ ---
80
+
81
+ ## Dependencies
82
+
83
+ - `numpy>=1.26` — used for array-aware value equality and repr.
@@ -0,0 +1,16 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/vcti/datanode/__init__.py
5
+ src/vcti/datanode/lazy_node.py
6
+ src/vcti/datanode/node.py
7
+ src/vcti/datanode/py.typed
8
+ src/vcti_datanode.egg-info/PKG-INFO
9
+ src/vcti_datanode.egg-info/SOURCES.txt
10
+ src/vcti_datanode.egg-info/dependency_links.txt
11
+ src/vcti_datanode.egg-info/not-zip-safe
12
+ src/vcti_datanode.egg-info/requires.txt
13
+ src/vcti_datanode.egg-info/top_level.txt
14
+ tests/test_lazy_node.py
15
+ tests/test_node.py
16
+ tests/test_version.py
@@ -0,0 +1,11 @@
1
+ numpy>=1.26
2
+
3
+ [lint]
4
+ ruff
5
+
6
+ [test]
7
+ pytest
8
+ pytest-cov
9
+
10
+ [typecheck]
11
+ mypy
@@ -0,0 +1,157 @@
1
+ # Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
2
+ # See LICENSE for details.
3
+ """Tests for LazyDataNode."""
4
+
5
+ import numpy as np
6
+ import pytest
7
+
8
+ from vcti.datanode import LazyDataNode
9
+
10
+
11
+ class TestConstruction:
12
+ def test_starts_unloaded(self):
13
+ n = LazyDataNode(loader=lambda: np.array([1, 2, 3]))
14
+ assert n.is_loaded is False
15
+ assert n.data is None
16
+
17
+ def test_with_attributes(self):
18
+ n = LazyDataNode(
19
+ loader=lambda: np.array([1, 2, 3]),
20
+ attributes={"units": "mm"},
21
+ )
22
+ assert n.attributes == {"units": "mm"}
23
+
24
+ def test_with_preloaded_data(self):
25
+ arr = np.array([1, 2, 3])
26
+ n = LazyDataNode(loader=lambda: None, data=arr)
27
+ assert n.is_loaded is True
28
+ assert n.data is arr
29
+
30
+
31
+ class TestLoad:
32
+ def test_load_returns_data(self):
33
+ arr = np.array([1, 2, 3])
34
+ n = LazyDataNode(loader=lambda: arr)
35
+ result = n.load()
36
+ assert result is arr
37
+ assert n.is_loaded is True
38
+
39
+ def test_load_idempotent(self):
40
+ calls = [0]
41
+
42
+ def loader():
43
+ calls[0] += 1
44
+ return np.array([1, 2, 3])
45
+
46
+ n = LazyDataNode(loader=loader)
47
+ n.load()
48
+ n.load()
49
+ n.load()
50
+ assert calls[0] == 1 # only called once
51
+
52
+ def test_loader_returning_none_raises(self):
53
+ n = LazyDataNode(loader=lambda: None)
54
+ with pytest.raises(RuntimeError, match="returned None"):
55
+ n.load()
56
+ assert n.is_loaded is False # raised before assignment; stays unloaded
57
+
58
+ def test_loader_exception_propagates_unchanged(self):
59
+ # The loader's own exception type must propagate so callers can
60
+ # handle distinct failures distinctly.
61
+ def bad_loader():
62
+ raise FileNotFoundError("file not found")
63
+
64
+ n = LazyDataNode(loader=bad_loader)
65
+ with pytest.raises(FileNotFoundError, match="file not found"):
66
+ n.load()
67
+
68
+ def test_loader_exception_leaves_node_unloaded(self):
69
+ def bad_loader():
70
+ raise OSError("boom")
71
+
72
+ n = LazyDataNode(loader=bad_loader)
73
+ with pytest.raises(OSError):
74
+ n.load()
75
+ assert n.is_loaded is False
76
+
77
+
78
+ class TestRelease:
79
+ def test_release_clears_data(self):
80
+ n = LazyDataNode(loader=lambda: np.array([1, 2, 3]))
81
+ n.load()
82
+ assert n.is_loaded is True
83
+ n.release()
84
+ assert n.is_loaded is False
85
+ assert n.data is None
86
+
87
+ def test_reload_after_release(self):
88
+ calls = [0]
89
+
90
+ def loader():
91
+ calls[0] += 1
92
+ return np.array([calls[0]])
93
+
94
+ n = LazyDataNode(loader=loader)
95
+ n.load()
96
+ n.release()
97
+ n.load()
98
+ assert calls[0] == 2 # called twice after release
99
+
100
+
101
+ class TestRepr:
102
+ def test_repr_when_unloaded(self):
103
+ n = LazyDataNode(loader=lambda: np.array([1, 2, 3]))
104
+ assert "unloaded" in repr(n)
105
+
106
+ def test_repr_when_loaded(self):
107
+ n = LazyDataNode(loader=lambda: np.array([1, 2, 3]))
108
+ n.load()
109
+ r = repr(n)
110
+ assert "shape" in r or "dtype" in r
111
+
112
+
113
+ class TestStr:
114
+ def test_str_when_unloaded(self):
115
+ n = LazyDataNode(loader=lambda: np.array([1, 2, 3]))
116
+ s = str(n)
117
+ assert "unloaded" in s
118
+
119
+ def test_str_when_loaded(self):
120
+ n = LazyDataNode(loader=lambda: np.array([1, 2, 3]))
121
+ n.load()
122
+ s = str(n)
123
+ assert "Data shape" in s
124
+
125
+
126
+ class TestInheritance:
127
+ def test_is_a_datanode(self):
128
+ from vcti.datanode import DataNode
129
+
130
+ assert isinstance(LazyDataNode(loader=lambda: 1), DataNode)
131
+
132
+ def test_is_unhashable(self):
133
+ # Inherits DataNode's __eq__, so __hash__ is None here too.
134
+ with pytest.raises(TypeError):
135
+ hash(LazyDataNode(loader=lambda: 1))
136
+
137
+
138
+ class TestEquality:
139
+ def test_two_loaded_lazy_nodes_equal(self):
140
+ a = LazyDataNode(loader=lambda: None, data=np.array([1, 2, 3]))
141
+ b = LazyDataNode(loader=lambda: None, data=np.array([1, 2, 3]))
142
+ assert a == b
143
+
144
+ def test_unloaded_lazy_nodes_with_same_attributes_equal(self):
145
+ # Equality uses inherited DataNode semantics (data + attributes);
146
+ # the loader is not part of identity.
147
+ a = LazyDataNode(loader=lambda: np.array([1]), attributes={"u": "mm"})
148
+ b = LazyDataNode(loader=lambda: np.array([2]), attributes={"u": "mm"})
149
+ assert a == b
150
+
151
+ def test_lazy_node_equals_plain_datanode_with_same_payload(self):
152
+ from vcti.datanode import DataNode
153
+
154
+ lazy = LazyDataNode(loader=lambda: None, data=np.array([1, 2, 3]))
155
+ plain = DataNode(data=np.array([1, 2, 3]))
156
+ assert lazy == plain
157
+ assert plain == lazy
@@ -0,0 +1,194 @@
1
+ # Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
2
+ # See LICENSE for details.
3
+ """Tests for DataNode."""
4
+
5
+ import numpy as np
6
+ import pytest
7
+
8
+ from vcti.datanode import DataNode
9
+
10
+
11
+ class TestConstruction:
12
+ def test_empty_defaults(self):
13
+ n = DataNode()
14
+ assert n.data is None
15
+ assert n.attributes == {}
16
+
17
+ def test_with_data(self):
18
+ arr = np.array([1.0, 2.0, 3.0])
19
+ n = DataNode(data=arr)
20
+ assert n.data is arr
21
+ assert n.attributes == {}
22
+
23
+ def test_with_attributes(self):
24
+ n = DataNode(attributes={"units": "mm"})
25
+ assert n.data is None
26
+ assert n.attributes == {"units": "mm"}
27
+
28
+ def test_with_both(self):
29
+ arr = np.array([1, 2])
30
+ n = DataNode(data=arr, attributes={"k": "v"})
31
+ assert n.data is arr
32
+ assert n.attributes == {"k": "v"}
33
+
34
+
35
+ class TestEquality:
36
+ def test_empty_nodes_equal(self):
37
+ assert DataNode() == DataNode()
38
+
39
+ def test_different_attributes_unequal(self):
40
+ assert DataNode(attributes={"a": 1}) != DataNode(attributes={"a": 2})
41
+
42
+ def test_same_data_arrays_equal(self):
43
+ a = DataNode(data=np.array([1, 2, 3]))
44
+ b = DataNode(data=np.array([1, 2, 3]))
45
+ assert a == b
46
+
47
+ def test_different_data_arrays_unequal(self):
48
+ a = DataNode(data=np.array([1, 2, 3]))
49
+ b = DataNode(data=np.array([1, 2, 4]))
50
+ assert a != b
51
+
52
+ def test_different_shape_unequal(self):
53
+ a = DataNode(data=np.array([1, 2, 3]))
54
+ b = DataNode(data=np.array([1, 2]))
55
+ assert a != b
56
+
57
+ def test_one_data_none_unequal(self):
58
+ a = DataNode(data=np.array([1, 2]))
59
+ b = DataNode()
60
+ assert a != b
61
+
62
+ def test_compared_with_non_node_returns_notimplemented(self):
63
+ assert DataNode().__eq__("not a node") is NotImplemented
64
+
65
+ def test_non_array_data_equal(self):
66
+ a = DataNode(data="hello")
67
+ b = DataNode(data="hello")
68
+ assert a == b
69
+
70
+ def test_ndarray_vs_list_unequal_without_raising(self):
71
+ # Mixed kind (ndarray vs list) must compare unequal, never raise.
72
+ a = DataNode(data=np.array([1, 2, 3]))
73
+ b = DataNode(data=[1, 2, 3])
74
+ assert a != b
75
+ assert b != a
76
+
77
+ def test_ndarray_vs_tuple_unequal_without_raising(self):
78
+ a = DataNode(data=np.array([1, 2, 3]))
79
+ b = DataNode(data=(1, 2, 3))
80
+ assert a != b
81
+
82
+ def test_equal_lists_equal(self):
83
+ # Non-ndarray array-likes fall back to == and reduce to a bool.
84
+ assert DataNode(data=[1, 2, 3]) == DataNode(data=[1, 2, 3])
85
+ assert DataNode(data=[1, 2, 3]) != DataNode(data=[1, 2, 4])
86
+
87
+ def test_nan_arrays_unequal(self):
88
+ # np.array_equal treats NaN != NaN, so NaN-bearing arrays are unequal.
89
+ a = DataNode(data=np.array([1.0, np.nan]))
90
+ b = DataNode(data=np.array([1.0, np.nan]))
91
+ assert a != b
92
+
93
+ def test_masked_arrays_compare_by_underlying_data(self):
94
+ a = DataNode(data=np.ma.array([1, 2, 3], mask=[0, 1, 0]))
95
+ b = DataNode(data=np.ma.array([1, 2, 3], mask=[0, 1, 0]))
96
+ assert a == b
97
+
98
+ def test_masked_arrays_ignore_the_mask(self):
99
+ # Equality compares the underlying data only; the mask is NOT
100
+ # consulted (np.array_equal drops it). Same data + different masks
101
+ # therefore compare EQUAL...
102
+ a = DataNode(data=np.ma.array([1, 2, 3], mask=[0, 0, 0]))
103
+ b = DataNode(data=np.ma.array([1, 2, 3], mask=[0, 1, 0]))
104
+ assert a == b
105
+
106
+ def test_masked_arrays_compare_data_under_the_mask(self):
107
+ # ...and data that differs under a masked-out cell still compares
108
+ # UNEQUAL, because the underlying values are what is compared.
109
+ a = DataNode(data=np.ma.array([1, 2, 9], mask=[0, 0, 1]))
110
+ b = DataNode(data=np.ma.array([1, 2, 3], mask=[0, 0, 1]))
111
+ assert a != b
112
+
113
+ def test_attribute_mismatch_short_circuits_before_data(self):
114
+ # Differing attributes => unequal regardless of data.
115
+ a = DataNode(data=np.array([1, 2, 3]), attributes={"a": 1})
116
+ b = DataNode(data=np.array([1, 2, 3]), attributes={"a": 2})
117
+ assert a != b
118
+
119
+ def test_non_ndarray_arraylike_with_vector_eq_reduces_to_bool(self):
120
+ # An array-like (not an ndarray) whose __eq__ returns a non-scalar
121
+ # must still resolve to a plain bool, not raise.
122
+ class ArrayLike:
123
+ def __init__(self, values):
124
+ self.values = values
125
+
126
+ def __eq__(self, other):
127
+ return np.asarray(self.values) == np.asarray(other.values)
128
+
129
+ assert DataNode(data=ArrayLike([1, 2, 3])) == DataNode(data=ArrayLike([1, 2, 3]))
130
+ assert DataNode(data=ArrayLike([1, 2, 3])) != DataNode(data=ArrayLike([1, 2, 4]))
131
+
132
+
133
+ class TestHashing:
134
+ def test_datanode_is_unhashable(self):
135
+ # Defining __eq__ on a mutable container drops __hash__ by design.
136
+ with pytest.raises(TypeError):
137
+ hash(DataNode())
138
+
139
+
140
+ class TestRepr:
141
+ def test_repr_includes_class_name(self):
142
+ n = DataNode()
143
+ assert "DataNode" in repr(n)
144
+
145
+ def test_repr_with_array_data(self):
146
+ n = DataNode(data=np.array([1, 2, 3]))
147
+ r = repr(n)
148
+ assert "shape" in r or "dtype" in r
149
+
150
+ def test_repr_with_non_array_data(self):
151
+ n = DataNode(data="hello")
152
+ assert "type=str" in repr(n)
153
+
154
+
155
+ class TestStr:
156
+ def test_str_includes_class_name(self):
157
+ n = DataNode()
158
+ assert "DataNode" in str(n)
159
+
160
+ def test_str_with_attributes(self):
161
+ n = DataNode(attributes={"name": "x", "units": "mm"})
162
+ s = str(n)
163
+ assert "Attributes" in s
164
+ assert "name" in s
165
+
166
+ def test_str_truncates_long_values(self):
167
+ n = DataNode(attributes={"k": "x" * 200})
168
+ s = str(n)
169
+ # Truncated values end with ellipsis.
170
+ assert "..." in s
171
+
172
+ def test_str_truncates_many_attributes(self):
173
+ attrs = {f"k{i}": i for i in range(10)}
174
+ n = DataNode(attributes=attrs)
175
+ s = str(n)
176
+ assert "more" in s
177
+
178
+ def test_str_with_array_data(self):
179
+ n = DataNode(data=np.array([1, 2, 3]))
180
+ s = str(n)
181
+ assert "Data shape" in s
182
+ assert "Data dtype" in s
183
+
184
+ def test_str_with_non_array_data(self):
185
+ n = DataNode(data="hello")
186
+ s = str(n)
187
+ assert "Data type" in s
188
+
189
+
190
+ class TestSlots:
191
+ def test_no_extra_attributes_allowed(self):
192
+ n = DataNode()
193
+ with pytest.raises(AttributeError):
194
+ n.extra = "x" # type: ignore[attr-defined]
@@ -0,0 +1,15 @@
1
+ # Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
2
+ # See LICENSE for details.
3
+ """Version tests for vcti-datanode."""
4
+
5
+ import re
6
+
7
+ import vcti.datanode
8
+
9
+
10
+ class TestVersion:
11
+ def test_version_exists(self):
12
+ assert hasattr(vcti.datanode, "__version__")
13
+
14
+ def test_version_is_valid_semver(self):
15
+ assert re.match(r"^\d+\.\d+\.\d+", vcti.datanode.__version__)