Skip to content

Commit 5e57623

Browse files
authored
Construct tree from rich tree using Tree.from_rich (#438)
* test: rename variables * test: rename variables + ensure tree helper methods return Tree type * docs: enhance docstring * feat: add get_subtree as plugin * docs: enhance docstring * docs: update CHANGELOG * test: rename variables * feat: get_attr to support nested attr * bump: v1.3.0 * test: handle warning from polar tests * test: handle warning from polar tests * fix: handle pandas v3 * fix: handle pandas v3 * fix: handle pandas v3 * fix: handle pandas v3 * docs: more examples for listing directories * docs: more examples for listing directories * docs: more examples for listing directories * docs: Fix CHANGELOG * bump: update CHANGELOG * fix: Fix attr square brackets being confused as rich formatting * bump: v1.3.1 * feat: construct tree using rich using Tree.from_rich * docs: Update docs * feat: add exceptions
1 parent b6a4341 commit 5e57623

File tree

15 files changed

+205
-13
lines changed

15 files changed

+205
-13
lines changed

CHANGELOG.md

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8-
### Added:
9-
- Docs: More examples for listing directories.
10-
### Fixed:
11-
- Docs: Some grammar improvements in docstring.
12-
- Setup: Test with different Python environments.
138

14-
## [1.3.1] - 2026-02-13
9+
## [1.3.1] - 2026-02-14
1510
### Added:
11+
- Tree Construct: Construct tree from rich tree using `rich_to_tree` or `Tree.from_rich`.
1612
- Docs: More examples for listing directories.
1713
### Fixed:
1814
- Tree Export: Fix attr square brackets being confused as rich formatting.
@@ -905,7 +901,8 @@ ignore null attribute columns.
905901
- Utility Iterator: Tree traversal methods.
906902
- Workflow To Do App: Tree use case with to-do list implementation.
907903

908-
[Unreleased]: https://github.com/kayjan/bigtree/compare/1.3.0...HEAD
904+
[Unreleased]: https://github.com/kayjan/bigtree/compare/1.3.1...HEAD
905+
[1.3.1]: https://github.com/kayjan/bigtree/compare/1.3.0...1.3.1
909906
[1.3.0]: https://github.com/kayjan/bigtree/compare/1.2.0...1.3.0
910907
[1.2.0]: https://github.com/kayjan/bigtree/compare/1.1.0...1.2.0
911908
[1.1.0]: https://github.com/kayjan/bigtree/compare/1.0.4...1.1.0

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,10 @@ For **Tree** implementation, there are 12 main components.
4242
5. From *pandas DataFrame*, using paths or parent-child columns
4343
6. From *polars DataFrame*, using paths or parent-child columns
4444
7. From *interactive UI*
45-
8. Add nodes to existing tree using path string
46-
9. Add nodes and attributes to existing tree using *dictionary*, *pandas DataFrame*, or *polars DataFrame*, using path
47-
10. Add only attributes to existing tree using *dictionary*, *pandas DataFrame*, or *polars DataFrame*, using node name
45+
8. From *rich trees*
46+
9. Add nodes to existing tree using path string
47+
10. Add nodes and attributes to existing tree using *dictionary*, *pandas DataFrame*, or *polars DataFrame*, using path
48+
11. Add only attributes to existing tree using *dictionary*, *pandas DataFrame*, or *polars DataFrame*, using node name
4849
4. [**➰ Traversing Tree**](https://bigtree.readthedocs.io/stable/bigtree/utils/iterators/)
4950
1. Pre-Order Traversal
5051
2. Post-Order Traversal

bigtree/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
polars_to_tree,
3131
polars_to_tree_by_relation,
3232
render_tree,
33+
rich_to_tree,
3334
str_to_tree,
3435
)
3536
from bigtree.tree.export import (

bigtree/_plugins.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ def register_tree_plugins() -> None:
8181
"from_list_relation": construct.list_to_tree_by_relation,
8282
"from_str": construct.str_to_tree,
8383
"from_newick": construct.newick_to_tree,
84+
"from_rich": construct.rich_to_tree,
8485
},
8586
method="class",
8687
)

bigtree/tree/construct/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
)
1818
from .lists import list_to_tree, list_to_tree_by_relation
1919
from .render import render_tree
20-
from .strings import add_path_to_tree, newick_to_tree, str_to_tree
20+
from .strings import add_path_to_tree, newick_to_tree, rich_to_tree, str_to_tree
2121

2222
__all__ = [
2323
"add_dataframe_to_tree_by_name",
@@ -38,5 +38,6 @@
3838
"render_tree",
3939
"add_path_to_tree",
4040
"newick_to_tree",
41+
"rich_to_tree",
4142
"str_to_tree",
4243
]

bigtree/tree/construct/strings.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,19 @@
88
from bigtree.tree import search
99
from bigtree.utils import assertions, constants, exceptions
1010

11+
try:
12+
import rich
13+
except ImportError: # pragma: no cover
14+
from unittest.mock import MagicMock
15+
16+
rich = MagicMock()
17+
18+
1119
__all__ = [
1220
"add_path_to_tree",
1321
"str_to_tree",
1422
"newick_to_tree",
23+
"rich_to_tree",
1524
]
1625

