Skip to content

Commit 7ceb875

Browse files
authored
Add nested dict key for tree construction and export (#395)
* feat: construct trees with nested_dict_key_to_tree * refactor: test clean up * feat: export trees with tree_to_nested_dict_key * feat: add docs * docs: update CHANGELOG * docs: enhance docstring
1 parent d839325 commit 7ceb875

File tree

14 files changed

+718
-96
lines changed

14 files changed

+718
-96
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ 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+
9+
## [0.30.0] - 2025-09-05
810
### Added:
11+
- Tree Construct: `nested_dict_key_to_tree` to construct trees with nested dictionary of another format.
12+
- Tree Export: `tree_to_nested_dict_key` to export trees to nested dictionary of another format.
913
- Tree Render: `render_tree` to accept theme argument.
1014
- Docs: Add pyvis html render to Tree Demonstration.
1115

@@ -799,7 +803,8 @@ ignore null attribute columns.
799803
- Utility Iterator: Tree traversal methods.
800804
- Workflow To Do App: Tree use case with to-do list implementation.
801805

802-
[Unreleased]: https://github.com/kayjan/bigtree/compare/0.29.2...HEAD
806+
[Unreleased]: https://github.com/kayjan/bigtree/compare/0.30.0...HEAD
807+
[0.30.0]: https://github.com/kayjan/bigtree/compare/0.29.2...0.30.0
803808
[0.29.2]: https://github.com/kayjan/bigtree/compare/0.29.1...0.29.2
804809
[0.29.1]: https://github.com/kayjan/bigtree/compare/0.29.0...0.29.1
805810
[0.29.0]: https://github.com/kayjan/bigtree/compare/0.28.0...0.29.0

assets/docs/tree_construct.png

4.23 KB
Loading

bigtree/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "0.29.2"
1+
__version__ = "0.30.0"
22

33
from bigtree.binarytree.construct import list_to_binarytree
44
from bigtree.dag.construct import dataframe_to_dag, dict_to_dag, list_to_dag
@@ -21,6 +21,7 @@
2121
dict_to_tree,
2222
list_to_tree,
2323
list_to_tree_by_relation,
24+
nested_dict_key_to_tree,
2425
nested_dict_to_tree,
2526
newick_to_tree,
2627
polars_to_tree,
@@ -37,6 +38,7 @@
3738
tree_to_dot,
3839
tree_to_mermaid,
3940
tree_to_nested_dict,
41+
tree_to_nested_dict_key,
4042
tree_to_newick,
4143
tree_to_pillow,
4244
tree_to_pillow_graph,

bigtree/tree/construct/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
add_dict_to_tree_by_name,
1313
add_dict_to_tree_by_path,
1414
dict_to_tree,
15+
nested_dict_key_to_tree,
1516
nested_dict_to_tree,
1617
)
1718
from .lists import list_to_tree, list_to_tree_by_relation
@@ -31,6 +32,7 @@
3132
"add_dict_to_tree_by_path",
3233
"dict_to_tree",
3334
"nested_dict_to_tree",
35+
"nested_dict_key_to_tree",
3436
"list_to_tree",
3537
"list_to_tree_by_relation",
3638
"render_tree",

bigtree/tree/construct/dictionaries.py

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"add_dict_to_tree_by_name",
1212
"dict_to_tree",
1313
"nested_dict_to_tree",
14+
"nested_dict_key_to_tree",
1415
]
1516

