Tree
The pyTooling.Tree
package provides fast and simple tree data structure based on a single
Node
class.
Hint
This tree data structure outperforms anytree by far and even itertree by factor of 2.
Further alternatives:
- treelib
Example Tree:
Comprehensive Example:
The following example code demonstrates a few features in a compact form:
# Create a new tree by creating a root node (no parent reference)
root = Node(value="OSVVM Regression Tests")
# Construct the tree top-down
lib = Node(value="Utility Library", parent=root)
# Another standalone node with unique ID (actually an independent tree)
common = Node(nodeID=5, value="Common")
# Construct bottom-up
axi = Node(value="AXI")
axiCommon = Node(value="AXI4 Common")
axi.AddChild(axiCommon)
# Group nodes and handover children at node creation time
vcList = [common, axi]
vcs = Node(value="Verification Components", parent=root, children=vcList)
# Add multiple nodes at once
axiProtocols = (
Node(value="AXI4-Stream"),
Node(value="AXI4-Lite"),
Node(value="AXI4")
)
axi.AddChildren(axiProtocols)
# Create another standalone node and attach it later to a tree.
uart = Node(value="UART")
uart.Parent = vcs
The presented code will generate this tree:
OSVVM Regression Tests
├── Utility Library
├── Verification Components
├── Common
├── AXI
│ ├── AXI4 Common
│ ├── AXI4-Stream
│ ├── AXI4-Lite
│ ├── AXI4
├── UART
Features
Fast and simple tree data structure based on a single
Node
class.A tree can be constructed top-down and bottom-up.
A node can have a unique ID.
A node knows its level (distance from root).
A node can have a value.
A node can store key-value-pairs via dictionary syntax.
A node has a reference to its parent node.
A node has a reference to the root node in a tree (representative node).
Rendering to simple ASCII art for debugging purposes.
Missing Features
Insert a node (currently, only add/append is supported).
Move a node in same hierarchy level.
Move node to a different level/node in the same tree in a single operation.
Allow node deletion.
Planned Features
Allow filters (predicates) in generators to allow node filtering.
Tree export to formats like GraphML, …
Export the tree data structure to file the YAML format.
Allow nodes to have tags and group nodes by tags.
Allow nodes to link to other nodes (implement proxy behavior?)
Out of Scope
Preserve or recover the tree data structure before an erroneous operation caused an exception and aborted a tree modification, which might leave the tree in a corrupted state.
Export the tree data structure to various file formats like JSON, TOML, …
Import a tree data structure from various file formats like JSON, YAML, TOML, …
Tree visualization or rendering to complex formats like GraphViz, Mermaid, …
By Feature
Danger
Accessing internal fields of a node is strongly not recommended for users, as it might lead to a corrupted tree data structure. If a power-user wants to access these fields, feel free to use them for achieving a higher performance, but you got warned 😉.
Unique ID
A node can be created with a unique ID when the object is created. Afterwards, the ID
is
a readonly property. Any hashable object can be used as an ID. The ID must be unique per tree. If trees are merged or
nodes are added to an existing tree, the newly added node’s ID(s) are checked and might cause an exception.
# Create node with unique ID 5
node = Node(nodeID=5)
# Read a node's ID
nodeID = node.ID
Level
Each node has a level describing the distance from root node. It can be accessed via the read-only
property Level
.
The root node has a level of 0
, children of root have a level of 1
, and so on.
# Create node
root = Node(nodeID=0)
node2 = Node(nodeID=1, parent=root)
# Read a node's level
nodeLevel = node2.Level
Value
A node’s value can be given at node creating time or it can be set ant any later time via property
Value
. Any data type is accepted. The internally stored value can be retrieved by the
same property. If a node’s string representation is requested via __str__()
and a node’s
value isn’t None, then the value’s string representation is returned.
# Create node with value 5
node = Node(value=5)
# Set or change a node's value
node.Value = 10
# Access a node's Value
value = node.Value
Key-Value-Pairs
Besides a unique ID and a value, each node can hold an arbitrary set of key-value-pairs.
# Create node
node = Node()
# Create or update a key-value-pair
node["key"] = value
# Access a value by key
value = node["key"]
# Delete a key-value-pair
del node["key"]
Parent Reference
Each node has a reference to its parent node. In case, the node is the root node, the
parent reference is None
. The parent-child relation can be set at node creation time, or a parent can be assigned to a node at any later time via
property Parent
. The same property can be used to retrieve the current parent reference.
# Create node without parent relation ship (root node)
root = Node(nodeID=0)
# Create a node add directly attach it to an existing tree
node = Node(nodeID=1, parent=root)
# Access a node's parent
parent = node.Parent
Merging Trees
In case, two trees were created (a single node is already a minimal tree), trees get merged if one tree’s root node is assigned a parent relationship.
# Create a tree with a single node
root = Node(nodeID=0)
# Create a second minimalistic tree
otherTree = Node(nodeID=100)
# Set parent relationship and merge trees
otherTree.Parent = root
See also
See Merging Trees for more details.
Splitting Trees
In case, a node within a tree’s hierarchy is updated with respect to it’s parent relationship to None
, then
the tree gets split into 2 trees.
# Create a tree of 4 nodes
root1 = Node(nodeID=0)
node1 = Node(nodeID=1, parent=root1)
root2 = Node(nodeID=2, parent=node1)
node3 = Node(nodeID=3, parent=root2)
# Split the tree between node1 and root2
root2.Parent = None
See also
See Splitting Trees for more details.
Moving a branch in same tree
Todo
TREE::Parent::move-branch in same tree - needs also testcases
Moving a branch to another tree
Todo
TREE::Parent::move-branch into another tree - needs also testcases
Root Reference
Each node has a reference to the tree’s root node. The root node can also be considered the
representative node of a tree and can be accessed via read-only property Root
.
When a node is assigned a new parent relation and this parent is a node in another tree, the root reference will change. (A.k.a. moving a branch to another tree.)
The root node of a tree contains tree-wide data structures like the list of unique IDs
(_nodesWithID
, _nodesWithoutID
). By utilizing the root
reference, each node can access these data structures by just one additional reference hop.
# Create a simple tree
root = Node()
nodeA = Node(parent=root)
nodeB = Node(parent=root)
# Check if nodeA and nodeB are in same tree
isSameTree = nodeA is nodeB
Path
The property Path
returns a tuple describing the path top-down from root node to the
current node.
# Create a simple tree representing directories
root = Node(value="C:")
dir = Node(value="temp", parent=root)
file = Node(value="test.log", parent=dir)
# Convert a path to string
path = "\".join(file.Path)
While the tuple returned by Path
can be used in an iteration (e.g. a for-loop), also a
generator is provided by method GetPath()
for iterations.
# Create a simple tree representing directories
root = Node(value="C:")
dir = Node(value="temp", parent=root)
file = Node(value="test.log", parent=dir)
# Render path from root to node with indentations to ASCII art
for level, node in enumerate(file.GetPath()):
print(f"{' '*level}'\-'{node}")
# \-C:
# \-temp
# \-test.log
Ancestors
The method GetAncestors()
returns a generator to traverse bottom-up from current node to
the root node. If the top-down direction is needed, see Path for more details.
Python Code |
Diagram |
---|---|
Tree Construction: # Create an example tree
root = Node(nodeID=0)
dots = Node(nodeID=1, parent=root)
node1 = Node(nodeID=2, parent=dots)
grandParent = Node(nodeID=3, parent=dots)
node2 = Node(nodeID=4, parent=dots)
uncle = Node(nodeID=5, parent=grandParent)
parent = Node(nodeID=6, parent=grandParent)
aunt = Node(nodeID=7, parent=grandParent)
sibling1 = Node(nodeID=8, parent=parent)
me = Node(nodeID=9, parent=parent)
sibling2 = Node(nodeID=10, parent=parent)
niece1 = Node(nodeID=11, parent=sibling1)
nephew1 = Node(nodeID=12, parent=sibling1)
child1 = Node(nodeID=13, parent=me)
child2 = Node(nodeID=14, parent=me)
child3 = Node(nodeID=15, parent=me)
niece2 = Node(nodeID=16, parent=sibling2)
nephew2 = Node(nodeID=17, parent=sibling2)
grandChild1 = Node(nodeID=18, parent=child2)
grandChild2 = Node(nodeID=19, parent=child2)
Usage # Walk bottom-up all the way to root
for node in me.GetAncestors():
print(node.ID)
Result 6 # parent
3 # grandparent
1 # ...
0 # root
|
%%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% graph TD R(Root) A(...) BL(Node); B(GrandParent); BR(Node) CL(Uncle); C(Parent); CR(Aunt) DL(Sibling); D(Node); DR(Sibling) ELN1(Niece); ELN2(Nephew) EL(Child); E(Child); ER(Child); ERN1(Niece);ERN2(Nephew) F1(GrandChild); F2(GrandChild) R:::mark1 --> A A:::mark2 --> BL & B & BR B:::mark2 --> CL & C & CR C:::mark2 --> DL & D & DR DL --> ELN1 & ELN2 D:::cur --> EL & E & ER DR --> ERN1 & ERN2 E --> F1 & F2 classDef node fill:#eee,stroke:#777,font-size:smaller; classDef cur fill:#9e9,stroke:#6e6; classDef mark1 fill:#69f,stroke:#37f,color:#eee; classDef mark2 fill:#69f,stroke:#37f; |
Common Ancestors
If needed, method GetCommonAncestors()
provides a generator to iterate the common
ancestors of two nodes in a tree. It iterates from root node top-down until the common branch in the tree splits of.
Python Code |
Diagram |
---|---|
Tree Construction: # Create an example tree
root = Node(nodeID=0)
dots = Node(nodeID=1, parent=root)
node1 = Node(nodeID=2, parent=dots)
grandParent = Node(nodeID=3, parent=dots)
node2 = Node(nodeID=4, parent=dots)
uncle = Node(nodeID=5, parent=grandParent)
parent = Node(nodeID=6, parent=grandParent)
aunt = Node(nodeID=7, parent=grandParent)
sibling1 = Node(nodeID=8, parent=parent)
me = Node(nodeID=9, parent=parent)
sibling2 = Node(nodeID=10, parent=parent)
niece1 = Node(nodeID=11, parent=sibling1)
nephew1 = Node(nodeID=12, parent=sibling1)
child1 = Node(nodeID=13, parent=me)
child2 = Node(nodeID=14, parent=me)
child3 = Node(nodeID=15, parent=me)
niece2 = Node(nodeID=16, parent=sibling2)
nephew2 = Node(nodeID=17, parent=sibling2)
grandChild1 = Node(nodeID=18, parent=child2)
grandChild2 = Node(nodeID=19, parent=child2)
Usage # Walk bottom-up all the way to root
for node in nephew1.GetCommonAncestors(grandChild2):
print(node.ID)
Result 0 # root
1 # ...
3 # grandparent
6 # parent
|
%%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% graph TD R(Root) A(...) BL(Node); B(GrandParent); BR(Node) CL(Uncle); C(Parent); CR(Aunt) DL(Sibling); D(Node); DR(Sibling) ELN1(Niece); ELN2(Nephew) EL(Child); E(Child); ER(Child); ERN1(Niece);ERN2(Nephew) F1(GrandChild); F2(GrandChild) R:::mark1 --> A A:::mark2 --> BL & B & BR B:::mark2 --> CL & C & CR C:::mark2 --> DL & D & DR DL --> ELN1 & ELN2 D --> EL & E & ER DR --> ERN1 & ERN2 E --> F1 & F2 ELN2:::cur; F2:::cur classDef node fill:#eee,stroke:#777,font-size:smaller; classDef cur fill:#9e9,stroke:#6e6; classDef mark1 fill:#69f,stroke:#37f,color:#eee; classDef mark2 fill:#69f,stroke:#37f; |
Children
Children are all direct successors of a node.
A node object supports returning children either as a tuple via a property or as a generator via a method call.
Return a Tuple |
Return a Generator |
|
---|---|---|
Children |
|
|
Children and children thereof |
— — — — |
Python Code |
Diagram |
---|---|
Tree Construction: # Create an example tree
root = Node(nodeID=0)
dots = Node(nodeID=1, parent=root)
node1 = Node(nodeID=2, parent=dots)
grandParent = Node(nodeID=3, parent=dots)
node2 = Node(nodeID=4, parent=dots)
uncle = Node(nodeID=5, parent=grandParent)
parent = Node(nodeID=6, parent=grandParent)
aunt = Node(nodeID=7, parent=grandParent)
sibling1 = Node(nodeID=8, parent=parent)
me = Node(nodeID=9, parent=parent)
sibling2 = Node(nodeID=10, parent=parent)
niece1 = Node(nodeID=11, parent=sibling1)
nephew1 = Node(nodeID=12, parent=sibling1)
child1 = Node(nodeID=13, parent=me)
child2 = Node(nodeID=14, parent=me)
child3 = Node(nodeID=15, parent=me)
niece2 = Node(nodeID=16, parent=sibling2)
nephew2 = Node(nodeID=17, parent=sibling2)
grandChild1 = Node(nodeID=18, parent=child2)
grandChild2 = Node(nodeID=19, parent=child2)
Usage # Walk bottom-up all the way to root
for node in me.GetChildren():
print(node.ID)
Result 13 # child1
14 # child2
15 # child3
|
%%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% graph TD R(Root) A(...) BL(Node); B(GrandParent); BR(Node) CL(Uncle); C(Parent); CR(Aunt) DL(Sibling); D(Node); DR(Sibling) ELN1(Niece); ELN2(Nephew) EL(Child); E(Child); ER(Child); ERN1(Niece);ERN2(Nephew) F1(GrandChild); F2(GrandChild) R --> A A --> BL & B & BR B --> CL & C & CR C --> DL & D & DR DL --> ELN1 & ELN2 D:::cur --> EL & E & ER DR --> ERN1 & ERN2 E --> F1 & F2 EL:::mark2; E:::mark2; ER:::mark2 classDef node fill:#eee,stroke:#777,font-size:smaller; classDef cur fill:#9e9,stroke:#6e6; classDef mark1 fill:#69f,stroke:#37f,color:#eee; classDef mark2 fill:#69f,stroke:#37f; |
Descendants
Descendants are all direct and indirect successors of a node (child nodes and child nodes thereof a.k.a. grandchild, grand-grandchildren, …).
A node object supports returning descendants as a generator via a method call to GetDescendants()
,
due to the recursive behavior.
See also
See Iterating a Tree for various other forms for iterating nodes in a tree.
Python Code |
Diagram |
---|---|
Tree Construction: # Create an example tree
root = Node(nodeID=0)
dots = Node(nodeID=1, parent=root)
node1 = Node(nodeID=2, parent=dots)
grandParent = Node(nodeID=3, parent=dots)
node2 = Node(nodeID=4, parent=dots)
uncle = Node(nodeID=5, parent=grandParent)
parent = Node(nodeID=6, parent=grandParent)
aunt = Node(nodeID=7, parent=grandParent)
sibling1 = Node(nodeID=8, parent=parent)
me = Node(nodeID=9, parent=parent)
sibling2 = Node(nodeID=10, parent=parent)
niece1 = Node(nodeID=11, parent=sibling1)
nephew1 = Node(nodeID=12, parent=sibling1)
child1 = Node(nodeID=13, parent=me)
child2 = Node(nodeID=14, parent=me)
child3 = Node(nodeID=15, parent=me)
niece2 = Node(nodeID=16, parent=sibling2)
nephew2 = Node(nodeID=17, parent=sibling2)
grandChild1 = Node(nodeID=18, parent=child2)
grandChild2 = Node(nodeID=19, parent=child2)
Usage # Walk bottom-up all the way to root
for node in me.GetDescendants():
print(node.ID)
Result 13 # child1
14 # child2
18 # grandChild1
19 # grandChild2
15 # child3
|
%%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% graph TD R(Root) A(...) BL(Node); B(GrandParent); BR(Node) CL(Uncle); C(Parent); CR(Aunt) DL(Sibling); D(Node); DR(Sibling) ELN1(Niece); ELN2(Nephew) EL(Child); E(Child); ER(Child); ERN1(Niece);ERN2(Nephew) F1(GrandChild); F2(GrandChild) R --> A A --> BL & B & BR B --> CL & C & CR C --> DL & D & DR DL --> ELN1 & ELN2 D:::cur --> EL & E & ER DR --> ERN1 & ERN2 E --> F1 & F2 EL:::mark2; E:::mark2; ER:::mark2; F1:::mark2; F2:::mark2 classDef node fill:#eee,stroke:#777,font-size:smaller; classDef cur fill:#9e9,stroke:#6e6; classDef mark1 fill:#69f,stroke:#37f,color:#eee; classDef mark2 fill:#69f,stroke:#37f; |
Siblings
Siblings are all direct child nodes of a node’s parent node except itself.
A node object supports returning siblings either as tuples via a property or as a generator via a method call. Either all siblings are returned or just siblings left from the current node (left siblings) or right from the current node (right siblings). Left and right is based on the order of child references in the current node’s parent.
Sibling Selection |
Return a Tuple |
Return a Generator |
---|---|---|
Left Siblings |
||
All Siblings |
||
Right Siblings |
Attention
In case a node has no parent, an exception is raised, because siblings cannot exist.
Python Code |
Diagram |
---|---|
Tree Construction: # Create an example tree
root = Node(nodeID=0)
dots = Node(nodeID=1, parent=root)
node1 = Node(nodeID=2, parent=dots)
grandParent = Node(nodeID=3, parent=dots)
node2 = Node(nodeID=4, parent=dots)
uncle = Node(nodeID=5, parent=grandParent)
parent = Node(nodeID=6, parent=grandParent)
aunt = Node(nodeID=7, parent=grandParent)
sibling1 = Node(nodeID=8, parent=parent)
me = Node(nodeID=9, parent=parent)
sibling2 = Node(nodeID=10, parent=parent)
niece1 = Node(nodeID=11, parent=sibling1)
nephew1 = Node(nodeID=12, parent=sibling1)
child1 = Node(nodeID=13, parent=me)
child2 = Node(nodeID=14, parent=me)
child3 = Node(nodeID=15, parent=me)
niece2 = Node(nodeID=16, parent=sibling2)
nephew2 = Node(nodeID=17, parent=sibling2)
grandChild1 = Node(nodeID=18, parent=child2)
grandChild2 = Node(nodeID=19, parent=child2)
Usage # Walk bottom-up all the way to root
for node in me.GetLeftSiblings():
print(node.ID)
for node in me.GetRightSiblings():
print(node.ID)
Result 8 # sibling1
10 # sibling2
|
%%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% graph TD R(Root) A(...) BL(Node); B(GrandParent); BR(Node) CL(Uncle); C(Parent); CR(Aunt) DL(Sibling); D(Node); DR(Sibling) ELN1(Niece); ELN2(Nephew) EL(Child); E(Child); ER(Child); ERN1(Niece);ERN2(Nephew) F1(GrandChild); F2(GrandChild) R --> A A --> BL & B & BR B --> CL & C & CR C --> DL & D & DR DL --> ELN1 & ELN2 D:::cur --> EL & E & ER DR --> ERN1 & ERN2 E --> F1 & F2 DL:::mark2; DR:::mark2 classDef node fill:#eee,stroke:#777,font-size:smaller; classDef cur fill:#9e9,stroke:#6e6; classDef mark1 fill:#69f,stroke:#37f,color:#eee; classDef mark2 fill:#69f,stroke:#37f; |
Relatives
Relatives are siblings and their descendants.
A node object supports returning relatives as a generator via a method call, due to the recursive behavior. Either all relatives are returned or just relatives left from the current node (left relatives) or right from the current node (right relatives). Left and right is based on the order of child references in the current node’s parent.
Relative Selection |
Return a Generator |
---|---|
Left Siblings |
|
All Siblings |
|
Right Siblings |
Attention
In case a node has no parent, an exception is raised, because siblings and therefore relatives cannot exist.
Python Code |
Diagram |
---|---|
Tree Construction: # Create an example tree
root = Node(nodeID=0)
dots = Node(nodeID=1, parent=root)
node1 = Node(nodeID=2, parent=dots)
grandParent = Node(nodeID=3, parent=dots)
node2 = Node(nodeID=4, parent=dots)
uncle = Node(nodeID=5, parent=grandParent)
parent = Node(nodeID=6, parent=grandParent)
aunt = Node(nodeID=7, parent=grandParent)
sibling1 = Node(nodeID=8, parent=parent)
me = Node(nodeID=9, parent=parent)
sibling2 = Node(nodeID=10, parent=parent)
niece1 = Node(nodeID=11, parent=sibling1)
nephew1 = Node(nodeID=12, parent=sibling1)
child1 = Node(nodeID=13, parent=me)
child2 = Node(nodeID=14, parent=me)
child3 = Node(nodeID=15, parent=me)
niece2 = Node(nodeID=16, parent=sibling2)
nephew2 = Node(nodeID=17, parent=sibling2)
grandChild1 = Node(nodeID=18, parent=child2)
grandChild2 = Node(nodeID=19, parent=child2)
Usage # Walk bottom-up all the way to root
for node in me.GetLeftRelatives():
print(node.ID)
for node in me.GetRightRelatives():
print(node.ID)
Result 8 # sibling1
11 # niece1
12 # nephew1
10 # sibling2
16 # niece2
17 # nephew2
|
%%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% graph TD R(Root) A(...) BL(Node); B(GrandParent); BR(Node) CL(Uncle); C(Parent); CR(Aunt) DL(Sibling); D(Node); DR(Sibling) ELN1(Niece); ELN2(Nephew) EL(Child); E(Child); ER(Child); ERN1(Niece);ERN2(Nephew) F1(GrandChild); F2(GrandChild) R --> A A --> BL & B & BR B --> CL & C & CR C --> DL & D & DR DL --> ELN1 & ELN2 D:::cur --> EL & E & ER DR --> ERN1 & ERN2 E --> F1 & F2 DL:::mark2; ELN1:::mark2; ELN2:::mark2; DR:::mark2; ERN1:::mark2; ERN2:::mark2 classDef node fill:#eee,stroke:#777,font-size:smaller; classDef cur fill:#9e9,stroke:#6e6; classDef mark1 fill:#69f,stroke:#37f,color:#eee; classDef mark2 fill:#69f,stroke:#37f; |
Iterating a Tree
A tree (starting at the root node) or a subtree (starting at any node in the tree) can be iterated in various orders:
IterateLeafs()
- iterates only over leafs from left to rightIterateLevelOrder()
- iterates all sub nodes level by levelIteratePreOrder()
- iterates left to right and returns itself before its descendantsIteratePostOrder()
- iterates left to right and returns its descendants before itself
Merging Trees
A tree B is merged into an existing tree A, when a tree B’s parent relation is set to a non-None
value. Therefore use the B.Parent
property and set it to A:
B.Parent = A
.
The following operations are executed on the tree B:
register all nodes of B with and without ID in A, then
delete the list and dictionary objects for nodes with and without IDs from B.
The following operations are executed on all nodes in tree B:
set root reference to A.
recompute the level within A.
Attention
In case a node’s ID already exists in A, an exception is raised, because IDs are unique.
Splitting Trees
Todo
TREE: splitting a tree
Tree Rendering
The tree data structure can be rendered as ASCII art. The Render()
method renders the tree
into a multi line string.
Todo
TREE:Render:: explain parameters
Example
<Root 0>
o-- <Node 1>
| o-- <Node 4>
| | o-- <Node 8>
| | o-- <Node 9>
| o-- <Node 5>
| o-- <Node 10>
| o-- <Node 11>
| o-- <Node 12>
| o-- <Node 13>
o-- <Node 2>
o-- <Node 3>
o-- <Node 6>
o-- <Node 7>
Competing Solutions
This tree data structure outperforms anytree by far and even itertree by factor of 2.
anytree
Source: anytree
Todo
TREE::anytree write comparison here.
Disadvantages
…
Standoff
…
Advantages
…
# add code here
itertree
Source: itertree
Todo
TREE::itertree write comparison here.
Disadvantages
…
Standoff
…
Advantages
…
# add code here
treelib
Source: treelib
Todo
TREE::treelib write comparison here.
Disadvantages
…
Standoff
…
Advantages
…
# add code here