vcti-tree 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vcti_tree-1.0.0/LICENSE +8 -0
- vcti_tree-1.0.0/PKG-INFO +122 -0
- vcti_tree-1.0.0/README.md +92 -0
- vcti_tree-1.0.0/pyproject.toml +77 -0
- vcti_tree-1.0.0/setup.cfg +4 -0
- vcti_tree-1.0.0/src/vcti/tree/__init__.py +41 -0
- vcti_tree-1.0.0/src/vcti/tree/dict_tree.py +240 -0
- vcti_tree-1.0.0/src/vcti/tree/locking.py +78 -0
- vcti_tree-1.0.0/src/vcti/tree/navigation.py +57 -0
- vcti_tree-1.0.0/src/vcti/tree/protocols.py +180 -0
- vcti_tree-1.0.0/src/vcti/tree/py.typed +0 -0
- vcti_tree-1.0.0/src/vcti/tree/walking.py +74 -0
- vcti_tree-1.0.0/src/vcti_tree.egg-info/PKG-INFO +122 -0
- vcti_tree-1.0.0/src/vcti_tree.egg-info/SOURCES.txt +22 -0
- vcti_tree-1.0.0/src/vcti_tree.egg-info/dependency_links.txt +1 -0
- vcti_tree-1.0.0/src/vcti_tree.egg-info/requires.txt +10 -0
- vcti_tree-1.0.0/src/vcti_tree.egg-info/top_level.txt +1 -0
- vcti_tree-1.0.0/src/vcti_tree.egg-info/zip-safe +1 -0
- vcti_tree-1.0.0/tests/test_dict_tree.py +435 -0
- vcti_tree-1.0.0/tests/test_locking.py +77 -0
- vcti_tree-1.0.0/tests/test_navigation.py +84 -0
- vcti_tree-1.0.0/tests/test_protocols.py +38 -0
- vcti_tree-1.0.0/tests/test_version.py +15 -0
- vcti_tree-1.0.0/tests/test_walking.py +68 -0
vcti_tree-1.0.0/LICENSE
ADDED
|
@@ -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.
|
vcti_tree-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vcti-tree
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Tree protocols and generic algorithms for the vcti ecosystem. Zero-dependency abstractions; pluggable backings.
|
|
5
|
+
Author: Visual Collaboration Technologies Inc.
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Project-URL: Homepage, https://github.com/vcollab/vcti-python-tree
|
|
8
|
+
Project-URL: Repository, https://github.com/vcollab/vcti-python-tree
|
|
9
|
+
Project-URL: Changelog, https://github.com/vcollab/vcti-python-tree/blob/main/CHANGELOG.md
|
|
10
|
+
Keywords: tree,protocol,traversal,dfs,bfs,data-structure,generic-algorithms
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: <3.15,>=3.12
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
Requires-Dist: pytest; extra == "test"
|
|
24
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
25
|
+
Provides-Extra: lint
|
|
26
|
+
Requires-Dist: ruff; extra == "lint"
|
|
27
|
+
Provides-Extra: typecheck
|
|
28
|
+
Requires-Dist: mypy; extra == "typecheck"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# vcti-tree
|
|
32
|
+
|
|
33
|
+
Tree protocols and generic algorithms for the vcti ecosystem.
|
|
34
|
+
Zero-dependency abstractions; pluggable backings.
|
|
35
|
+
|
|
36
|
+
## Overview
|
|
37
|
+
|
|
38
|
+
`vcti-tree` defines what a tree is in the vcti ecosystem: three
|
|
39
|
+
Protocols (`ReadOnlyTree`, `MutableTree`, `LockableTree`)
|
|
40
|
+
parameterised in payload type `T` and handle type `H`, plus generic
|
|
41
|
+
algorithms — DFS and BFS walks, ancestor/depth/path queries,
|
|
42
|
+
locking guards — written against those Protocols. The Protocols
|
|
43
|
+
specify *semantics* (what a tree must support); they do not
|
|
44
|
+
prescribe storage. Any backing — numpy-array, pointer-based,
|
|
45
|
+
dict-based — that satisfies the Protocols gets the algorithms for
|
|
46
|
+
free. The package has no numpy or other heavy dependency; it is a
|
|
47
|
+
pure-Python abstraction layer that downstream tree packages depend
|
|
48
|
+
on.
|
|
49
|
+
|
|
50
|
+
## Why this exists
|
|
51
|
+
|
|
52
|
+
Tree code in the Python ecosystem typically bundles storage,
|
|
53
|
+
mutation, and traversal into a single class — every new tree
|
|
54
|
+
implementation re-implements its own walker, its own navigation
|
|
55
|
+
helpers, its own delete semantics. This package brings across a
|
|
56
|
+
pattern that's well-established in C++: a small abstract interface
|
|
57
|
+
defining what a tree *is*, multiple concrete implementations
|
|
58
|
+
choosing their own storage, and generic algorithms written once
|
|
59
|
+
against the interface and reused across every backing. The result
|
|
60
|
+
is that adding a new tree implementation (numpy-backed,
|
|
61
|
+
pointer-backed, persistent, …) doesn't mean rewriting walkers and
|
|
62
|
+
guards — it means satisfying three small Protocols and inheriting
|
|
63
|
+
the entire algorithm library. See [docs/design.md](docs/design.md)
|
|
64
|
+
for the full background and the design decisions.
|
|
65
|
+
|
|
66
|
+
## Installation
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install vcti-tree
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### In `requirements.txt`
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
vcti-tree>=1.0.0
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### In `pyproject.toml` dependencies
|
|
79
|
+
|
|
80
|
+
```toml
|
|
81
|
+
dependencies = [
|
|
82
|
+
"vcti-tree>=1.0.0",
|
|
83
|
+
]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Quick Start
|
|
89
|
+
|
|
90
|
+
The protocols and algorithms are the API; you bring (or import) a
|
|
91
|
+
backing. `DictTree` is the bundled dict-backed reference
|
|
92
|
+
implementation — enough to build a tree and run the generic algorithms
|
|
93
|
+
against it:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from vcti.tree import DictTree, dfs_preorder, ancestors, depth
|
|
97
|
+
|
|
98
|
+
tree = DictTree[str]("root") # every tree has a root from the start
|
|
99
|
+
a = tree.create_child(tree.root_handle, "a")
|
|
100
|
+
b = tree.create_child(tree.root_handle, "b")
|
|
101
|
+
tree.create_child(a, "a1")
|
|
102
|
+
|
|
103
|
+
# Generic walks/queries work against any tree that satisfies the protocols.
|
|
104
|
+
print([tree.payload(h) for h in dfs_preorder(tree)]) # ['root', 'a', 'a1', 'b']
|
|
105
|
+
print([tree.payload(h) for h in ancestors(tree, a)]) # ['root']
|
|
106
|
+
print(depth(tree, a)) # 1
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The protocols (`ReadOnlyTree`, `MutableTree`, `LockableTree`) and the
|
|
110
|
+
rest of the algorithm set are importable from the same package root:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from vcti.tree import ReadOnlyTree, MutableTree, LockableTree
|
|
114
|
+
from vcti.tree import dfs_preorder, dfs_postorder, bfs
|
|
115
|
+
from vcti.tree import ancestors, depth, path, descendants
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Dependencies
|
|
121
|
+
|
|
122
|
+
None. Standard library only.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# vcti-tree
|
|
2
|
+
|
|
3
|
+
Tree protocols and generic algorithms for the vcti ecosystem.
|
|
4
|
+
Zero-dependency abstractions; pluggable backings.
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
`vcti-tree` defines what a tree is in the vcti ecosystem: three
|
|
9
|
+
Protocols (`ReadOnlyTree`, `MutableTree`, `LockableTree`)
|
|
10
|
+
parameterised in payload type `T` and handle type `H`, plus generic
|
|
11
|
+
algorithms — DFS and BFS walks, ancestor/depth/path queries,
|
|
12
|
+
locking guards — written against those Protocols. The Protocols
|
|
13
|
+
specify *semantics* (what a tree must support); they do not
|
|
14
|
+
prescribe storage. Any backing — numpy-array, pointer-based,
|
|
15
|
+
dict-based — that satisfies the Protocols gets the algorithms for
|
|
16
|
+
free. The package has no numpy or other heavy dependency; it is a
|
|
17
|
+
pure-Python abstraction layer that downstream tree packages depend
|
|
18
|
+
on.
|
|
19
|
+
|
|
20
|
+
## Why this exists
|
|
21
|
+
|
|
22
|
+
Tree code in the Python ecosystem typically bundles storage,
|
|
23
|
+
mutation, and traversal into a single class — every new tree
|
|
24
|
+
implementation re-implements its own walker, its own navigation
|
|
25
|
+
helpers, its own delete semantics. This package brings across a
|
|
26
|
+
pattern that's well-established in C++: a small abstract interface
|
|
27
|
+
defining what a tree *is*, multiple concrete implementations
|
|
28
|
+
choosing their own storage, and generic algorithms written once
|
|
29
|
+
against the interface and reused across every backing. The result
|
|
30
|
+
is that adding a new tree implementation (numpy-backed,
|
|
31
|
+
pointer-backed, persistent, …) doesn't mean rewriting walkers and
|
|
32
|
+
guards — it means satisfying three small Protocols and inheriting
|
|
33
|
+
the entire algorithm library. See [docs/design.md](docs/design.md)
|
|
34
|
+
for the full background and the design decisions.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install vcti-tree
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### In `requirements.txt`
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
vcti-tree>=1.0.0
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### In `pyproject.toml` dependencies
|
|
49
|
+
|
|
50
|
+
```toml
|
|
51
|
+
dependencies = [
|
|
52
|
+
"vcti-tree>=1.0.0",
|
|
53
|
+
]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
The protocols and algorithms are the API; you bring (or import) a
|
|
61
|
+
backing. `DictTree` is the bundled dict-backed reference
|
|
62
|
+
implementation — enough to build a tree and run the generic algorithms
|
|
63
|
+
against it:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from vcti.tree import DictTree, dfs_preorder, ancestors, depth
|
|
67
|
+
|
|
68
|
+
tree = DictTree[str]("root") # every tree has a root from the start
|
|
69
|
+
a = tree.create_child(tree.root_handle, "a")
|
|
70
|
+
b = tree.create_child(tree.root_handle, "b")
|
|
71
|
+
tree.create_child(a, "a1")
|
|
72
|
+
|
|
73
|
+
# Generic walks/queries work against any tree that satisfies the protocols.
|
|
74
|
+
print([tree.payload(h) for h in dfs_preorder(tree)]) # ['root', 'a', 'a1', 'b']
|
|
75
|
+
print([tree.payload(h) for h in ancestors(tree, a)]) # ['root']
|
|
76
|
+
print(depth(tree, a)) # 1
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The protocols (`ReadOnlyTree`, `MutableTree`, `LockableTree`) and the
|
|
80
|
+
rest of the algorithm set are importable from the same package root:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from vcti.tree import ReadOnlyTree, MutableTree, LockableTree
|
|
84
|
+
from vcti.tree import dfs_preorder, dfs_postorder, bfs
|
|
85
|
+
from vcti.tree import ancestors, depth, path, descendants
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Dependencies
|
|
91
|
+
|
|
92
|
+
None. Standard library only.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vcti-tree"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Tree protocols and generic algorithms for the vcti ecosystem. Zero-dependency abstractions; pluggable backings."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{name = "Visual Collaboration Technologies Inc."}
|
|
12
|
+
]
|
|
13
|
+
requires-python = ">=3.12,<3.15"
|
|
14
|
+
license = "LicenseRef-Proprietary"
|
|
15
|
+
license-files = ["LICENSE"]
|
|
16
|
+
keywords = ["tree", "protocol", "traversal", "dfs", "bfs", "data-structure", "generic-algorithms"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 5 - Production/Stable",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
dependencies = []
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/vcollab/vcti-python-tree"
|
|
31
|
+
Repository = "https://github.com/vcollab/vcti-python-tree"
|
|
32
|
+
Changelog = "https://github.com/vcollab/vcti-python-tree/blob/main/CHANGELOG.md"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.packages.find]
|
|
35
|
+
where = ["src"]
|
|
36
|
+
include = ["vcti.tree", "vcti.tree.*"]
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.package-data]
|
|
39
|
+
"vcti.tree" = ["py.typed"]
|
|
40
|
+
|
|
41
|
+
[project.optional-dependencies]
|
|
42
|
+
test = ["pytest", "pytest-cov"]
|
|
43
|
+
lint = ["ruff"]
|
|
44
|
+
typecheck = ["mypy"]
|
|
45
|
+
|
|
46
|
+
[tool.setuptools]
|
|
47
|
+
zip-safe = true
|
|
48
|
+
|
|
49
|
+
[tool.pytest.ini_options]
|
|
50
|
+
addopts = "--cov=vcti.tree --cov-report=term-missing --cov-fail-under=95"
|
|
51
|
+
|
|
52
|
+
[tool.mypy]
|
|
53
|
+
python_version = "3.12"
|
|
54
|
+
strict = true
|
|
55
|
+
files = ["src"]
|
|
56
|
+
namespace_packages = true
|
|
57
|
+
explicit_package_bases = true
|
|
58
|
+
mypy_path = ["src"]
|
|
59
|
+
|
|
60
|
+
[tool.coverage.run]
|
|
61
|
+
branch = true
|
|
62
|
+
|
|
63
|
+
[tool.coverage.report]
|
|
64
|
+
exclude_also = [
|
|
65
|
+
"raise NotImplementedError",
|
|
66
|
+
"if TYPE_CHECKING:",
|
|
67
|
+
"if __name__ == .__main__.:",
|
|
68
|
+
"@(abc\\.)?abstractmethod",
|
|
69
|
+
"\\.\\.\\.",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
[tool.ruff]
|
|
73
|
+
target-version = "py312"
|
|
74
|
+
line-length = 99
|
|
75
|
+
|
|
76
|
+
[tool.ruff.lint]
|
|
77
|
+
select = ["E", "F", "W", "I", "UP"]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""vcti.tree — Tree protocols and generic algorithms for the vcti ecosystem."""
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import version
|
|
6
|
+
|
|
7
|
+
from .dict_tree import DictTree, DictTreeHandle
|
|
8
|
+
from .locking import (
|
|
9
|
+
collect_deletable_subtree,
|
|
10
|
+
ensure_payload_unlocked,
|
|
11
|
+
ensure_structure_unlocked,
|
|
12
|
+
)
|
|
13
|
+
from .navigation import ancestors, depth, descendants, path
|
|
14
|
+
from .protocols import LockableTree, MutableTree, ReadOnlyTree
|
|
15
|
+
from .walking import bfs, dfs_postorder, dfs_preorder
|
|
16
|
+
|
|
17
|
+
__version__ = version("vcti-tree")
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"__version__",
|
|
21
|
+
# Protocols
|
|
22
|
+
"ReadOnlyTree",
|
|
23
|
+
"MutableTree",
|
|
24
|
+
"LockableTree",
|
|
25
|
+
# Walking
|
|
26
|
+
"dfs_preorder",
|
|
27
|
+
"dfs_postorder",
|
|
28
|
+
"bfs",
|
|
29
|
+
# Navigation
|
|
30
|
+
"ancestors",
|
|
31
|
+
"depth",
|
|
32
|
+
"path",
|
|
33
|
+
"descendants",
|
|
34
|
+
# Locking guards
|
|
35
|
+
"collect_deletable_subtree",
|
|
36
|
+
"ensure_structure_unlocked",
|
|
37
|
+
"ensure_payload_unlocked",
|
|
38
|
+
# Reference implementation
|
|
39
|
+
"DictTree",
|
|
40
|
+
"DictTreeHandle",
|
|
41
|
+
]
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""DictTree — minimal dict-backed reference implementation of LockableTree.
|
|
4
|
+
|
|
5
|
+
A simple in-memory tree backed by a Python dict of node records, used
|
|
6
|
+
internally for testing the generic algorithms in
|
|
7
|
+
:mod:`vcti.tree.walking`, :mod:`vcti.tree.navigation`, and
|
|
8
|
+
:mod:`vcti.tree.locking` without pulling in numpy or other heavy
|
|
9
|
+
dependencies. Also serves as a working example of how to implement
|
|
10
|
+
:class:`~vcti.tree.protocols.LockableTree` from scratch.
|
|
11
|
+
|
|
12
|
+
This is **not** the recommended production implementation — for
|
|
13
|
+
numpy-backed storage at file-structure-tree scale, use
|
|
14
|
+
``vcti-nptree``'s ``ArrayTree``.
|
|
15
|
+
|
|
16
|
+
Handle representation
|
|
17
|
+
---------------------
|
|
18
|
+
|
|
19
|
+
Handles are ``DictTreeHandle(tree_id, key)`` named tuples:
|
|
20
|
+
|
|
21
|
+
- ``tree_id`` is a process-global counter incremented on each
|
|
22
|
+
``DictTree`` construction, so handles from different trees compare
|
|
23
|
+
unequal (the cross-tree inequality requirement of the protocol).
|
|
24
|
+
- ``key`` is an integer slot key auto-generated inside the tree.
|
|
25
|
+
|
|
26
|
+
The tuple wrapping is the minimum needed to satisfy cross-tree
|
|
27
|
+
inequality on top of bare keys.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from collections.abc import Hashable, Iterable, Iterator
|
|
31
|
+
from dataclasses import dataclass, field
|
|
32
|
+
from itertools import count
|
|
33
|
+
from typing import NamedTuple
|
|
34
|
+
|
|
35
|
+
from .locking import collect_deletable_subtree
|
|
36
|
+
|
|
37
|
+
# Process-global counter; one tag per DictTree instance.
|
|
38
|
+
_tree_id_counter = count(1)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class DictTreeHandle(NamedTuple):
|
|
42
|
+
"""Opaque handle to a node in a :class:`DictTree`."""
|
|
43
|
+
|
|
44
|
+
tree_id: int
|
|
45
|
+
key: int
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class _NodeRecord[T]:
|
|
50
|
+
"""Internal storage for one node."""
|
|
51
|
+
|
|
52
|
+
parent: int | None # parent key; None for root
|
|
53
|
+
children: list[int] = field(default_factory=list)
|
|
54
|
+
payload: T | None = None
|
|
55
|
+
active: bool = True
|
|
56
|
+
structure_locked: bool = False
|
|
57
|
+
payload_locked: bool = False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class DictTree[T]:
|
|
61
|
+
"""Dict-backed reference implementation of
|
|
62
|
+
:class:`~vcti.tree.protocols.LockableTree`.
|
|
63
|
+
|
|
64
|
+
Satisfies ``LockableTree[T, DictTreeHandle]`` via the structural-
|
|
65
|
+
typing protocol. Soft-delete model (matches the ``ArrayTree``
|
|
66
|
+
semantics): :meth:`delete_subtree` marks nodes inactive but leaves
|
|
67
|
+
them in the parent's children list; ``children()`` filters them out.
|
|
68
|
+
|
|
69
|
+
Locks are flat bitsets propagated across the whole active subtree
|
|
70
|
+
on each ``lock_*`` / ``unlock_*`` call (no refcount).
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, root_data: T | None = None) -> None:
|
|
74
|
+
self._tree_id: int = next(_tree_id_counter)
|
|
75
|
+
self._next_key: int = 0
|
|
76
|
+
self._records: dict[int, _NodeRecord[T]] = {}
|
|
77
|
+
self._active_count: int = 0
|
|
78
|
+
# Allocate root at key 0 with parent=None.
|
|
79
|
+
root_key = self._allocate(parent=None, data=root_data)
|
|
80
|
+
self._root_key: int = root_key
|
|
81
|
+
|
|
82
|
+
# ---- handle (un)wrapping ----
|
|
83
|
+
|
|
84
|
+
def _unwrap(self, handle: DictTreeHandle) -> int:
|
|
85
|
+
if handle.tree_id != self._tree_id:
|
|
86
|
+
raise LookupError(
|
|
87
|
+
f"Handle {handle} is from a different tree (this tree's id={self._tree_id})"
|
|
88
|
+
)
|
|
89
|
+
if handle.key not in self._records:
|
|
90
|
+
raise KeyError(f"Handle {handle} refers to a key that was never allocated")
|
|
91
|
+
return handle.key
|
|
92
|
+
|
|
93
|
+
def _wrap(self, key: int) -> DictTreeHandle:
|
|
94
|
+
return DictTreeHandle(self._tree_id, key)
|
|
95
|
+
|
|
96
|
+
def _require_active(self, handle: DictTreeHandle) -> tuple[int, "_NodeRecord[T]"]:
|
|
97
|
+
"""Unwrap ``handle`` and return ``(key, record)`` for a live node.
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
LookupError: handle is from a different tree.
|
|
101
|
+
KeyError: handle's key was never allocated.
|
|
102
|
+
ValueError: the node has been soft-deleted.
|
|
103
|
+
"""
|
|
104
|
+
key = self._unwrap(handle)
|
|
105
|
+
rec = self._records[key]
|
|
106
|
+
if not rec.active:
|
|
107
|
+
raise ValueError(f"Node {handle} has been deleted")
|
|
108
|
+
return key, rec
|
|
109
|
+
|
|
110
|
+
# ---- internal allocation ----
|
|
111
|
+
|
|
112
|
+
def _allocate(self, parent: int | None, data: T | None) -> int:
|
|
113
|
+
key = self._next_key
|
|
114
|
+
self._next_key += 1
|
|
115
|
+
self._records[key] = _NodeRecord(parent=parent, payload=data)
|
|
116
|
+
self._active_count += 1
|
|
117
|
+
return key
|
|
118
|
+
|
|
119
|
+
# ---- ReadOnlyTree ----
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def root_handle(self) -> DictTreeHandle:
|
|
123
|
+
return self._wrap(self._root_key)
|
|
124
|
+
|
|
125
|
+
def payload(self, handle: DictTreeHandle) -> T | None:
|
|
126
|
+
_, rec = self._require_active(handle)
|
|
127
|
+
return rec.payload
|
|
128
|
+
|
|
129
|
+
def parent(self, handle: DictTreeHandle) -> DictTreeHandle | None:
|
|
130
|
+
_, rec = self._require_active(handle)
|
|
131
|
+
return None if rec.parent is None else self._wrap(rec.parent)
|
|
132
|
+
|
|
133
|
+
def children(self, handle: DictTreeHandle) -> Iterable[DictTreeHandle]:
|
|
134
|
+
_, rec = self._require_active(handle)
|
|
135
|
+
# Walk the full child list, skip inactive — do not terminate early.
|
|
136
|
+
return [
|
|
137
|
+
self._wrap(child_key) for child_key in rec.children if self._records[child_key].active
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
def __contains__(self, handle: object) -> bool:
|
|
141
|
+
if not isinstance(handle, DictTreeHandle):
|
|
142
|
+
return False
|
|
143
|
+
if handle.tree_id != self._tree_id:
|
|
144
|
+
return False
|
|
145
|
+
rec = self._records.get(handle.key)
|
|
146
|
+
return rec is not None and rec.active
|
|
147
|
+
|
|
148
|
+
def __iter__(self) -> Iterator[DictTreeHandle]:
|
|
149
|
+
for key, rec in self._records.items():
|
|
150
|
+
if rec.active:
|
|
151
|
+
yield self._wrap(key)
|
|
152
|
+
|
|
153
|
+
def __len__(self) -> int:
|
|
154
|
+
return self._active_count
|
|
155
|
+
|
|
156
|
+
# ---- MutableTree ----
|
|
157
|
+
|
|
158
|
+
def set_payload(self, handle: DictTreeHandle, data: T | None) -> None:
|
|
159
|
+
_, rec = self._require_active(handle)
|
|
160
|
+
if rec.payload_locked:
|
|
161
|
+
raise RuntimeError(f"Node {handle} is payload-locked")
|
|
162
|
+
rec.payload = data
|
|
163
|
+
|
|
164
|
+
def create_child(self, parent: DictTreeHandle, data: T | None = None) -> DictTreeHandle:
|
|
165
|
+
parent_key, parent_rec = self._require_active(parent)
|
|
166
|
+
if parent_rec.structure_locked:
|
|
167
|
+
raise RuntimeError(f"Parent {parent} is structure-locked")
|
|
168
|
+
child_key = self._allocate(parent=parent_key, data=data)
|
|
169
|
+
parent_rec.children.append(child_key)
|
|
170
|
+
return self._wrap(child_key)
|
|
171
|
+
|
|
172
|
+
def delete_subtree(self, handle: DictTreeHandle) -> None:
|
|
173
|
+
_, rec = self._require_active(handle)
|
|
174
|
+
if rec.parent is None:
|
|
175
|
+
raise RuntimeError("Cannot delete the root")
|
|
176
|
+
# Fail-fast collect via the shared protocol-level helper (raises if
|
|
177
|
+
# any node in the subtree is structure-locked), then deactivate the
|
|
178
|
+
# whole set atomically — no partial deletion on a locked subtree.
|
|
179
|
+
to_delete = collect_deletable_subtree(self, handle)
|
|
180
|
+
for h in to_delete:
|
|
181
|
+
self._records[h.key].active = False
|
|
182
|
+
self._active_count -= len(to_delete)
|
|
183
|
+
|
|
184
|
+
# ---- LockableTree ----
|
|
185
|
+
|
|
186
|
+
def is_structure_locked(self, handle: DictTreeHandle) -> bool:
|
|
187
|
+
_, rec = self._require_active(handle)
|
|
188
|
+
return rec.structure_locked
|
|
189
|
+
|
|
190
|
+
def is_payload_locked(self, handle: DictTreeHandle) -> bool:
|
|
191
|
+
_, rec = self._require_active(handle)
|
|
192
|
+
return rec.payload_locked
|
|
193
|
+
|
|
194
|
+
def lock_structure(self, handle: DictTreeHandle) -> None:
|
|
195
|
+
self._apply_flag(handle, "structure_locked", True)
|
|
196
|
+
|
|
197
|
+
def unlock_structure(self, handle: DictTreeHandle) -> None:
|
|
198
|
+
self._apply_flag(handle, "structure_locked", False)
|
|
199
|
+
|
|
200
|
+
def lock_payload(self, handle: DictTreeHandle) -> None:
|
|
201
|
+
self._apply_flag(handle, "payload_locked", True)
|
|
202
|
+
|
|
203
|
+
def unlock_payload(self, handle: DictTreeHandle) -> None:
|
|
204
|
+
self._apply_flag(handle, "payload_locked", False)
|
|
205
|
+
|
|
206
|
+
def _apply_flag(self, handle: DictTreeHandle, attr: str, value: bool) -> None:
|
|
207
|
+
"""Set ``attr=value`` on ``handle`` and all active descendants.
|
|
208
|
+
|
|
209
|
+
Raises if ``handle`` itself is not a live node (so lock/unlock on a
|
|
210
|
+
deleted handle fails loudly, consistent with the rest of the surface
|
|
211
|
+
rather than silently no-op'ing). Inactive *descendants* are skipped:
|
|
212
|
+
propagation covers the active subtree only.
|
|
213
|
+
"""
|
|
214
|
+
root_key, _ = self._require_active(handle)
|
|
215
|
+
# Walk the subtree skipping inactive nodes; full child-chain walk.
|
|
216
|
+
stack: list[int] = [root_key]
|
|
217
|
+
while stack:
|
|
218
|
+
k = stack.pop()
|
|
219
|
+
rec = self._records[k]
|
|
220
|
+
if not rec.active:
|
|
221
|
+
continue
|
|
222
|
+
setattr(rec, attr, value)
|
|
223
|
+
for child_key in reversed(rec.children):
|
|
224
|
+
stack.append(child_key)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# Static structural conformance check (mypy will see this).
|
|
228
|
+
def _check_protocol_conformance() -> None: # pragma: no cover
|
|
229
|
+
from .protocols import LockableTree, MutableTree, ReadOnlyTree
|
|
230
|
+
|
|
231
|
+
tree: DictTree[str] = DictTree("root")
|
|
232
|
+
_ro: ReadOnlyTree[str, DictTreeHandle] = tree
|
|
233
|
+
_mu: MutableTree[str, DictTreeHandle] = tree
|
|
234
|
+
_lo: LockableTree[str, DictTreeHandle] = tree
|
|
235
|
+
_ = _ro, _mu, _lo
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _is_hashable_handle() -> None: # pragma: no cover
|
|
239
|
+
# Sanity: DictTreeHandle must satisfy H: Hashable bound.
|
|
240
|
+
_: Hashable = DictTreeHandle(1, 0)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""Locking guards over any :class:`LockableTree`.
|
|
4
|
+
|
|
5
|
+
Two complementary helpers:
|
|
6
|
+
|
|
7
|
+
- :func:`collect_deletable_subtree` is the fused common-case for
|
|
8
|
+
``delete_subtree`` implementations — single-pass walk + structure-lock
|
|
9
|
+
check, fail-fast on the first locked node.
|
|
10
|
+
- :func:`ensure_structure_unlocked` and :func:`ensure_payload_unlocked`
|
|
11
|
+
are predicate-only forms for arbitrary handle iterables (e.g.,
|
|
12
|
+
user-supplied selections) that don't need subtree walking.
|
|
13
|
+
|
|
14
|
+
All three operate against the :class:`LockableTree` protocol — never
|
|
15
|
+
against any concrete implementation.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from collections.abc import Hashable, Iterable
|
|
19
|
+
|
|
20
|
+
from .protocols import LockableTree
|
|
21
|
+
from .walking import dfs_preorder
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def collect_deletable_subtree[T, H: Hashable](tree: LockableTree[T, H], root_handle: H) -> list[H]:
|
|
25
|
+
"""Walk the subtree at ``root_handle`` DFS pre-order and verify
|
|
26
|
+
no node is structure-locked.
|
|
27
|
+
|
|
28
|
+
Returns the list of handles in DFS pre-order; callers can then
|
|
29
|
+
deactivate them atomically without rolling back.
|
|
30
|
+
|
|
31
|
+
Intentionally fused (walks + validates in a single pass) rather
|
|
32
|
+
than composing :func:`descendants` + :func:`ensure_structure_unlocked`,
|
|
33
|
+
so that an early-locked node fails fast without materialising the
|
|
34
|
+
rest of the subtree.
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
RuntimeError: If any node in the subtree is structure-locked.
|
|
38
|
+
"""
|
|
39
|
+
nodes: list[H] = []
|
|
40
|
+
for h in dfs_preorder(tree, root_handle):
|
|
41
|
+
if tree.is_structure_locked(h):
|
|
42
|
+
raise RuntimeError(
|
|
43
|
+
f"Cannot delete subtree at {root_handle}: node {h} is structure-locked"
|
|
44
|
+
)
|
|
45
|
+
nodes.append(h)
|
|
46
|
+
return nodes
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def ensure_structure_unlocked[T, H: Hashable](
|
|
50
|
+
tree: LockableTree[T, H], handles: Iterable[H]
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Raise on the first structure-locked handle in ``handles``.
|
|
53
|
+
|
|
54
|
+
Use this when the caller already has a handle list (e.g., a
|
|
55
|
+
user-supplied selection or a result from a separate walk) and
|
|
56
|
+
doesn't need subtree collection. For the subtree-delete common
|
|
57
|
+
case, prefer :func:`collect_deletable_subtree` which does both
|
|
58
|
+
in a single pass.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
RuntimeError: On the first structure-locked handle.
|
|
62
|
+
"""
|
|
63
|
+
for h in handles:
|
|
64
|
+
if tree.is_structure_locked(h):
|
|
65
|
+
raise RuntimeError(f"Node {h} is structure-locked")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def ensure_payload_unlocked[T, H: Hashable](
|
|
69
|
+
tree: LockableTree[T, H], handles: Iterable[H]
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Raise on the first payload-locked handle in ``handles``.
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
RuntimeError: On the first payload-locked handle.
|
|
75
|
+
"""
|
|
76
|
+
for h in handles:
|
|
77
|
+
if tree.is_payload_locked(h):
|
|
78
|
+
raise RuntimeError(f"Node {h} is payload-locked")
|