1617
T = TypeVar("T", bound=node.Node)
@@ -238,7 +239,7 @@ def nested_dict_to_tree(
238239
239240
Examples:
240241
>>> from bigtree import nested_dict_to_tree
241-
>>> path_dict = {
242+
>>> nested_dict = {
242243
... "name": "a",
243244
... "age": 90,
244245
... "children": [
@@ -252,7 +253,7 @@ def nested_dict_to_tree(
252253
... ]},
253254
... ],
254255
... }
255-
>>> root = nested_dict_to_tree(path_dict)
256+
>>> root = nested_dict_to_tree(nested_dict)
256257
>>> root.show(attr_list=["age"])
257258
a [age=90]
258259
└── b [age=65]
@@ -300,3 +301,88 @@ def _recursive_add_child(
300301

301302
root_node = _recursive_add_child(node_attrs)
302303
return root_node
304+
305+
306+
def nested_dict_key_to_tree(
307+
node_attrs: Mapping[str, Mapping[str, Any]],
308+
child_key: str = "children",
309+
node_type: Type[T] = node.Node, # type: ignore[assignment]
310+
) -> T:
311+
"""Construct tree from nested recursive dictionary, where the keys are node names.
312+
313+
- ``key``: node name
314+
- ``value``: dict of node attributes and node children (recursive)
315+
316+
Value dictionary
317+
318+
- ``key`` that is not ``child_key`` has node attribute as value
319+
- ``key`` that is ``child_key`` has dictionary of node children as value (recursive)
320+
321+
Examples:
322+
>>> from bigtree import nested_dict_key_to_tree
323+
>>> nested_dict = {
324+
... "a": {
325+
... "age": 90,
326+
... "children": {
327+
... "b": {
328+
... "age": 65,
329+
... "children": {
330+
... "d": {"age": 40},
331+
... "e": {
332+
... "age": 35,
333+
... "children": {"g": {"age": 10}},
334+
... },
335+
... },
336+
... },
337+
... },
338+
... }
339+
... }
340+
>>> root = nested_dict_key_to_tree(nested_dict)
341+
>>> root.show(attr_list=["age"])
342+
a [age=90]
343+
└── b [age=65]
344+
├── d [age=40]
345+
└── e [age=35]
346+
└── g [age=10]
347+
348+
Args:
349+
node_attrs: node, children, and node attribute information,
350+
key: node name
351+
value: dictionary of node attributes and node children
352+
child_key: key of child dict, value is type dict
353+
node_type: node type of tree to be created
354+
355+
Returns:
356+
Node
357+
"""
358+
assertions.assert_length(node_attrs, 1, "Dictionary", "node_attrs")
359+
360+
def _recursive_add_child(
361+
child_name: str, child_dict: Mapping[str, Any], parent_node: Optional[T] = None
362+
) -> T:
363+
"""Recursively add child to tree, given child attributes and parent node.
364+
365+
Args:
366+
child_name: child name to be added to tree
367+
child_dict: child to be added to tree, from dictionary
368+
parent_node: parent node to be assigned to child node
369+
370+
Returns:
371+
Node
372+
"""
373+
child_dict = dict(child_dict)
374+
node_children = child_dict.pop(child_key, {})
375+
if not isinstance(node_children, Mapping):
376+
raise TypeError(
377+
f"child_key {child_key} should be Dict type, received {node_children}"
378+
)
379+
root = node_type(child_name, parent=parent_node, **child_dict)
380+
for _child_name in node_children:
381+
_recursive_add_child(
382+
_child_name, node_children[_child_name], parent_node=root
383+
)
384+
return root
385+
386+
root_node_name = list(node_attrs.keys())[0]
387+
root_node = _recursive_add_child(root_node_name, node_attrs[root_node_name])
388+
return root_node

bigtree/tree/export/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
from .dataframes import tree_to_dataframe, tree_to_polars # noqa
2-
from .dictionaries import tree_to_dict, tree_to_nested_dict # noqa
2+
from .dictionaries import ( # noqa
3+
tree_to_dict,
4+
tree_to_nested_dict,
5+
tree_to_nested_dict_key,
6+
)
37
from .images import ( # noqa
48
tree_to_dot,
59
tree_to_mermaid,
@@ -22,6 +26,7 @@
2226
"tree_to_polars",
2327
"tree_to_dict",
2428
"tree_to_nested_dict",
29+
"tree_to_nested_dict_key",
2530
"tree_to_dot",
2631
"tree_to_mermaid",
2732
"tree_to_pillow",

bigtree/tree/export/dictionaries.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
__all__ = [
88
"tree_to_dict",
99
"tree_to_nested_dict",
10+
"tree_to_nested_dict_key",
1011
]
1112

1213
T = TypeVar("T", bound=node.Node)
@@ -165,3 +166,71 @@ def _recursive_append(_node: T, parent_dict: Dict[str, Any]) -> None:
165166

166167
_recursive_append(tree, data_dict)
167168
return data_dict[child_key][0]
169+
170+
171+
def tree_to_nested_dict_key(
172+
tree: T,
173+
child_key: str = "children",
174+
attr_dict: Optional[Dict[str, str]] = None,
175+
all_attrs: bool = False,
176+
max_depth: int = 0,
177+
) -> Dict[str, Any]:
178+
"""Export tree to nested dictionary, where the keys are node names.
179+
180+
All descendants from `tree` will be exported, `tree` can be the root node or child node of tree.
181+
182+
Exported dictionary will have key as node names, and children as node attributes and nested recursive dictionary.
183+
184+
Examples:
185+
>>> from bigtree import Node, tree_to_nested_dict_key
186+
>>> root = Node("a", age=90)
187+
>>> b = Node("b", age=65, parent=root)
188+
>>> c = Node("c", age=60, parent=root)
189+
>>> d = Node("d", age=40, parent=b)
190+
>>> e = Node("e", age=35, parent=b)
191+
>>> tree_to_nested_dict_key(root, all_attrs=True)
192+
{'a': {'age': 90, 'children': {'b': {'age': 65, 'children': {'d': {'age': 40}, 'e': {'age': 35}}}, 'c': {'age': 60}}}}
193+
194+
Args:
195+
tree: tree to be exported
196+
child_key: dictionary key for children
197+
attr_dict: node attributes mapped to dictionary key, key: node attributes, value: corresponding dictionary key
198+
all_attrs: indicator whether to retrieve all ``Node`` attributes, overrides `attr_dict`
199+
max_depth: maximum depth to export tree
200+
201+
Returns:
202+
Dictionary containing tree information
203+
"""
204+
data_dict: Dict[str, Dict[str, Any]] = {}
205+
206+
def _recursive_append(_node: T, parent_dict: Dict[str, Any]) -> None:
207+
"""Recursively iterate through node and its children to export to nested dictionary.
208+
209+
Args:
210+
_node: current node
211+
parent_dict: parent dictionary
212+
"""
213+
if _node:
214+
if not max_depth or _node.depth <= max_depth:
215+
data_child = {}
216+
if all_attrs:
217+
data_child.update(
218+
dict(
219+
_node.describe(
220+
exclude_attributes=["name"], exclude_prefix="_"
221+
)
222+
)
223+
)
224+
elif attr_dict:
225+
for k, v in attr_dict.items():
226+
data_child[v] = _node.get_attr(k)
227+
if child_key in parent_dict:
228+
parent_dict[child_key][_node.node_name] = data_child
229+
else:
230+
parent_dict[child_key] = {_node.node_name: data_child}
231+
232+
for _child in _node.children:
233+
_recursive_append(_child, data_child)
234+
235+
_recursive_append(tree, data_dict)
236+
return data_dict[child_key]

bigtree/utils/assertions.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,23 @@ def assert_length_not_empty(
134134
)
135135

136136

137+
def assert_length(
138+
data: Collection[Any], length: int, argument_name: str, argument: str
139+
) -> None:
140+
"""Raise ValueError if data does not have specific length.
141+
142+
Args:
143+
data: data to check
144+
length: length to check
145+
argument_name: argument name for data, for error message
146+
argument: argument for data, for error message
147+
"""
148+
if len(data) != length:
149+
raise ValueError(
150+
f"{argument_name} is not of length {length}, check `{argument}`"
151+
)
152+
153+
137154
def assert_dataframe_not_empty(data: pd.DataFrame) -> None:
138155
"""Raise ValueError is dataframe is empty.
139156

docs/bigtree/tree/construct.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ Construct Tree from list, dictionary, and pandas DataFrame.
1010

1111
To decide which method to use, consider your data type and data values.
1212

13-
| Construct tree from | Using full path | Using parent-child relation | Using notation | Add node attributes |
14-
|---------------------|---------------------|---------------------------------|------------------|---------------------------------------------------------|
15-
| String | `str_to_tree` | NA | `newick_to_tree` | No (for ` str_to_tree `)<br>Yes (for `newick_to_tree`) |
16-
| List | `list_to_tree` | ` list_to_tree_by_relation` | NA | No |
17-
| Dictionary | `dict_to_tree` | ` nested_dict_to_tree` | NA | Yes |
18-
| pandas DataFrame | `dataframe_to_tree` | `dataframe_to_tree_by_relation` | NA | Yes |
19-
| polars DataFrame | `polars_to_tree` | `polars_to_tree_by_relation` | NA | Yes |
20-
| Interactive UI | NA | `render_tree` | NA | No |
13+
| Construct tree from | Using full path | Using parent-child relation | Using notation | Add node attributes |
14+
|---------------------|---------------------|--------------------------------------------------|------------------|---------------------------------------------------------|
15+
| String | `str_to_tree` | NA | `newick_to_tree` | No (for ` str_to_tree `)<br>Yes (for `newick_to_tree`) |
16+
| List | `list_to_tree` | `list_to_tree_by_relation` | NA | No |
17+
| Dictionary | `dict_to_tree` | `nested_dict_to_tree`, `nested_dict_key_to_tree` | NA | Yes |
18+
| pandas DataFrame | `dataframe_to_tree` | `dataframe_to_tree_by_relation` | NA | Yes |
19+
| polars DataFrame | `polars_to_tree` | `polars_to_tree_by_relation` | NA | Yes |
20+
| Interactive UI | NA | `render_tree` | NA | No |
2121

2222
## Tree Add Attributes Methods
2323

0 commit comments

Comments
 (0)