vcti-tree 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
vcti/tree/__init__.py ADDED
@@ -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
+ ]
vcti/tree/dict_tree.py ADDED
@@ -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)
vcti/tree/locking.py ADDED
@@ -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")
@@ -0,0 +1,57 @@
1
+ # Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
2
+ # See LICENSE for details.
3
+ """Relationship-oriented navigation helpers over any :class:`ReadOnlyTree`.
4
+
5
+ These are the "answers" layer: given a handle, return its ancestors,
6
+ depth, path, or descendants as concrete handles. The iterator-oriented
7
+ walks live in :mod:`vcti.tree.walking`; this module composes them with
8
+ the parent-chain query exposed by the protocol.
9
+ """
10
+
11
+ from collections.abc import Hashable, Iterator
12
+
13
+ from .protocols import ReadOnlyTree
14
+ from .walking import dfs_preorder
15
+
16
+
17
+ def ancestors[T, H: Hashable](tree: ReadOnlyTree[T, H], handle: H) -> Iterator[H]:
18
+ """Yield ancestors of ``handle`` from parent up to (and including) root.
19
+
20
+ Yields nothing if ``handle`` is the root.
21
+ """
22
+ nid = tree.parent(handle)
23
+ while nid is not None:
24
+ yield nid
25
+ nid = tree.parent(nid)
26
+
27
+
28
+ def depth[T, H: Hashable](tree: ReadOnlyTree[T, H], handle: H) -> int:
29
+ """Distance from ``handle`` to the root. ``depth(root_handle) == 0``."""
30
+ return sum(1 for _ in ancestors(tree, handle))
31
+
32
+
33
+ def path[T, H: Hashable](tree: ReadOnlyTree[T, H], handle: H) -> list[H]:
34
+ """List of handles from root to ``handle`` (inclusive).
35
+
36
+ For the root, returns ``[root_handle]``.
37
+ """
38
+ result = [handle]
39
+ result.extend(ancestors(tree, handle))
40
+ result.reverse()
41
+ return result
42
+
43
+
44
+ def descendants[T, H: Hashable](
45
+ tree: ReadOnlyTree[T, H], handle: H, *, include_self: bool = False
46
+ ) -> Iterator[H]:
47
+ """DFS pre-order descendants of ``handle``.
48
+
49
+ With ``include_self=False`` (the default), ``handle`` itself is not
50
+ yielded; the first yielded item is the first child (if any).
51
+ With ``include_self=True``, ``handle`` is yielded first.
52
+ """
53
+ walker = dfs_preorder(tree, handle)
54
+ first = next(walker, None)
55
+ if include_self and first is not None:
56
+ yield first
57
+ yield from walker
vcti/tree/protocols.py ADDED
@@ -0,0 +1,180 @@
1
+ # Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
2
+ # See LICENSE for details.
3
+ """Tree protocols — ReadOnlyTree, MutableTree, LockableTree.
4
+
5
+ Defines what a tree IS in the vcti ecosystem, parameterised in payload
6
+ type ``T`` and handle type ``H``. The protocols specify semantics (what
7
+ a tree must support); they do not prescribe storage. Any backing that
8
+ satisfies the protocols gets the generic algorithms in
9
+ :mod:`vcti.tree.walking`, :mod:`vcti.tree.navigation`, and
10
+ :mod:`vcti.tree.locking` for free.
11
+
12
+ See the module-level docs for each protocol for the contract details
13
+ (root invariant, active-membership semantics, cross-tree handle
14
+ inequality requirement, handle validity rules).
15
+ """
16
+
17
+ from collections.abc import Hashable, Iterable, Iterator
18
+ from typing import Protocol, runtime_checkable
19
+
20
+
21
+ @runtime_checkable
22
+ class ReadOnlyTree[T, H: Hashable](Protocol):
23
+ """A tree of T-valued nodes addressed by handles of type H.
24
+
25
+ Every Tree has a root from construction onward. Implementations
26
+ decide how the root is allocated and how active membership is
27
+ tracked, but the protocol-level contract requires:
28
+
29
+ - ``tree.root_handle`` always returns a handle to a live node.
30
+ - ``parent(tree.root_handle)`` returns ``None``.
31
+ - ``len(tree) >= 1`` always; the empty-tree state does not exist.
32
+ - Handles obtained from different tree instances must compare
33
+ unequal (implementations satisfy this via tagged handles, node
34
+ object identity, or key wrapping — protocol does not prescribe
35
+ the mechanism).
36
+ - ``__contains__``, ``__iter__``, and ``children`` must agree on
37
+ what "currently active" means. The representation
38
+ (soft-delete bit, hard-delete, tombstones, generation counters,
39
+ etc.) is implementation choice; the obligation is
40
+ self-consistency.
41
+
42
+ The iterable returned by :meth:`children` should be treated as
43
+ transient: unless an implementation documents stronger
44
+ guarantees, mutating the tree may invalidate any previously
45
+ obtained child iterable. Materialise first
46
+ (``list(tree.children(h))``) if mutation during traversal is
47
+ required.
48
+ """
49
+
50
+ @property
51
+ def root_handle(self) -> H:
52
+ """The handle of the root node. Always valid."""
53
+ ...
54
+
55
+ def payload(self, handle: H) -> T | None:
56
+ """Return the payload at the handle.
57
+
58
+ Raises:
59
+ Implementation-specific exceptions if the handle is not
60
+ currently a live node (out of range, soft-deleted, from
61
+ another tree, or held across a structural invalidation
62
+ event). The convention is to raise ``LookupError`` or a
63
+ subclass for handle-validity failures.
64
+ """
65
+ ...
66
+
67
+ def parent(self, handle: H) -> H | None:
68
+ """Return the parent handle, or ``None`` for the root."""
69
+ ...
70
+
71
+ def children(self, handle: H) -> Iterable[H]:
72
+ """Return an iterable over the active children of ``handle``."""
73
+ ...
74
+
75
+ def __contains__(self, handle: object) -> bool:
76
+ """Report current active membership.
77
+
78
+ ``handle in tree`` is ``True`` iff ``handle`` refers to a
79
+ currently active node, and ``False`` in every other case —
80
+ including non-handle inputs, foreign-tree handles, and
81
+ stale handles. This method does not raise.
82
+ """
83
+ ...
84
+
85
+ def __iter__(self) -> Iterator[H]:
86
+ """Iterate over active handles in implementation-defined order."""
87
+ ...
88
+
89
+ def __len__(self) -> int:
90
+ """Count of currently active nodes. Always ``>= 1``."""
91
+ ...
92
+
93
+
94
+ @runtime_checkable
95
+ class MutableTree[T, H: Hashable](ReadOnlyTree[T, H], Protocol):
96
+ """A :class:`ReadOnlyTree` that supports mutation.
97
+
98
+ ``create_child`` is the only operation that adds a node, and the
99
+ new node is always allocated as a child of an existing one — the
100
+ root is established by the constructor (implementation-specific)
101
+ and cannot be added or removed through the protocol.
102
+
103
+ ``delete_subtree`` removes a handle and all its descendants
104
+ atomically. There is no protocol-level distinction between
105
+ leaf-delete and subtree-delete; callers that want leaf-only
106
+ semantics should check ``tree.children(handle)`` first.
107
+ """
108
+
109
+ def set_payload(self, handle: H, data: T | None) -> None:
110
+ """Write the payload at the handle."""
111
+ ...
112
+
113
+ def create_child(self, parent: H, data: T | None = None) -> H:
114
+ """Allocate a new child of ``parent`` and return its handle."""
115
+ ...
116
+
117
+ def delete_subtree(self, handle: H) -> None:
118
+ """Remove ``handle`` and all its descendants.
119
+
120
+ Calling this on the root handle must raise (the root cannot
121
+ be removed). The exact exception type is implementation
122
+ choice; the *behaviour* is protocol-required.
123
+ """
124
+ ...
125
+
126
+
127
+ @runtime_checkable
128
+ class LockableTree[T, H: Hashable](MutableTree[T, H], Protocol):
129
+ """A :class:`MutableTree` with per-node structure/payload locks.
130
+
131
+ Lock semantics:
132
+
133
+ - **Subtree-propagating.** ``lock_structure(h)`` sets the lock on
134
+ ``h`` and on every active descendant; ``unlock_structure(h)``
135
+ clears it on the same set.
136
+ - **Flat bitset (no refcount).** Unlocking an outer subtree clears
137
+ the lock on any nested independently-locked subtrees too.
138
+ - **Guards are enforced in mutators.** ``STRUCTURE_LOCKED`` blocks
139
+ ``create_child(locked_node, ...)``, ``delete_subtree(locked_node)``,
140
+ and ``delete_subtree`` of any ancestor whose subtree contains a
141
+ structure-locked node (atomic — no partial deletion).
142
+ ``PAYLOAD_LOCKED`` blocks ``set_payload(locked_node, value)``.
143
+ - **Predicates are pure reads.** ``is_structure_locked`` /
144
+ ``is_payload_locked`` do not mutate and do not raise *because of*
145
+ lock state — a locked node is a valid read, returning ``True``.
146
+ - **Handle validity applies uniformly.** Every method here other
147
+ than the membership test follows the same handle-validity contract
148
+ as :class:`ReadOnlyTree`: the predicates and the ``lock_*`` /
149
+ ``unlock_*`` mutators raise on a stale, foreign, or inactive
150
+ (deleted) handle rather than silently returning stale state or
151
+ no-op'ing. Only ``__contains__`` tolerates a bad handle (it
152
+ returns ``False``).
153
+ - Locks do **not** prevent in-place mutation of payload objects
154
+ (``tree.payload(h).data = ...``). That is the payload type's
155
+ concern, not the tree's.
156
+ """
157
+
158
+ def is_structure_locked(self, handle: H) -> bool:
159
+ """Whether the structure lock is set on ``handle``."""
160
+ ...
161
+
162
+ def is_payload_locked(self, handle: H) -> bool:
163
+ """Whether the payload lock is set on ``handle``."""
164
+ ...
165
+
166
+ def lock_structure(self, handle: H) -> None:
167
+ """Lock structure on ``handle`` and all active descendants."""
168
+ ...
169
+
170
+ def unlock_structure(self, handle: H) -> None:
171
+ """Unlock structure on ``handle`` and all active descendants."""
172
+ ...
173
+
174
+ def lock_payload(self, handle: H) -> None:
175
+ """Lock payload on ``handle`` and all active descendants."""
176
+ ...
177
+
178
+ def unlock_payload(self, handle: H) -> None:
179
+ """Unlock payload on ``handle`` and all active descendants."""
180
+ ...
vcti/tree/py.typed ADDED
File without changes
vcti/tree/walking.py ADDED
@@ -0,0 +1,74 @@
1
+ # Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
2
+ # See LICENSE for details.
3
+ """Walking algorithms over any :class:`ReadOnlyTree`.
4
+
5
+ Free functions following Python's ``sorted(iterable)`` / ``itertools.*``
6
+ tradition. They allocate their own stack/queue and call back into the
7
+ tree only through :attr:`~ReadOnlyTree.root_handle` and
8
+ :meth:`~ReadOnlyTree.children` — they do not touch any implementation
9
+ internals.
10
+
11
+ All three walks start at ``start`` if provided, otherwise at
12
+ ``tree.root_handle``, and yield handles lazily as the walk progresses.
13
+ """
14
+
15
+ from collections import deque
16
+ from collections.abc import Hashable, Iterator
17
+
18
+ from .protocols import ReadOnlyTree
19
+
20
+
21
+ def dfs_preorder[T, H: Hashable](tree: ReadOnlyTree[T, H], start: H | None = None) -> Iterator[H]:
22
+ """DFS pre-order traversal from ``start``.
23
+
24
+ Yields ``start`` first, then each of its subtrees in left-to-right
25
+ order, each subtree recursively in pre-order. Defaults to the
26
+ tree's root.
27
+ """
28
+ nid = tree.root_handle if start is None else start
29
+ stack: list[H] = [nid]
30
+ while stack:
31
+ n = stack.pop()
32
+ yield n
33
+ # Push children in reverse so leftmost is popped first.
34
+ children = list(tree.children(n))
35
+ for c in reversed(children):
36
+ stack.append(c)
37
+
38
+
39
+ def dfs_postorder[T, H: Hashable](tree: ReadOnlyTree[T, H], start: H | None = None) -> Iterator[H]:
40
+ """DFS post-order traversal from ``start``.
41
+
42
+ Yields each subtree in left-to-right post-order (children before
43
+ parent). Defaults to the tree's root.
44
+
45
+ Implemented with an explicit stack of ``(handle, expanded)`` pairs
46
+ rather than Python recursion, so deep trees don't hit the Python
47
+ recursion limit.
48
+ """
49
+ nid = tree.root_handle if start is None else start
50
+ stack: list[tuple[H, bool]] = [(nid, False)]
51
+ while stack:
52
+ n, expanded = stack.pop()
53
+ if expanded:
54
+ yield n
55
+ else:
56
+ stack.append((n, True))
57
+ children = list(tree.children(n))
58
+ for c in reversed(children):
59
+ stack.append((c, False))
60
+
61
+
62
+ def bfs[T, H: Hashable](tree: ReadOnlyTree[T, H], start: H | None = None) -> Iterator[H]:
63
+ """Breadth-first (level-order) traversal from ``start``.
64
+
65
+ Yields ``start`` first, then all nodes at depth 1, then depth 2,
66
+ and so on. Defaults to the tree's root.
67
+ """
68
+ nid = tree.root_handle if start is None else start
69
+ queue: deque[H] = deque([nid])
70
+ while queue:
71
+ n = queue.popleft()
72
+ yield n
73
+ for c in tree.children(n):
74
+ queue.append(c)
@@ -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,13 @@
1
+ vcti/tree/__init__.py,sha256=86WHsJNF74N78KpDivrIeiY9sFV_Y9wbpBJ9TFWCvdE,1039
2
+ vcti/tree/dict_tree.py,sha256=NvOVElVOhBWheKMeF0sgMSTe7z1Zr8ALcZ_7ka6j96E,9029
3
+ vcti/tree/locking.py,sha256=cblkzVJrEQbFNdBtUy9PDqRXPpwwM5UZjjiAEN6U0rE,2778
4
+ vcti/tree/navigation.py,sha256=q-zwZ2VpaxDZAR7O0Pehc9xF0i3jGXYQRE0b2DmmQ14,1911
5
+ vcti/tree/protocols.py,sha256=U1maKg_eQqMasScWeK4-pR7tVePAJp_G4PN_ApTtcak,7287
6
+ vcti/tree/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ vcti/tree/walking.py,sha256=ouEas9CNGkarClkFDoUxVz6OjjrItMkStDl7dyLHOO8,2604
8
+ vcti_tree-1.0.0.dist-info/licenses/LICENSE,sha256=gqRj-E4YRsT7mZ52W76LG6aTTFv6iEOK9QR_fV5EdrI,369
9
+ vcti_tree-1.0.0.dist-info/METADATA,sha256=CWZcQn00x59NKTYF6nb3EzoztkVwC46wooqPPaM-xew,4283
10
+ vcti_tree-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
11
+ vcti_tree-1.0.0.dist-info/top_level.txt,sha256=Jl6AIAI3Xhru_BFQAhD_13VeXLmZQd9BqBNUaAKNgKs,5
12
+ vcti_tree-1.0.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
13
+ vcti_tree-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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 @@
1
+ vcti
@@ -0,0 +1 @@
1
+