1726
T = TypeVar("T", bound=node.Node)
@@ -469,3 +478,61 @@ def _raise_value_error(tree_idx: int) -> None:
469478
current_depth,
470479
)
471480
return current_node
481+
482+
483+
@exceptions.optional_dependencies_rich
484+
def rich_to_tree(
485+
rich_tree: rich.tree.Tree, node_format_attr: str = "style"
486+
) -> node.Node:
487+
"""Construct tree from rich tree to allow more tree operations, return root of tree.
488+
489+
If the rich node label is of type rich.text.Text, it will save the style, if any, as
490+
a node attribute. This does not support inferring rich node label string using
491+
square brackets.
492+
493+
Examples:
494+
>>> from bigtree import Tree
495+
>>> from rich.tree import Tree as RichTree
496+
>>> from rich.text import Text
497+
>>> rich_root = RichTree(Text("Grandparent", style="magenta"))
498+
>>> child = rich_root.add("Child")
499+
>>> _ = child.add(Text("Grandchild", style="red"))
500+
>>> _ = rich_root.add("Child 2")
501+
>>> tree = Tree.rich_to_tree(rich_root)
502+
>>> tree.show(all_attrs=True) # Try with rich=True, node_format_attr="style"
503+
Grandparent [style=magenta]
504+
├── Child
505+
│ └── Grandchild [style=red]
506+
└── Child 2
507+
508+
Args:
509+
rich_tree: rich Tree
510+
node_format_attr: attribute name for the node format
511+
512+
Returns:
513+
Node
514+
"""
515+
516+
def extract_label_and_style(label: rich.text.Text | str) -> tuple[str, str | None]:
517+
"""Extract label and list of styles spans from a rich label.
518+
519+
Returns:
520+
label and style (if label is rich.text.Text and has style)
521+
"""
522+
if isinstance(label, rich.text.Text):
523+
return label.plain, label.style
524+
else:
525+
return str(label), None
526+
527+
def convert_node(
528+
rich_node: rich.tree.Tree, parent: node.Node | None = None
529+
) -> node.Node:
530+
node_name, node_format = extract_label_and_style(rich_node.label)
531+
_node = node.Node(node_name, parent=parent)
532+
if node_format:
533+
_node.set_attrs({node_format_attr: node_format})
534+
for child in rich_node.children:
535+
convert_node(child, parent=_node)
536+
return _node
537+
538+
return convert_node(rich_tree)

bigtree/tree/export/stdout.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,13 @@ def print_tree(
277277
# Handle specific case where [ will be mistaken as rich formatting
278278
attr_bracket = ("\\" + attr_bracket[0], attr_bracket[1])
279279

280+
# Handle specific case where [ will be mistaken as rich formatting
281+
attr_bracket = (
282+
("\\" + attr_bracket[0], attr_bracket[1])
283+
if attr_bracket[0] == "["
284+
else attr_bracket
285+
)
286+
280287
for pre_str, fill_str, _node in yield_tree(
281288
tree=tree,
282289
node_name_or_path=node_name_or_path,

docs/bigtree/tree/construct.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ To decide which method to use, consider your data type and data values.
1919
| polars DataFrame | `polars_to_tree` | `polars_to_tree_by_relation` | NA | Yes |
2020
| Interactive UI | NA | `render_tree` | NA | No |
2121

22+
| Construct tree from | Notation | Add node attributes |
23+
|---------------------|----------------|-----------------------|
24+
| rich Tree | `rich_to_tree` | Only style attributes |
25+
2226
## Tree Add Attributes Methods
2327

2428
To add attributes to an existing tree,

docs/bigtree/tree/tree.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ methods.
2525
| pandas DataFrame | `Tree.from_dataframe` | `Tree.from_dataframe_relation` | NA | Yes |
2626
| polars DataFrame | `Tree.from_polars` | `Tree.from_polars_relation` | NA | Yes |
2727

28+
| Construct tree from | Notation | Add node attributes |
29+
|---------------------|---------------------|-----------------------|
30+
| rich Tree | `Tree.from_rich` | Only style attributes |
31+
2832
To add attributes to an existing tree,
2933

3034
| Add attributes from | Using full path | Using node name |

docs/gettingstarted/demo/tree.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,31 @@ Construct nodes with attributes. *DataFrame* can contain either <mark>path colum
377377
# └── c [age=60]
378378
```
379379

380+
### 6. From rich Trees
381+
382+
Convert rich.tree.Tree to bigtree Trees.
383+
384+
=== "rich"
385+
```python hl_lines="10"
386+
from rich.tree import Tree as RichTree
387+
from rich.text import Text
388+
389+
from bigtree import Tree
390+
391+
rich_root = RichTree(Text("a", style="magenta"))
392+
b = rich_root.add(Text("b", style="red"))
393+
d = b.add("d")
394+
c = rich_root.add(Text("c", style="red"))
395+
tree = Tree.from_rich(rich_root)
396+
397+
# Style does not show up in documentation
398+
tree.show(rich=True, node_format_attr="style")
399+
# a
400+
# ├── b
401+
# │ └── d
402+
# └── c
403+
```
404+
380405
!!! note
381406

382407
If tree is already created, nodes can still be added using path string, dictionary, and pandas/polars DataFrame!<br>
@@ -544,7 +569,7 @@ Other customisations for printing are also available, such as:
544569
```
545570
=== "Rich Render"
546571
```python
547-
# Does not show up in documentation
572+
# Style does not show up in documentation
548573
tree.show(rich=True, node_format="bold magenta", edge_format="blue")
549574
# a
550575
# ├── b

0 commit comments

Comments
 (0)