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.
- vcti_datanode-1.0.1/LICENSE +8 -0
- vcti_datanode-1.0.1/PKG-INFO +83 -0
- vcti_datanode-1.0.1/README.md +65 -0
- vcti_datanode-1.0.1/pyproject.toml +63 -0
- vcti_datanode-1.0.1/setup.cfg +4 -0
- vcti_datanode-1.0.1/src/vcti/datanode/__init__.py +22 -0
- vcti_datanode-1.0.1/src/vcti/datanode/lazy_node.py +139 -0
- vcti_datanode-1.0.1/src/vcti/datanode/node.py +142 -0
- vcti_datanode-1.0.1/src/vcti/datanode/py.typed +0 -0
- vcti_datanode-1.0.1/src/vcti_datanode.egg-info/PKG-INFO +83 -0
- vcti_datanode-1.0.1/src/vcti_datanode.egg-info/SOURCES.txt +16 -0
- vcti_datanode-1.0.1/src/vcti_datanode.egg-info/dependency_links.txt +1 -0
- vcti_datanode-1.0.1/src/vcti_datanode.egg-info/not-zip-safe +1 -0
- vcti_datanode-1.0.1/src/vcti_datanode.egg-info/requires.txt +11 -0
- vcti_datanode-1.0.1/src/vcti_datanode.egg-info/top_level.txt +1 -0
- vcti_datanode-1.0.1/tests/test_lazy_node.py +157 -0
- vcti_datanode-1.0.1/tests/test_node.py +194 -0
- vcti_datanode-1.0.1/tests/test_version.py +15 -0
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
vcti
|
|
@@ -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__)
|