diff --git a/CHANGELOG.md b/CHANGELOG.md
index 429b815f..c1c5db95 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,10 +5,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+- BaseNode: Method `get_common_ancestors` for getting common ancestors, this was already available in parsing module.
+- Docs: Enumerate the demonstration page, fix url links.
+- Docs: Update demonstration to include query examples.
## [1.4.1] - 2026-05-02
### Changed:
-- Tree Helper: Tree diff modify type hint to include returning None.
+- Tree Diff: Modify type hint to include returning None.
- Misc: Mypy type checks to remove ignoring warn_no_return.
## [1.4.1] - 2026-05-02
diff --git a/bigtree/_plugins.py b/bigtree/_plugins.py
index 4890e311..6f10fe28 100644
--- a/bigtree/_plugins.py
+++ b/bigtree/_plugins.py
@@ -16,6 +16,7 @@ def register_binarytree_plugins() -> None:
# Iterator methods
"inorder_iter": iterators.inorder_iter,
},
+ method="default",
)
plugin_docs = "\n".join(
@@ -52,6 +53,7 @@ def register_dag_plugins() -> None:
# Iterator methods
"iterate": iterators.dag_iterator,
},
+ method="default",
)
plugin_docs = "\n".join(
@@ -143,7 +145,8 @@ def register_tree_plugins() -> None:
"find_children": search.find_children,
"find_child": search.find_child,
"find_child_by_name": search.find_child_by_name,
- }
+ },
+ method="default",
)
Tree.register_plugins(
{
diff --git a/bigtree/node/basenode.py b/bigtree/node/basenode.py
index 37bde59c..f9b11c3b 100644
--- a/bigtree/node/basenode.py
+++ b/bigtree/node/basenode.py
@@ -114,13 +114,14 @@ class BaseNode:
1. ``describe()``: Get node information sorted by attributes, return list of tuples
2. ``get_attr(attr_name: str)``: Get value of node attribute
3. ``set_attrs(attrs: dict)``: Set node attribute name(s) and value(s)
- 4. ``go_to(node: Self)``: Get a path from own node to another node from same tree
- 5. ``append(node: Self)``: Add child to node
- 6. ``extend(nodes: list[Self])``: Add multiple children to node
- 7. ``copy()``: Deep copy self
- 8. ``sort()``: Sort child nodes
- 9. ``plot()``: Plot tree in line form
- 10. ``query(query: str)``: Filter tree using Tree Query Language
+ 4. ``get_common_ancestors(node: Self)``: Get common ancestors with another node from same tree
+ 5. ``go_to(node: Self)``: Get a path from own node to another node from same tree
+ 6. ``append(node: Self)``: Add child to node
+ 7. ``extend(nodes: list[Self])``: Add multiple children to node
+ 8. ``copy()``: Deep copy self
+ 9. ``sort()``: Sort child nodes
+ 10. ``plot()``: Plot tree in line form
+ 11. ``query(query: str)``: Filter tree using Tree Query Language
----
@@ -652,8 +653,18 @@ def set_attrs(self, attrs: Mapping[str, Any]) -> None:
"""
self.__dict__.update(attrs)
+ def get_common_ancestors(self: T, node: T) -> Iterable[T]:
+ """Get common ancestors of current node and specified node from same tree.
+
+ Args:
+ node: node to get common ancestors
+ """
+ from bigtree.tree.parsing import get_common_ancestors
+
+ return get_common_ancestors([self, node])
+
def go_to(self: T, node: T) -> Iterable[T]:
- """Get path from current node to specified node from same tree, uses `get_path` function.
+ """Get path from current node to specified node from same tree.
Args:
node: node to travel to from current node, inclusive of start and end node
diff --git a/docs/gettingstarted/demo/binarytree.md b/docs/gettingstarted/demo/binarytree.md
index f871cc12..901be159 100644
--- a/docs/gettingstarted/demo/binarytree.md
+++ b/docs/gettingstarted/demo/binarytree.md
@@ -12,9 +12,9 @@ Compared to nodes in tree, nodes in Binary Tree are only allowed maximum of 2 ch
Since BinaryNode extends from Node, construct, traverse, search, export methods from Node are applicable to
Binary Tree as well.
-## Construct Binary Tree
+## 1. Construct Binary Tree
-### 1. From BinaryNode
+### 1.1 From BinaryNode
BinaryNode can be linked to each other with `parent`, `children`, `left`, and `right` setter methods,
or using bitshift operator with the convention `parent_node >> child_node` or `child_node << parent_node`.
@@ -37,7 +37,7 @@ graph.write_png("assets/demo/binarytree.png")

-### 2. From list
+### 1.2 From list
Construct nodes only, list has similar format as `heapq` list.
@@ -56,7 +56,7 @@ tree.hshow()
# └─ 7
```
-## Traverse Binary Tree
+## 2. Traverse Binary Tree
In addition to the traversal methods in the usual tree, binary tree includes in-order traversal method.
diff --git a/docs/gettingstarted/demo/dag.md b/docs/gettingstarted/demo/dag.md
index ed50cfa6..c921fc65 100644
--- a/docs/gettingstarted/demo/dag.md
+++ b/docs/gettingstarted/demo/dag.md
@@ -10,9 +10,9 @@ to implement dag-level methods (for construct/export etc.) for a more intuitive
Compared to nodes in tree, nodes in DAG are able to have multiple parents.
-## Construct DAG
+## 1. Construct DAG
-### 1. From DAGNode
+### 1.1 From DAGNode
DAGNodes can be linked to each other in the following ways:
@@ -39,7 +39,7 @@ graph.write_png("assets/demo/dag.png")

-### 2. From list
+### 1.2 From list
Construct nodes only, list contains parent-child tuples.
@@ -53,7 +53,7 @@ print([(parent.node_name, child.node_name) for parent, child in dag.iterate()])
# [('a', 'd'), ('c', 'd'), ('d', 'e'), ('a', 'c'), ('b', 'c')]
```
-### 3. From nested dictionary
+### 1.3 From nested dictionary
Construct nodes with attributes, `key`: child name, `value`: dict of parent name, child node attributes.
@@ -73,7 +73,7 @@ print([(parent.node_name, child.node_name) for parent, child in dag.iterate()])
# [('a', 'd'), ('c', 'd'), ('d', 'e'), ('a', 'c'), ('b', 'c')]
```
-### 4. From pandas DataFrame
+### 1.4 From pandas DataFrame
Construct nodes with attributes, *pandas DataFrame* contains child column, parent column, and attribute columns.
@@ -99,7 +99,7 @@ print([(parent.node_name, child.node_name) for parent, child in dag.iterate()])
# [('a', 'd'), ('c', 'd'), ('d', 'e'), ('a', 'c'), ('b', 'c')]
```
-## DAG Attributes and Operations
+## 2. DAG Attributes and Operations
Note that using `DAGNode` as superclass inherits the default class attributes (properties) and operations (methods).
diff --git a/docs/gettingstarted/demo/tree.md b/docs/gettingstarted/demo/tree.md
index b61c6edd..03553ccc 100644
--- a/docs/gettingstarted/demo/tree.md
+++ b/docs/gettingstarted/demo/tree.md
@@ -10,12 +10,12 @@ implement tree-level methods for a more intuitive API.
Here are some codes to get started.
-## Construct Tree
+## 1. Construct Tree
Nodes can have attributes if they are initialized from `Node`, *dictionary*, *pandas DataFrame*, or *polars DataFrame*.
-Read more [here](/../../bigtree/tree/tree/#tree-construct-methods).
+Read more [here](https://bigtree.readthedocs.io/stable/bigtree/tree/construct/#tree-construct-methods).
-### 1. From Node
+### 1.1 From Node
Nodes can be linked to each other in the following ways:
@@ -104,7 +104,7 @@ Nodes can be linked to each other in the following ways:

-### 2. From str
+### 1.2 From str
Construct nodes only. Newick string notation supports parsing attributes.
@@ -153,7 +153,7 @@ Construct nodes only. Newick string notation supports parsing attributes.
# └── f
```
-### 3. From list
+### 1.3 From list
Construct nodes only. List can contain either full paths or tuples of parent-child names.
@@ -183,7 +183,7 @@ Construct nodes only. List can contain either full paths or tuples
# └── c
```
-### 4. From nested dictionary
+### 1.4 From nested dictionary
Construct nodes with attributes. Dictionary can be in a flat structure where `key` is path and `value` is
dictionary of node attribute names and values, or in a recursive structure where `key` is node attribute
@@ -277,7 +277,7 @@ names and `value` is node attribute values, and list of children (recursive).
```
-### 5. From pandas/polars DataFrame
+### 1.5 From pandas/polars DataFrame
Construct nodes with attributes. *DataFrame* can contain either path column or
parent-child columns. Other columns can be used to specify attributes.
@@ -378,7 +378,7 @@ Construct nodes with attributes. *DataFrame* can contain either path colum
# └── c [age=60]
```
-### 6. From rich Trees
+### 1.6 From rich Trees
Convert rich.tree.Tree to bigtree Trees.
@@ -408,9 +408,9 @@ Convert rich.tree.Tree to bigtree Trees.
If tree is already created, nodes can still be added using path string, dictionary, and pandas/polars DataFrame!
Attributes can be added to existing nodes using a dictionary or pandas/polars DataFrame.
-## View Tree
+## 2. View Tree
-### 1. Print Tree
+### 2.1 Print Tree
After tree is constructed, it can be viewed by printing to console using `show`, `hshow`, or `vshow` method directly,
for compact, horizontal, and vertical orientation respectively.
@@ -578,7 +578,7 @@ Other customisations for printing are also available, such as:
# └── c
```
-### 2. Display on Jupyter Notebook
+### 2.2 Display in Jupyter Notebook
Tree can be displayed interactively on jupyter notebook using `ishow`.
@@ -606,7 +606,7 @@ tree.ishow(all_attrs=True, height=400) # (1)!
-### 3. Plot Tree
+### 2.3 Plot Tree
Tree can also be plotted using `plot` method directly with the help of `matplotlib` library.
@@ -635,7 +635,7 @@ fig.savefig("assets/demo/tree_plot.png") # Save figure

-## Tree Attributes and Operations
+## 3. Tree Attributes and Operations
Note that using `BaseNode` or `Node` as superclass inherits the default class attributes (properties)
and operations (methods).
@@ -693,7 +693,8 @@ Below is the table of operations available to `BaseNode` and `Node` classes.
|------------------------------------|------------------------------------------------------------------|--------------------------------------------|
| Visualize tree (only for `Node`) | `root.show()` / `root.hshow()` / `root.vshow()` | None |
| Get node information | `root.describe(exclude_prefix="_")` | [('name', 'a')] |
-| Find path from one node to another | `root.go_to(node_e)` | [Node(/a, ), Node(/a/b, ), Node(/a/b/e, )] |
+| Find common ancestors | `root.get_common_ancestors(node_e)` | [Node(/a/b, ), Node(/a/b/e, )] |
+| Find path from one node to another | `node_b.go_to(node_e)` | [Node(/a, ), Node(/a/b, ), Node(/a/b/e, )] |
| Add one or more children to node | `root.append(Node("j"))` / `root.extend([Node("k"), Node("l")])` | Node(/a, ) |
| Set attribute(s) | `root.set_attrs({"description": "root-tag"})` | None |
| Get attribute | `root.get_attr("description")` | 'root-tag' |
@@ -702,7 +703,7 @@ Below is the table of operations available to `BaseNode` and `Node` classes.
| Plot tree | `root.plot("-ok")` | plt.Figure() |
| Query tree | `root.query('name == "b"')` | [Node(/a/b, )] |
-## Traverse Tree
+## 4. Traverse Tree
Tree can be traversed using the following traversal methods.
@@ -742,7 +743,7 @@ a
# [['a'], ['c', 'b'], ['d', 'e']]
```
-## Modify Tree
+## 5. Modify Tree
Nodes can be shifted (with or without replacement) or copied (without replacement)
from one path to another, this changes the tree in-place.
@@ -887,13 +888,15 @@ root_other.show()
4. The first copy and replace of `Documents/Pictures/photo2.jpg` with `photo1.jpg`
5. The second copy and replace of `Documents/file2.doc` with `file1.doc`
-## Tree Search
+## 6. Tree Search and Query
One or multiple nodes can be searched based on name, path, attribute value, or user-defined condition.
It is also possible to search for one or more child node(s) based on attributes, this search will be faster as
it does not require traversing the whole tree to find the node(s).
-Read more [here](/../../bigtree/tree/tree/#tree-query-and-search-methods).
+Tree can be queried using Tree Query Language.
+
+Read more [here](https://bigtree.readthedocs.io/stable/bigtree/tree/tree/#tree-query-and-search-methods).
=== "Find single node"
```python hl_lines="13 16 19 22 25 28"
@@ -985,11 +988,32 @@ Read more [here](/../../bigtree/tree/tree/#tree-query-and-search-methods).
# Node(/a/c/c, age=40)
```
-## Helper Utility
+=== "Tree Query"
+ ```python hl_lines="13 16"
+ from bigtree import Node, Tree
+ root = Node("a", age=90)
+ b = Node("b", age=65, parent=root)
+ c = Node("c", age=60, parent=root)
+ d = Node("d", age=40, parent=c)
+ tree = Tree(root)
+ tree.show(attr_list=["age"])
+ # a [age=90]
+ # ├── b [age=65]
+ # └── c [age=60]
+ # └── d [age=40]
+
+ tree.query('name == "b" OR age <= 40')
+ # [Node(/a/b, age=65), Node(/a/c/d, age=40)]
+
+ tree.query("is_leaf")
+ # [Node(/a/b, age=65), Node(/a/c/d, age=40)]
+ ```
+
+## 7. Helper Utility
-Read more [here](/../../bigtree/tree/tree/#tree-helper-methods).
+Read more [here](https://bigtree.readthedocs.io/stable/bigtree/tree/tree/#tree-helper-methods).
-### 1. Clone tree
+### 7.1 Clone tree
Trees can be cloned to another Node type. If the same type is desired, use `tree.copy()` instead.
@@ -1003,7 +1027,7 @@ clone_tree(root, Node) # clone from `BaseNode` to `Node` type
# Node(/a, )
```
-### 2. Get subtree
+### 7.2 Get subtree
Subtree refers to a smaller tree with a different tree root.
@@ -1026,7 +1050,7 @@ root_subtree.show()
# └── e
```
-### 3. Prune tree
+### 7.3 Prune tree
Pruned tree refers to a smaller tree with the same tree root. Trees can be pruned by one or more of the following filters:
@@ -1090,7 +1114,7 @@ Pruned tree refers to a smaller tree with the same tree root. Trees can be prune
# └── c
```
-### 4. Get tree differences
+### 7.4 Get tree differences
View the differences in structure and/or attributes between two trees. The changes reflected are relative to the first
tree. By default, only the differences are shown. It is possible to view the full original tree with the differences.
@@ -1282,7 +1306,7 @@ For aggregating the differences at the parent-level instead of having `(+)` and
# └── d (~) [tags=('original d', 'new d')]
```
-## Export Tree
+## 8. Export Tree
Tree can be exported to other data types:
@@ -1295,7 +1319,7 @@ Tree can be exported to other data types:
7. Mermaid Flowchart (can display on .md)
8. Pyvis Network (can display interactive .html)
-Read more [here](/../../bigtree/tree/tree/#tree-export-methods).
+Read more [here](https://bigtree.readthedocs.io/stable/bigtree/tree/tree/#tree-export-methods).
```python
from bigtree import Node, Tree
diff --git a/tests/tree/test_parsing.py b/tests/tree/test_parsing.py
index 0f7f19a4..dc67ac45 100644
--- a/tests/tree/test_parsing.py
+++ b/tests/tree/test_parsing.py
@@ -103,7 +103,57 @@ def test_get_path(self):
actual_path == expected_path
), f"Wrong path for {node_pair}, expected {expected_path}, received {actual_path}"
+ def test_get_common_ancestors(self):
+ # Using get_common_ancestors from BaseNode
+ assert_tree_structure_basenode_root(self.a)
+ assert_tree_structure_basenode_root_attr(self.a)
+ assert_tree_structure_basenode_self(self)
+ assert_tree_structure_node_root(self.a)
+ assert_tree_structure_node_self(self)
+
+ expected_paths = [
+ ["a"],
+ ["a"],
+ ["a"],
+ ["a"],
+ ["a"],
+ ["a"],
+ ["a"],
+ ["b", "a"],
+ ["b", "a"],
+ ["b", "a"],
+ ["b", "a"],
+ ["a"],
+ ["a"],
+ ["b", "a"],
+ ["b", "a"],
+ ["b", "a"],
+ ["a"],
+ ["a"],
+ ["e", "b", "a"],
+ ["e", "b", "a"],
+ ["a"],
+ ["a"],
+ ["e", "b", "a"],
+ ["a"],
+ ["a"],
+ ["a"],
+ ["a"],
+ ["c", "a"],
+ ]
+ for node_pair, expected in zip(
+ combinations(list(iterators.preorder_iter(self.a)), 2), expected_paths
+ ):
+ actual = [
+ _node.node_name
+ for _node in node_pair[0].get_common_ancestors(node_pair[1])
+ ]
+ assert (
+ actual == expected
+ ), f"Wrong ancestors for {node_pair}, expected {expected}, received {actual}"
+
def test_go_to(self):
+ # Using go_to from BaseNode
assert_tree_structure_basenode_root(self.a)
assert_tree_structure_basenode_root_attr(self.a)
assert_tree_structure_basenode_self(self)
@@ -140,25 +190,21 @@ def test_go_to(self):
["h", "e", "b", "a", "c", "f"],
["c", "f"],
]
- for node_pair, expected_path in zip(
+ for node_pair, expected in zip(
combinations(list(iterators.preorder_iter(self.a)), 2), expected_paths
):
- actual_path = [
- _node.node_name for _node in node_pair[0].go_to(node_pair[1])
- ]
+ actual = [_node.node_name for _node in node_pair[0].go_to(node_pair[1])]
assert (
- actual_path == expected_path
- ), f"Wrong path for {node_pair}, expected {expected_path}, received {actual_path}"
+ actual == expected
+ ), f"Wrong path for {node_pair}, expected {expected}, received {actual}"
def test_get_path_same_node(self):
for _node in iterators.preorder_iter(self.a):
- actual_path = [
- _node1.node_name for _node1 in parsing.get_path(_node, _node)
- ]
- expected_path = [_node.node_name]
+ actual = [_node1.node_name for _node1 in parsing.get_path(_node, _node)]
+ expected = [_node.node_name]
assert (
- actual_path == expected_path
- ), f"Wrong path for {_node}, expected {expected_path}, received {actual_path}"
+ actual == expected
+ ), f"Wrong path for {_node}, expected {expected}, received {actual}"
def test_get_path_type_error(self):
source = node.Node("a")