Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ All notable changes to this project will be documented in this file.
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]
## [0.30.1] - 2025-09-10
### Added:
- Tree Construct: `nested_dict_key_to_tree` to support child_key=None.
- Tree Export: `tree_to_nested_dict_key` to support child_key=None.
### Changed
- Misc: Some code refactoring, enhance assemble_attributes.
### Fixed
Expand Down Expand Up @@ -808,7 +811,8 @@ ignore null attribute columns.
- Utility Iterator: Tree traversal methods.
- Workflow To Do App: Tree use case with to-do list implementation.

[Unreleased]: https://github.com/kayjan/bigtree/compare/0.30.0...HEAD
[Unreleased]: https://github.com/kayjan/bigtree/compare/0.30.1...HEAD
[0.30.1]: https://github.com/kayjan/bigtree/compare/0.30.0...0.30.1
[0.30.0]: https://github.com/kayjan/bigtree/compare/0.29.2...0.30.0
[0.29.2]: https://github.com/kayjan/bigtree/compare/0.29.1...0.29.2
[0.29.1]: https://github.com/kayjan/bigtree/compare/0.29.0...0.29.1
Expand Down
2 changes: 1 addition & 1 deletion bigtree/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "0.30.0"
__version__ = "0.30.1"

from bigtree.binarytree.construct import list_to_binarytree
from bigtree.dag.construct import dataframe_to_dag, dict_to_dag, list_to_dag
Expand Down
4 changes: 2 additions & 2 deletions bigtree/dag/construct.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ def list_to_dag(
assertions.assert_length_not_empty(relations, "Input list", "relations")

node_dict: Dict[str, T] = dict()
child_name: str = ""
parent_name: str = ""

for parent_name, child_name in relations:
node_dict[parent_name] = node_dict.get(parent_name, node_type(parent_name))
node_dict[child_name] = node_dict.get(child_name, node_type(child_name))
node_dict[child_name].parents = [node_dict[parent_name]]

return node_dict[child_name]
return node_dict[parent_name]


def dict_to_dag(
Expand Down
38 changes: 35 additions & 3 deletions bigtree/tree/construct/dictionaries.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,11 +305,13 @@ def _recursive_add_child(

def nested_dict_key_to_tree(
node_attrs: Mapping[str, Mapping[str, Any]],
child_key: str = "children",
child_key: Optional[str] = "children",
node_type: Type[T] = node.Node, # type: ignore[assignment]
) -> T:
"""Construct tree from nested recursive dictionary, where the keys are node names.

If child_key is a string

- ``key``: node name
- ``value``: dict of node attributes and node children (recursive)

Expand All @@ -318,6 +320,15 @@ def nested_dict_key_to_tree(
- ``key`` that is not ``child_key`` has node attribute as value
- ``key`` that is ``child_key`` has dictionary of node children as value (recursive)

---

If child_key is None

- ``key``: node name
- ``value``: dict of node children (recursive), there are no node attributes

Value dictionary consist of ``key`` that is node names of children

Examples:
>>> from bigtree import nested_dict_key_to_tree
>>> nested_dict = {
Expand Down Expand Up @@ -345,6 +356,23 @@ def nested_dict_key_to_tree(
└── e [age=35]
└── g [age=10]

>>> from bigtree import nested_dict_key_to_tree
>>> nested_dict = {
... "a": {
... "b": {
... "d": {},
... "e": {"g": {}},
... },
... }
... }
>>> root = nested_dict_key_to_tree(nested_dict, child_key=None)
>>> root.show()
a
└── b
├── d
└── e
└── g

Args:
node_attrs: node, children, and node attribute information,
key: node name
Expand All @@ -370,8 +398,12 @@ def _recursive_add_child(
Returns:
Node
"""
child_dict = dict(child_dict)
node_children = child_dict.pop(child_key, {})
if child_key:
child_dict = dict(child_dict)
node_children = child_dict.pop(child_key, {})
else:
node_children = child_dict
child_dict = {}
if not isinstance(node_children, Mapping):
raise TypeError(
f"child_key {child_key} should be Dict type, received {node_children}"
Expand Down
23 changes: 18 additions & 5 deletions bigtree/tree/export/dictionaries.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def _recursive_append(_node: T, parent_dict: Dict[str, Any]) -> None:

def tree_to_nested_dict_key(
tree: T,
child_key: str = "children",
child_key: Optional[str] = "children",
attr_dict: Optional[Dict[str, str]] = None,
all_attrs: bool = False,
max_depth: int = 0,
Expand All @@ -160,6 +160,7 @@ def tree_to_nested_dict_key(
All descendants from `tree` will be exported, `tree` can be the root node or child node of tree.

Exported dictionary will have key as node names, and children as node attributes and nested recursive dictionary.
If child_key is None, the children key is nested recursive dictionary of node names (there will be no attributes).

Examples:
>>> from bigtree import Node, tree_to_nested_dict_key
Expand All @@ -171,6 +172,9 @@ def tree_to_nested_dict_key(
>>> tree_to_nested_dict_key(root, all_attrs=True)
{'a': {'age': 90, 'children': {'b': {'age': 65, 'children': {'d': {'age': 40}, 'e': {'age': 35}}}, 'c': {'age': 60}}}}

>>> tree_to_nested_dict_key(root, child_key=None)
{'a': {'b': {'d': {}, 'e': {}}, 'c': {}}}

Args:
tree: tree to be exported
child_key: dictionary key for children
Expand All @@ -190,16 +194,25 @@ def _recursive_append(_node: T, parent_dict: Dict[str, Any]) -> None:
_node: current node
parent_dict: parent dictionary
"""
if child_key is None:
if attr_dict or all_attrs:
raise ValueError(
"If child_key is None, no node attributes can be exported"
)

if _node:
if not max_depth or _node.depth <= max_depth:
data_child = common.assemble_attributes(_node, attr_dict, all_attrs)
if child_key in parent_dict:
parent_dict[child_key][_node.node_name] = data_child
if child_key:
if child_key in parent_dict:
parent_dict[child_key][_node.node_name] = data_child
else:
parent_dict[child_key] = {_node.node_name: data_child}
else:
parent_dict[child_key] = {_node.node_name: data_child}
parent_dict[_node.node_name] = data_child

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

_recursive_append(tree, data_dict)
return data_dict[child_key]
return data_dict[child_key] if child_key else data_dict
25 changes: 21 additions & 4 deletions docs/gettingstarted/demo/tree.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,8 @@ names and `value` is node attribute values, and list of children (recursive).
# └── c [age=60]
```

=== "Recursive structure 2"
```python hl_lines="17"
=== "Recursive structure by key"
```python hl_lines="17 31"
from bigtree import nested_dict_key_to_tree

nested_dict = {
Expand All @@ -254,6 +254,20 @@ names and `value` is node attribute values, and list of children (recursive).
# ├── b [age=65]
# │ └── d [age=40]
# └── c [age=60]

nested_dict = {
"a": {
"b": {"d": {}},
"c": {},
}
}
root = nested_dict_key_to_tree(nested_dict, child_key=None)

root.show()
# a
# ├── b
# │ └── d
# └── c
```


Expand Down Expand Up @@ -1294,15 +1308,18 @@ root.show()
# }
```

=== "Dictionary (recursive structure 2)"
```python hl_lines="3"
=== "Dictionary (recursive structure by key)"
```python hl_lines="3 9"
from bigtree import tree_to_nested_dict_key

tree_to_nested_dict_key(root, all_attrs=True)
# {'a': {'age': 90,
# 'children': {'b': {'age': 65,
# 'children': {'d': {'age': 40}, 'e': {'age': 35}}},
# 'c': {'age': 60}}}}

tree_to_nested_dict_key(root, child_key=None)
# {'a': {'b': {'d': {}, 'e': {}}, 'c': {}}}
```

=== "pandas DataFrame"
Expand Down
3 changes: 3 additions & 0 deletions tests/test_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ class Constants:

# tree/export
ERROR_NODE_TYPE = "Tree should be of type `{type}`, or inherit from `{type}`"
ERROR_NODE_EXPORT_DICT_NO_ATTRS = (
"If child_key is None, no node attributes can be exported"
)
ERROR_NODE_EXPORT_PRINT_ATTR_BRACKET = (
"Expect open and close brackets in `attr_bracket`, received {attr_bracket}"
)
Expand Down
90 changes: 90 additions & 0 deletions tests/tree/construct/test_dictionaries.py
Original file line number Diff line number Diff line change
Expand Up @@ -1016,3 +1016,93 @@ def test_nested_dict_key_to_tree_custom_node_type():
assert_tree_structure_basenode_root(root)
assert_tree_structure_customnode_root_attr(root)
assert_tree_structure_node_root(root)


class TestNestedDictKeyToTreeNullKey(unittest.TestCase):
def setUp(self):
"""
Tree should have structure
a
|-- b
| |-- d
| +-- e
| |-- g
| +-- h
+-- c
+-- f
"""
self.nested_dict = {
"a": {
"b": {
"d": {},
"e": {
"g": {},
"h": {},
},
},
"c": {"f": {}},
}
}

def tearDown(self):
self.nested_dict = None

def test_nested_dict_key_to_tree(self):
root = construct.nested_dict_key_to_tree(self.nested_dict, child_key=None)
assert_tree_structure_basenode_root(root)
assert_tree_structure_node_root(root)

@staticmethod
def test_nested_dict_key_to_tree_null_children_error():
child_key = None
child = None
nested_dict = {
"a": {
"b": {
"d": {},
"e": {
"g": child,
"h": {},
},
},
"c": {"f": {}},
}
}
with pytest.raises(TypeError) as exc_info:
construct.nested_dict_key_to_tree(nested_dict, child_key=child_key)
assert str(exc_info.value) == Constants.ERROR_NODE_DICT_CHILD_TYPE.format(
child_key=child_key, type="Dict", child=child
)

@staticmethod
def test_nested_dict_key_to_tree_int_children_error():
child_key = None
child = 1
nested_dict = {
"a": {
"b": {
"d": {},
"e": {
"g": child,
"h": {},
},
},
"c": {"f": {}},
}
}
with pytest.raises(TypeError) as exc_info:
construct.nested_dict_key_to_tree(nested_dict, child_key=child_key)
assert str(exc_info.value) == Constants.ERROR_NODE_DICT_CHILD_TYPE.format(
child_key=child_key, type="Dict", child=child
)

def test_nested_dict_key_to_tree_node_type(self):
root = construct.nested_dict_key_to_tree(
self.nested_dict, child_key=None, node_type=NodeA
)
assert isinstance(root, NodeA), Constants.ERROR_CUSTOM_TYPE.format(type="NodeA")
assert all(
isinstance(_node, NodeA) for _node in root.children
), Constants.ERROR_CUSTOM_TYPE.format(type="NodeA")
assert_tree_structure_basenode_root(root)
assert_tree_structure_node_root(root)
Loading