Skip to content

Commit 93dcaba

Browse files
authored
Construct/Export to nested_dict_key to support child_key=None (#400)
* feat: nested_key_dict to tree to support child_key=None * feat: tree to nested_key_dict to support child_key=None * docs: update CHANGELOG and docs * fix: fix doctest
1 parent 2fc9e32 commit 93dcaba

File tree

9 files changed

+235
-18
lines changed

9 files changed

+235
-18
lines changed

CHANGELOG.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ All notable changes to this project will be documented in this file.
44
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

7-
## [Unreleased]
7+
## [0.30.1] - 2025-09-10
8+
### Added:
9+
- Tree Construct: `nested_dict_key_to_tree` to support child_key=None.
10+
- Tree Export: `tree_to_nested_dict_key` to support child_key=None.
811
### Changed
912
- Misc: Some code refactoring, enhance assemble_attributes.
1013
### Fixed
@@ -808,7 +811,8 @@ ignore null attribute columns.
808811
- Utility Iterator: Tree traversal methods.
809812
- Workflow To Do App: Tree use case with to-do list implementation.
810813

811-
[Unreleased]: https://github.com/kayjan/bigtree/compare/0.30.0...HEAD
814+
[Unreleased]: https://github.com/kayjan/bigtree/compare/0.30.1...HEAD
815+
[0.30.1]: https://github.com/kayjan/bigtree/compare/0.30.0...0.30.1
812816
[0.30.0]: https://github.com/kayjan/bigtree/compare/0.29.2...0.30.0
813817
[0.29.2]: https://github.com/kayjan/bigtree/compare/0.29.1...0.29.2
814818
[0.29.1]: https://github.com/kayjan/bigtree/compare/0.29.0...0.29.1

bigtree/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "0.30.0"
1+
__version__ = "0.30.1"
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

bigtree/dag/construct.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@ def list_to_dag(
4040
assertions.assert_length_not_empty(relations, "Input list", "relations")
4141

4242
node_dict: Dict[str, T] = dict()
43-
child_name: str = ""
43+
parent_name: str = ""
4444

4545
for parent_name, child_name in relations:
4646
node_dict[parent_name] = node_dict.get(parent_name, node_type(parent_name))
4747
node_dict[child_name] = node_dict.get(child_name, node_type(child_name))
4848
node_dict[child_name].parents = [node_dict[parent_name]]
4949

50-
return node_dict[child_name]
50+
return node_dict[parent_name]
5151

5252

5353
def dict_to_dag(

bigtree/tree/construct/dictionaries.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -305,11 +305,13 @@ def _recursive_add_child(
305305

306306
def nested_dict_key_to_tree(
307307
node_attrs: Mapping[str, Mapping[str, Any]],
308-
child_key: str = "children",
308+
child_key: Optional[str] = "children",
309309
node_type: Type[T] = node.Node, # type: ignore[assignment]
310310
) -> T:
311311
"""Construct tree from nested recursive dictionary, where the keys are node names.
312312
313+
If child_key is a string
314+
313315
- ``key``: node name
314316
- ``value``: dict of node attributes and node children (recursive)
315317
@@ -318,6 +320,15 @@ def nested_dict_key_to_tree(
318320
- ``key`` that is not ``child_key`` has node attribute as value
319321
- ``key`` that is ``child_key`` has dictionary of node children as value (recursive)
320322
323+
---
324+
325+
If child_key is None
326+
327+
- ``key``: node name
328+
- ``value``: dict of node children (recursive), there are no node attributes
329+
330+
Value dictionary consist of ``key`` that is node names of children
331+
321332
Examples:
322333
>>> from bigtree import nested_dict_key_to_tree
323334
>>> nested_dict = {
@@ -345,6 +356,23 @@ def nested_dict_key_to_tree(
345356
└── e [age=35]
346357
└── g [age=10]
347358
359+
>>> from bigtree import nested_dict_key_to_tree
360+
>>> nested_dict = {
361+
... "a": {
362+
... "b": {
363+
... "d": {},
364+
... "e": {"g": {}},
365+
... },
366+
... }
367+
... }
368+
>>> root = nested_dict_key_to_tree(nested_dict, child_key=None)
369+
>>> root.show()
370+
a
371+
└── b
372+
├── d
373+
└── e
374+
└── g
375+
348376
Args:
349377
node_attrs: node, children, and node attribute information,
350378
key: node name
@@ -370,8 +398,12 @@ def _recursive_add_child(
370398
Returns:
371399
Node
372400
"""
373-
child_dict = dict(child_dict)
374-
node_children = child_dict.pop(child_key, {})
401+
if child_key:
402+
child_dict = dict(child_dict)
403+
node_children = child_dict.pop(child_key, {})
404+
else:
405+
node_children = child_dict
406+
child_dict = {}
375407
if not isinstance(node_children, Mapping):
376408
raise TypeError(
377409
f"child_key {child_key} should be Dict type, received {node_children}"

bigtree/tree/export/dictionaries.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def _recursive_append(_node: T, parent_dict: Dict[str, Any]) -> None:
150150

151151
def tree_to_nested_dict_key(
152152
tree: T,
153-
child_key: str = "children",
153+
child_key: Optional[str] = "children",
154154
attr_dict: Optional[Dict[str, str]] = None,
155155
all_attrs: bool = False,
156156
max_depth: int = 0,
@@ -160,6 +160,7 @@ def tree_to_nested_dict_key(
160160
All descendants from `tree` will be exported, `tree` can be the root node or child node of tree.
161161
162162
Exported dictionary will have key as node names, and children as node attributes and nested recursive dictionary.
163+
If child_key is None, the children key is nested recursive dictionary of node names (there will be no attributes).
163164
164165
Examples:
165166
>>> from bigtree import Node, tree_to_nested_dict_key
@@ -171,6 +172,9 @@ def tree_to_nested_dict_key(
171172
>>> tree_to_nested_dict_key(root, all_attrs=True)
172173
{'a': {'age': 90, 'children': {'b': {'age': 65, 'children': {'d': {'age': 40}, 'e': {'age': 35}}}, 'c': {'age': 60}}}}
173174
175+
>>> tree_to_nested_dict_key(root, child_key=None)
176+
{'a': {'b': {'d': {}, 'e': {}}, 'c': {}}}
177+
174178
Args:
175179
tree: tree to be exported
176180
child_key: dictionary key for children
@@ -190,16 +194,25 @@ def _recursive_append(_node: T, parent_dict: Dict[str, Any]) -> None:
190194
_node: current node
191195
parent_dict: parent dictionary
192196
"""
197+
if child_key is None:
198+
if attr_dict or all_attrs:
199+
raise ValueError(
200+
"If child_key is None, no node attributes can be exported"
201+
)
202+
193203
if _node:
194204
if not max_depth or _node.depth <= max_depth:
195205
data_child = common.assemble_attributes(_node, attr_dict, all_attrs)
196-
if child_key in parent_dict:
197-
parent_dict[child_key][_node.node_name] = data_child
206+
if child_key:
207+
if child_key in parent_dict:
208+
parent_dict[child_key][_node.node_name] = data_child
209+
else:
210+
parent_dict[child_key] = {_node.node_name: data_child}
198211
else:
199-
parent_dict[child_key] = {_node.node_name: data_child}
212+
parent_dict[_node.node_name] = data_child
200213

201214
for _child in _node.children:
202215
_recursive_append(_child, data_child)
203216

204217
_recursive_append(tree, data_dict)
205-
return data_dict[child_key]
218+
return data_dict[child_key] if child_key else data_dict

docs/gettingstarted/demo/tree.md

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,8 @@ names and `value` is node attribute values, and list of children (recursive).
229229
# └── c [age=60]
230230
```
231231

232-
=== "Recursive structure 2"
233-
```python hl_lines="17"
232+
=== "Recursive structure by key"
233+
```python hl_lines="17 31"
234234
from bigtree import nested_dict_key_to_tree
235235

236236
nested_dict = {
@@ -254,6 +254,20 @@ names and `value` is node attribute values, and list of children (recursive).
254254
# ├── b [age=65]
255255
# │ └── d [age=40]
256256
# └── c [age=60]
257+
258+
nested_dict = {
259+
"a": {
260+
"b": {"d": {}},
261+
"c": {},
262+
}
263+
}
264+
root = nested_dict_key_to_tree(nested_dict, child_key=None)
265+
266+
root.show()
267+
# a
268+
# ├── b
269+
# │ └── d
270+
# └── c
257271
```
258272

259273

@@ -1294,15 +1308,18 @@ root.show()
12941308
# }
12951309
```
12961310

1297-
=== "Dictionary (recursive structure 2)"
1298-
```python hl_lines="3"
1311+
=== "Dictionary (recursive structure by key)"
1312+
```python hl_lines="3 9"
12991313
from bigtree import tree_to_nested_dict_key
13001314

13011315
tree_to_nested_dict_key(root, all_attrs=True)
13021316
# {'a': {'age': 90,
13031317
# 'children': {'b': {'age': 65,
13041318
# 'children': {'d': {'age': 40}, 'e': {'age': 35}}},
13051319
# 'c': {'age': 60}}}}
1320+
1321+
tree_to_nested_dict_key(root, child_key=None)
1322+
# {'a': {'b': {'d': {}, 'e': {}}, 'c': {}}}
13061323
```
13071324

13081325
=== "pandas DataFrame"

tests/test_constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ class Constants:
136136

137137
# tree/export
138138
ERROR_NODE_TYPE = "Tree should be of type `{type}`, or inherit from `{type}`"
139+
ERROR_NODE_EXPORT_DICT_NO_ATTRS = (
140+
"If child_key is None, no node attributes can be exported"
141+
)
139142
ERROR_NODE_EXPORT_PRINT_ATTR_BRACKET = (
140143
"Expect open and close brackets in `attr_bracket`, received {attr_bracket}"
141144
)

tests/tree/construct/test_dictionaries.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,3 +1016,93 @@ def test_nested_dict_key_to_tree_custom_node_type():
10161016
assert_tree_structure_basenode_root(root)
10171017
assert_tree_structure_customnode_root_attr(root)
10181018
assert_tree_structure_node_root(root)
1019+
1020+
1021+
class TestNestedDictKeyToTreeNullKey(unittest.TestCase):
1022+
def setUp(self):
1023+
"""
1024+
Tree should have structure
1025+
a
1026+
|-- b
1027+
| |-- d
1028+
| +-- e
1029+
| |-- g
1030+
| +-- h
1031+
+-- c
1032+
+-- f
1033+
"""
1034+
self.nested_dict = {
1035+
"a": {
1036+
"b": {
1037+
"d": {},
1038+
"e": {
1039+
"g": {},
1040+
"h": {},
1041+
},
1042+
},
1043+
"c": {"f": {}},
1044+
}
1045+
}
1046+
1047+
def tearDown(self):
1048+
self.nested_dict = None
1049+
1050+
def test_nested_dict_key_to_tree(self):
1051+
root = construct.nested_dict_key_to_tree(self.nested_dict, child_key=None)
1052+
assert_tree_structure_basenode_root(root)
1053+
assert_tree_structure_node_root(root)
1054+
1055+
@staticmethod
1056+
def test_nested_dict_key_to_tree_null_children_error():
1057+
child_key = None
1058+
child = None
1059+
nested_dict = {
1060+
"a": {
1061+
"b": {
1062+
"d": {},
1063+
"e": {
1064+
"g": child,
1065+
"h": {},
1066+
},
1067+
},
1068+
"c": {"f": {}},
1069+
}
1070+
}
1071+
with pytest.raises(TypeError) as exc_info:
1072+
construct.nested_dict_key_to_tree(nested_dict, child_key=child_key)
1073+
assert str(exc_info.value) == Constants.ERROR_NODE_DICT_CHILD_TYPE.format(
1074+
child_key=child_key, type="Dict", child=child
1075+
)
1076+
1077+
@staticmethod
1078+
def test_nested_dict_key_to_tree_int_children_error():
1079+
child_key = None
1080+
child = 1
1081+
nested_dict = {
1082+
"a": {
1083+
"b": {
1084+
"d": {},
1085+
"e": {
1086+
"g": child,
1087+
"h": {},
1088+
},
1089+
},
1090+
"c": {"f": {}},
1091+
}
1092+
}
1093+
with pytest.raises(TypeError) as exc_info:
1094+
construct.nested_dict_key_to_tree(nested_dict, child_key=child_key)
1095+
assert str(exc_info.value) == Constants.ERROR_NODE_DICT_CHILD_TYPE.format(
1096+
child_key=child_key, type="Dict", child=child
1097+
)
1098+
1099+
def test_nested_dict_key_to_tree_node_type(self):
1100+
root = construct.nested_dict_key_to_tree(
1101+
self.nested_dict, child_key=None, node_type=NodeA
1102+
)
1103+
assert isinstance(root, NodeA), Constants.ERROR_CUSTOM_TYPE.format(type="NodeA")
1104+
assert all(
1105+
isinstance(_node, NodeA) for _node in root.children
1106+
), Constants.ERROR_CUSTOM_TYPE.format(type="NodeA")
1107+
assert_tree_structure_basenode_root(root)
1108+
assert_tree_structure_node_root(root)

0 commit comments

Comments
 (0)