diff --git a/CHANGELOG.md b/CHANGELOG.md
index e5b19532..c45f9a4f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,11 @@ 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.0] - 2025-09-05
### Added:
+- Tree Construct: `nested_dict_key_to_tree` to construct trees with nested dictionary of another format.
+- Tree Export: `tree_to_nested_dict_key` to export trees to nested dictionary of another format.
- Tree Render: `render_tree` to accept theme argument.
- Docs: Add pyvis html render to Tree Demonstration.
@@ -799,7 +803,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.29.2...HEAD
+[Unreleased]: https://github.com/kayjan/bigtree/compare/0.30.0...HEAD
+[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
[0.29.0]: https://github.com/kayjan/bigtree/compare/0.28.0...0.29.0
diff --git a/assets/docs/tree_construct.png b/assets/docs/tree_construct.png
index a5c5830b..0ef7afd4 100644
Binary files a/assets/docs/tree_construct.png and b/assets/docs/tree_construct.png differ
diff --git a/bigtree/__init__.py b/bigtree/__init__.py
index 2bcd516a..94bffa21 100644
--- a/bigtree/__init__.py
+++ b/bigtree/__init__.py
@@ -1,4 +1,4 @@
-__version__ = "0.29.2"
+__version__ = "0.30.0"
from bigtree.binarytree.construct import list_to_binarytree
from bigtree.dag.construct import dataframe_to_dag, dict_to_dag, list_to_dag
@@ -21,6 +21,7 @@
dict_to_tree,
list_to_tree,
list_to_tree_by_relation,
+ nested_dict_key_to_tree,
nested_dict_to_tree,
newick_to_tree,
polars_to_tree,
@@ -37,6 +38,7 @@
tree_to_dot,
tree_to_mermaid,
tree_to_nested_dict,
+ tree_to_nested_dict_key,
tree_to_newick,
tree_to_pillow,
tree_to_pillow_graph,
diff --git a/bigtree/tree/construct/__init__.py b/bigtree/tree/construct/__init__.py
index f1adcea4..5b2efd45 100644
--- a/bigtree/tree/construct/__init__.py
+++ b/bigtree/tree/construct/__init__.py
@@ -12,6 +12,7 @@
add_dict_to_tree_by_name,
add_dict_to_tree_by_path,
dict_to_tree,
+ nested_dict_key_to_tree,
nested_dict_to_tree,
)
from .lists import list_to_tree, list_to_tree_by_relation
@@ -31,6 +32,7 @@
"add_dict_to_tree_by_path",
"dict_to_tree",
"nested_dict_to_tree",
+ "nested_dict_key_to_tree",
"list_to_tree",
"list_to_tree_by_relation",
"render_tree",
diff --git a/bigtree/tree/construct/dictionaries.py b/bigtree/tree/construct/dictionaries.py
index c09ab1b6..ee8f3429 100644
--- a/bigtree/tree/construct/dictionaries.py
+++ b/bigtree/tree/construct/dictionaries.py
@@ -11,6 +11,7 @@
"add_dict_to_tree_by_name",
"dict_to_tree",
"nested_dict_to_tree",
+ "nested_dict_key_to_tree",
]
T = TypeVar("T", bound=node.Node)
@@ -238,7 +239,7 @@ def nested_dict_to_tree(
Examples:
>>> from bigtree import nested_dict_to_tree
- >>> path_dict = {
+ >>> nested_dict = {
... "name": "a",
... "age": 90,
... "children": [
@@ -252,7 +253,7 @@ def nested_dict_to_tree(
... ]},
... ],
... }
- >>> root = nested_dict_to_tree(path_dict)
+ >>> root = nested_dict_to_tree(nested_dict)
>>> root.show(attr_list=["age"])
a [age=90]
└── b [age=65]
@@ -300,3 +301,88 @@ def _recursive_add_child(
root_node = _recursive_add_child(node_attrs)
return root_node
+
+
+def nested_dict_key_to_tree(
+ node_attrs: Mapping[str, Mapping[str, Any]],
+ child_key: str = "children",
+ node_type: Type[T] = node.Node, # type: ignore[assignment]
+) -> T:
+ """Construct tree from nested recursive dictionary, where the keys are node names.
+
+ - ``key``: node name
+ - ``value``: dict of node attributes and node children (recursive)
+
+ Value dictionary
+
+ - ``key`` that is not ``child_key`` has node attribute as value
+ - ``key`` that is ``child_key`` has dictionary of node children as value (recursive)
+
+ Examples:
+ >>> from bigtree import nested_dict_key_to_tree
+ >>> nested_dict = {
+ ... "a": {
+ ... "age": 90,
+ ... "children": {
+ ... "b": {
+ ... "age": 65,
+ ... "children": {
+ ... "d": {"age": 40},
+ ... "e": {
+ ... "age": 35,
+ ... "children": {"g": {"age": 10}},
+ ... },
+ ... },
+ ... },
+ ... },
+ ... }
+ ... }
+ >>> root = nested_dict_key_to_tree(nested_dict)
+ >>> root.show(attr_list=["age"])
+ a [age=90]
+ └── b [age=65]
+ ├── d [age=40]
+ └── e [age=35]
+ └── g [age=10]
+
+ Args:
+ node_attrs: node, children, and node attribute information,
+ key: node name
+ value: dictionary of node attributes and node children
+ child_key: key of child dict, value is type dict
+ node_type: node type of tree to be created
+
+ Returns:
+ Node
+ """
+ assertions.assert_length(node_attrs, 1, "Dictionary", "node_attrs")
+
+ def _recursive_add_child(
+ child_name: str, child_dict: Mapping[str, Any], parent_node: Optional[T] = None
+ ) -> T:
+ """Recursively add child to tree, given child attributes and parent node.
+
+ Args:
+ child_name: child name to be added to tree
+ child_dict: child to be added to tree, from dictionary
+ parent_node: parent node to be assigned to child node
+
+ Returns:
+ Node
+ """
+ child_dict = dict(child_dict)
+ node_children = child_dict.pop(child_key, {})
+ if not isinstance(node_children, Mapping):
+ raise TypeError(
+ f"child_key {child_key} should be Dict type, received {node_children}"
+ )
+ root = node_type(child_name, parent=parent_node, **child_dict)
+ for _child_name in node_children:
+ _recursive_add_child(
+ _child_name, node_children[_child_name], parent_node=root
+ )
+ return root
+
+ root_node_name = list(node_attrs.keys())[0]
+ root_node = _recursive_add_child(root_node_name, node_attrs[root_node_name])
+ return root_node
diff --git a/bigtree/tree/export/__init__.py b/bigtree/tree/export/__init__.py
index 0a2c4ba5..56ad148e 100644
--- a/bigtree/tree/export/__init__.py
+++ b/bigtree/tree/export/__init__.py
@@ -1,5 +1,9 @@
from .dataframes import tree_to_dataframe, tree_to_polars # noqa
-from .dictionaries import tree_to_dict, tree_to_nested_dict # noqa
+from .dictionaries import ( # noqa
+ tree_to_dict,
+ tree_to_nested_dict,
+ tree_to_nested_dict_key,
+)
from .images import ( # noqa
tree_to_dot,
tree_to_mermaid,
@@ -22,6 +26,7 @@
"tree_to_polars",
"tree_to_dict",
"tree_to_nested_dict",
+ "tree_to_nested_dict_key",
"tree_to_dot",
"tree_to_mermaid",
"tree_to_pillow",
diff --git a/bigtree/tree/export/dictionaries.py b/bigtree/tree/export/dictionaries.py
index 4a29c776..9a4a5543 100644
--- a/bigtree/tree/export/dictionaries.py
+++ b/bigtree/tree/export/dictionaries.py
@@ -7,6 +7,7 @@
__all__ = [
"tree_to_dict",
"tree_to_nested_dict",
+ "tree_to_nested_dict_key",
]
T = TypeVar("T", bound=node.Node)
@@ -165,3 +166,71 @@ def _recursive_append(_node: T, parent_dict: Dict[str, Any]) -> None:
_recursive_append(tree, data_dict)
return data_dict[child_key][0]
+
+
+def tree_to_nested_dict_key(
+ tree: T,
+ child_key: str = "children",
+ attr_dict: Optional[Dict[str, str]] = None,
+ all_attrs: bool = False,
+ max_depth: int = 0,
+) -> Dict[str, Any]:
+ """Export tree to nested dictionary, where the keys are node names.
+
+ 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.
+
+ Examples:
+ >>> from bigtree import Node, tree_to_nested_dict_key
+ >>> 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=b)
+ >>> e = Node("e", age=35, parent=b)
+ >>> 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}}}}
+
+ Args:
+ tree: tree to be exported
+ child_key: dictionary key for children
+ attr_dict: node attributes mapped to dictionary key, key: node attributes, value: corresponding dictionary key
+ all_attrs: indicator whether to retrieve all ``Node`` attributes, overrides `attr_dict`
+ max_depth: maximum depth to export tree
+
+ Returns:
+ Dictionary containing tree information
+ """
+ data_dict: Dict[str, Dict[str, Any]] = {}
+
+ def _recursive_append(_node: T, parent_dict: Dict[str, Any]) -> None:
+ """Recursively iterate through node and its children to export to nested dictionary.
+
+ Args:
+ _node: current node
+ parent_dict: parent dictionary
+ """
+ if _node:
+ if not max_depth or _node.depth <= max_depth:
+ data_child = {}
+ if all_attrs:
+ data_child.update(
+ dict(
+ _node.describe(
+ exclude_attributes=["name"], exclude_prefix="_"
+ )
+ )
+ )
+ elif attr_dict:
+ for k, v in attr_dict.items():
+ data_child[v] = _node.get_attr(k)
+ 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}
+
+ for _child in _node.children:
+ _recursive_append(_child, data_child)
+
+ _recursive_append(tree, data_dict)
+ return data_dict[child_key]
diff --git a/bigtree/utils/assertions.py b/bigtree/utils/assertions.py
index d21e6b1d..6efe6023 100644
--- a/bigtree/utils/assertions.py
+++ b/bigtree/utils/assertions.py
@@ -134,6 +134,23 @@ def assert_length_not_empty(
)
+def assert_length(
+ data: Collection[Any], length: int, argument_name: str, argument: str
+) -> None:
+ """Raise ValueError if data does not have specific length.
+
+ Args:
+ data: data to check
+ length: length to check
+ argument_name: argument name for data, for error message
+ argument: argument for data, for error message
+ """
+ if len(data) != length:
+ raise ValueError(
+ f"{argument_name} is not of length {length}, check `{argument}`"
+ )
+
+
def assert_dataframe_not_empty(data: pd.DataFrame) -> None:
"""Raise ValueError is dataframe is empty.
diff --git a/docs/bigtree/tree/construct.md b/docs/bigtree/tree/construct.md
index e4322c78..66de5948 100644
--- a/docs/bigtree/tree/construct.md
+++ b/docs/bigtree/tree/construct.md
@@ -10,14 +10,14 @@ Construct Tree from list, dictionary, and pandas DataFrame.
To decide which method to use, consider your data type and data values.
-| Construct tree from | Using full path | Using parent-child relation | Using notation | Add node attributes |
-|---------------------|---------------------|---------------------------------|------------------|---------------------------------------------------------|
-| String | `str_to_tree` | NA | `newick_to_tree` | No (for ` str_to_tree `)
Yes (for `newick_to_tree`) |
-| List | `list_to_tree` | ` list_to_tree_by_relation` | NA | No |
-| Dictionary | `dict_to_tree` | ` nested_dict_to_tree` | NA | Yes |
-| pandas DataFrame | `dataframe_to_tree` | `dataframe_to_tree_by_relation` | NA | Yes |
-| polars DataFrame | `polars_to_tree` | `polars_to_tree_by_relation` | NA | Yes |
-| Interactive UI | NA | `render_tree` | NA | No |
+| Construct tree from | Using full path | Using parent-child relation | Using notation | Add node attributes |
+|---------------------|---------------------|--------------------------------------------------|------------------|---------------------------------------------------------|
+| String | `str_to_tree` | NA | `newick_to_tree` | No (for ` str_to_tree `)
Yes (for `newick_to_tree`) |
+| List | `list_to_tree` | `list_to_tree_by_relation` | NA | No |
+| Dictionary | `dict_to_tree` | `nested_dict_to_tree`, `nested_dict_key_to_tree` | NA | Yes |
+| pandas DataFrame | `dataframe_to_tree` | `dataframe_to_tree_by_relation` | NA | Yes |
+| polars DataFrame | `polars_to_tree` | `polars_to_tree_by_relation` | NA | Yes |
+| Interactive UI | NA | `render_tree` | NA | No |
## Tree Add Attributes Methods
diff --git a/docs/bigtree/tree/export.md b/docs/bigtree/tree/export.md
index 833684a4..9b7f32e1 100644
--- a/docs/bigtree/tree/export.md
+++ b/docs/bigtree/tree/export.md
@@ -8,17 +8,17 @@ title: Tree Export
Export Tree to list, dictionary, pandas/polars DataFrame, and various formats.
-| Export Tree to | Method |
-|-----------------------------------------|--------------------------------------------|
-| Command Line / Print | `print_tree`, `hprint_tree`, `vprint_tree` |
-| Generator (versatile) | `yield_tree`, `hyield_tree`, `vyield_tree` |
-| String | `tree_to_newick` |
-| Dictionary | `tree_to_dict`, `tree_to_nested_dict` |
-| DataFrame (pandas, polars) | `tree_to_dataframe`, `tree_to_polars` |
-| Dot (for .dot, .png, .svg, .jpeg, etc.) | `tree_to_dot` |
-| Pillow (for .png, .jpg, .jpeg, etc.) | `tree_to_pillow`, `tree_to_pillow_graph` |
-| Mermaid Markdown (for .md) | `tree_to_mermaid` |
-| Visualization | `tree_to_vis` |
+| Export Tree to | Method |
+|-----------------------------------------|------------------------------------------------------------------|
+| Command Line / Print | `print_tree`, `hprint_tree`, `vprint_tree` |
+| Generator (versatile) | `yield_tree`, `hyield_tree`, `vyield_tree` |
+| String | `tree_to_newick` |
+| Dictionary | `tree_to_dict`, `tree_to_nested_dict`, `tree_to_nested_dict_key` |
+| DataFrame (pandas, polars) | `tree_to_dataframe`, `tree_to_polars` |
+| Dot (for .dot, .png, .svg, .jpeg, etc.) | `tree_to_dot` |
+| Pillow (for .png, .jpg, .jpeg, etc.) | `tree_to_pillow`, `tree_to_pillow_graph` |
+| Mermaid Markdown (for .md) | `tree_to_mermaid` |
+| Visualization | `tree_to_vis` |
@@ -26,24 +26,25 @@ Export Tree to list, dictionary, pandas/polars DataFrame, and various formats.
While exporting to another data type, methods can take in arguments to determine what information to extract.
-| Method | Extract node attributes | Specify maximum depth | Skip depth | Extract leaves only | Others |
-|------------------------|-------------------------------------|-----------------------|------------|---------------------------------------|-------------------------------------------------------|
-| `print_tree` | Yes with `attr_list` or `all_attrs` | Yes | No | No | Tree style |
-| `yield_tree` | No, returns node | Yes | No | No | Tree style |
-| `hprint_tree` | No | Yes | No | Yes, by hiding intermediate node name | Tree style, border style |
-| `hyield_tree` | No | Yes | No | Yes, by hiding intermediate node name | Tree style, border style |
-| `vprint_tree` | No | Yes | No | Yes, by hiding intermediate node name | Tree style, border style |
-| `vyield_tree` | No | Yes | No | Yes, by hiding intermediate node name | Tree style, border style |
-| `tree_to_newick` | Yes with `attr_list` | No | No | Yes, by hiding intermediate node name | Length separator and attribute prefix and separator |
-| `tree_to_dict` | Yes with `attr_dict` or `all_attrs` | Yes | Yes | Yes with `leaf_only` | Dict key for parent |
-| `tree_to_nested_dict` | Yes with `attr_dict` or `all_attrs` | Yes | No | No | Dict key for node name and node children |
-| `tree_to_dataframe` | Yes with `attr_dict` or `all_attrs` | Yes | Yes | Yes with `leaf_only` | Column name for path, node name, node parent |
-| `tree_to_polars` | Yes with `attr_dict` or `all_attrs` | Yes | Yes | Yes with `leaf_only` | Column name for path, node name, node parent |
-| `tree_to_dot` | No | No | No | No | Graph attributes, background, node, edge colour etc. |
-| `tree_to_pillow_graph` | Yes with `node_content` | Yes | No | No | Font (family, size, colour), background colour etc. |
-| `tree_to_pillow` | No | Yes | No | No | Font (family, size, colour), background colour etc. |
-| `tree_to_mermaid` | No | Yes | No | No | Node shape, node fill, edge arrow, edge label etc. |
-| `tree_to_vis` | No | Yes | No | No | Background style, node style, edge style etc. |
+| Method | Extract node attributes | Specify maximum depth | Skip depth | Extract leaves only | Others |
+|---------------------------|-------------------------------------|-----------------------|------------|---------------------------------------|------------------------------------------------------|
+| `print_tree` | Yes with `attr_list` or `all_attrs` | Yes | No | No | Tree style |
+| `yield_tree` | No, returns node | Yes | No | No | Tree style |
+| `hprint_tree` | No | Yes | No | Yes, by hiding intermediate node name | Tree style, border style |
+| `hyield_tree` | No | Yes | No | Yes, by hiding intermediate node name | Tree style, border style |
+| `vprint_tree` | No | Yes | No | Yes, by hiding intermediate node name | Tree style, border style |
+| `vyield_tree` | No | Yes | No | Yes, by hiding intermediate node name | Tree style, border style |
+| `tree_to_newick` | Yes with `attr_list` | No | No | Yes, by hiding intermediate node name | Length separator and attribute prefix and separator |
+| `tree_to_dict` | Yes with `attr_dict` or `all_attrs` | Yes | Yes | Yes with `leaf_only` | Dict key for parent |
+| `tree_to_nested_dict` | Yes with `attr_dict` or `all_attrs` | Yes | No | No | Dict key for node name and node children |
+| `tree_to_nested_dict_key` | Yes with `attr_dict` or `all_attrs` | Yes | No | No | Dict key for node children |
+| `tree_to_dataframe` | Yes with `attr_dict` or `all_attrs` | Yes | Yes | Yes with `leaf_only` | Column name for path, node name, node parent |
+| `tree_to_polars` | Yes with `attr_dict` or `all_attrs` | Yes | Yes | Yes with `leaf_only` | Column name for path, node name, node parent |
+| `tree_to_dot` | No | No | No | No | Graph attributes, background, node, edge colour etc. |
+| `tree_to_pillow_graph` | Yes with `node_content` | Yes | No | No | Font (family, size, colour), background colour etc. |
+| `tree_to_pillow` | No | Yes | No | No | Font (family, size, colour), background colour etc. |
+| `tree_to_mermaid` | No | Yes | No | No | Node shape, node fill, edge arrow, edge label etc. |
+| `tree_to_vis` | No | Yes | No | No | Background style, node style, edge style etc. |
-----
diff --git a/docs/gettingstarted/demo/tree.md b/docs/gettingstarted/demo/tree.md
index e5519a64..12d4ea98 100644
--- a/docs/gettingstarted/demo/tree.md
+++ b/docs/gettingstarted/demo/tree.md
@@ -206,7 +206,7 @@ names and `value` is node attribute values, and list of children (recursive).
```python hl_lines="17"
from bigtree import nested_dict_to_tree
- path_dict = {
+ nested_dict = {
"name": "a",
"age": 90,
"children": [
@@ -220,7 +220,34 @@ names and `value` is node attribute values, and list of children (recursive).
{"name": "c", "age": 60},
],
}
- root = nested_dict_to_tree(path_dict)
+ root = nested_dict_to_tree(nested_dict)
+
+ root.show(attr_list=["age"])
+ # a [age=90]
+ # ├── b [age=65]
+ # │ └── d [age=40]
+ # └── c [age=60]
+ ```
+
+=== "Recursive structure 2"
+ ```python hl_lines="17"
+ from bigtree import nested_dict_key_to_tree
+
+ nested_dict = {
+ "a": {
+ "age": 90,
+ "children": {
+ "b": {
+ "age": 65,
+ "children": {
+ "d": {"age": 40},
+ },
+ },
+ "c": {"age": 60},
+ },
+ }
+ }
+ root = nested_dict_key_to_tree(nested_dict)
root.show(attr_list=["age"])
# a [age=90]
@@ -229,6 +256,7 @@ names and `value` is node attribute values, and list of children (recursive).
# └── c [age=60]
```
+
### 5. From pandas/polars DataFrame
Construct nodes with attributes. *DataFrame* can contain either path column or
@@ -1266,6 +1294,17 @@ root.show()
# }
```
+=== "Dictionary (recursive structure 2)"
+ ```python hl_lines="3"
+ 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}}}}
+ ```
+
=== "pandas DataFrame"
```python hl_lines="3-9"
from bigtree import tree_to_dataframe
diff --git a/tests/test_constants.py b/tests/test_constants.py
index 518e5cf1..110e3dfa 100644
--- a/tests/test_constants.py
+++ b/tests/test_constants.py
@@ -103,8 +103,9 @@ class Constants:
"Unable to determine root node\nPossible root nodes: {root_nodes}"
)
ERROR_NODE_DICT_EMPTY = "Dictionary does not contain any data, check `{parameter}`"
+ ERROR_NODE_DICT_LEN = "Dictionary is not of length {length}, check `{parameter}`"
ERROR_NODE_DICT_CHILD_TYPE = (
- "child_key {child_key} should be List type, received {child}"
+ "child_key {child_key} should be {type} type, received {child}"
)
ERROR_NODE_LIST_EMPTY = "Path list does not contain any data, check `{parameter}`"
ERROR_NODE_PATH_EMPTY = "Path does not contain any data, check `path`"
diff --git a/tests/tree/construct/test_dictionaries.py b/tests/tree/construct/test_dictionaries.py
index 6cc55536..62481d28 100644
--- a/tests/tree/construct/test_dictionaries.py
+++ b/tests/tree/construct/test_dictionaries.py
@@ -613,7 +613,7 @@ def setUp(self):
+-- c (age=60)
+-- f (age=38)
"""
- self.path_dict = {
+ self.nested_dict = {
"name": "a",
"age": 90,
"children": [
@@ -637,10 +637,10 @@ def setUp(self):
}
def tearDown(self):
- self.path_dict = None
+ self.nested_dict = None
def test_nested_dict_to_tree(self):
- root = construct.nested_dict_to_tree(self.path_dict)
+ root = construct.nested_dict_to_tree(self.nested_dict)
assert_tree_structure_basenode_root(root)
assert_tree_structure_basenode_root_attr(root)
assert_tree_structure_node_root(root)
@@ -652,10 +652,11 @@ def test_nested_dict_to_tree_empty_error(self):
parameter="node_attrs"
)
- def test_nested_dict_to_tree_null_children_error(self):
+ @staticmethod
+ def test_nested_dict_to_tree_null_children_error():
child_key = "children"
child = None
- path_dict = {
+ nested_dict = {
"name": "a",
"age": 90,
"children": [
@@ -676,15 +677,16 @@ def test_nested_dict_to_tree_null_children_error(self):
],
}
with pytest.raises(TypeError) as exc_info:
- construct.nested_dict_to_tree(path_dict)
+ construct.nested_dict_to_tree(nested_dict)
assert str(exc_info.value) == Constants.ERROR_NODE_DICT_CHILD_TYPE.format(
- child_key=child_key, child=child
+ child_key=child_key, type="List", child=child
)
- def test_nested_dict_to_tree_int_children_error(self):
+ @staticmethod
+ def test_nested_dict_to_tree_int_children_error():
child_key = "children"
child = 1
- path_dict = {
+ nested_dict = {
"name": "a",
"age": 90,
"children": [
@@ -705,48 +707,50 @@ def test_nested_dict_to_tree_int_children_error(self):
],
}
with pytest.raises(TypeError) as exc_info:
- construct.nested_dict_to_tree(path_dict)
+ construct.nested_dict_to_tree(nested_dict)
assert str(exc_info.value) == Constants.ERROR_NODE_DICT_CHILD_TYPE.format(
- child_key=child_key, child=child
+ child_key=child_key, type="List", child=child
)
@staticmethod
def test_nested_dict_to_tree_key_name():
- path_dict = {
- "node_name": "a",
+ name_key = "node_name"
+ child_key = "node_children"
+ nested_dict = {
+ name_key: "a",
"age": 90,
- "node_children": [
+ child_key: [
{
- "node_name": "b",
+ name_key: "b",
"age": 65,
- "node_children": [
- {"node_name": "d", "age": 40},
+ child_key: [
+ {name_key: "d", "age": 40},
{
- "node_name": "e",
+ name_key: "e",
"age": 35,
- "node_children": [
- {"node_name": "g", "age": 10},
- {"node_name": "h", "age": 6},
+ child_key: [
+ {name_key: "g", "age": 10},
+ {name_key: "h", "age": 6},
],
},
],
},
{
- "node_name": "c",
+ name_key: "c",
"age": 60,
- "node_children": [{"node_name": "f", "age": 38}],
+ child_key: [{name_key: "f", "age": 38}],
},
],
}
root = construct.nested_dict_to_tree(
- path_dict, name_key="node_name", child_key="node_children"
+ nested_dict, name_key=name_key, child_key=child_key
)
assert_tree_structure_basenode_root(root)
assert_tree_structure_basenode_root_attr(root)
assert_tree_structure_node_root(root)
def test_nested_dict_to_tree_node_type(self):
- root = construct.nested_dict_to_tree(self.path_dict, node_type=NodeA)
+ root = construct.nested_dict_to_tree(self.nested_dict, node_type=NodeA)
assert isinstance(root, NodeA), Constants.ERROR_CUSTOM_TYPE.format(type="NodeA")
assert all(
isinstance(_node, NodeA) for _node in root.children
@@ -755,8 +759,9 @@ def test_nested_dict_to_tree_node_type(self):
assert_tree_structure_basenode_root_attr(root)
assert_tree_structure_node_root(root)
- def test_nested_dict_to_tree_custom_node_type(self):
- path_dict = {
+ @staticmethod
+ def test_nested_dict_to_tree_custom_node_type():
+ nested_dict = {
"name": "a",
"custom_field": 90,
"custom_field_str": "a",
@@ -800,7 +805,208 @@ def test_nested_dict_to_tree_custom_node_type(self):
},
],
}
- root = construct.nested_dict_to_tree(path_dict, node_type=CustomNode)
+ root = construct.nested_dict_to_tree(nested_dict, node_type=CustomNode)
+ assert isinstance(root, CustomNode), Constants.ERROR_CUSTOM_TYPE.format(
+ type="CustomNode"
+ )
+ assert all(
+ isinstance(_node, CustomNode) for _node in root.children
+ ), Constants.ERROR_CUSTOM_TYPE.format(type="CustomNode")
+ assert_tree_structure_basenode_root(root)
+ assert_tree_structure_customnode_root_attr(root)
+ assert_tree_structure_node_root(root)
+
+
+class TestNestedDictKeyToTree(unittest.TestCase):
+ def setUp(self):
+ """
+ Tree should have structure
+ a (age=90)
+ |-- b (age=65)
+ | |-- d (age=40)
+ | +-- e (age=35)
+ | |-- g (age=10)
+ | +-- h (age=6)
+ +-- c (age=60)
+ +-- f (age=38)
+ """
+ self.nested_dict = {
+ "a": {
+ "age": 90,
+ "children": {
+ "b": {
+ "age": 65,
+ "children": {
+ "d": {"age": 40},
+ "e": {
+ "age": 35,
+ "children": {
+ "g": {"age": 10},
+ "h": {"age": 6},
+ },
+ },
+ },
+ },
+ "c": {"age": 60, "children": {"f": {"age": 38}}},
+ },
+ }
+ }
+
+ 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)
+ assert_tree_structure_basenode_root(root)
+ assert_tree_structure_basenode_root_attr(root)
+ assert_tree_structure_node_root(root)
+
+ def test_nested_dict_key_to_tree_empty_error(self):
+ with pytest.raises(ValueError) as exc_info:
+ construct.nested_dict_key_to_tree({})
+ assert str(exc_info.value) == Constants.ERROR_NODE_DICT_LEN.format(
+ length=1, parameter="node_attrs"
+ )
+
+ @staticmethod
+ def test_nested_dict_key_to_tree_null_children_error():
+ child_key = "children"
+ child = None
+ nested_dict = {
+ "a": {
+ "age": 90,
+ "children": {
+ "b": {
+ "age": 65,
+ "children": {
+ "d": {"age": 40},
+ "e": {
+ "age": 35,
+ "children": {
+ "g": {"age": 10, "children": child},
+ },
+ },
+ },
+ },
+ },
+ }
+ }
+ with pytest.raises(TypeError) as exc_info:
+ construct.nested_dict_key_to_tree(nested_dict)
+ 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 = "children"
+ child = 1
+ nested_dict = {
+ "a": {
+ "age": 90,
+ "children": {
+ "b": {
+ "age": 65,
+ "children": {
+ "d": {"age": 40},
+ "e": {
+ "age": 35,
+ "children": {
+ "g": {"age": 10, "children": child},
+ },
+ },
+ },
+ },
+ },
+ }
+ }
+ with pytest.raises(TypeError) as exc_info:
+ construct.nested_dict_key_to_tree(nested_dict)
+ 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_key_name():
+ child_key = "node_children"
+ nested_dict = {
+ "a": {
+ "age": 90,
+ child_key: {
+ "b": {
+ "age": 65,
+ child_key: {
+ "d": {"age": 40},
+ "e": {
+ "age": 35,
+ child_key: {
+ "g": {"age": 10},
+ "h": {"age": 6},
+ },
+ },
+ },
+ },
+ "c": {"age": 60, child_key: {"f": {"age": 38}}},
+ },
+ }
+ }
+ root = construct.nested_dict_key_to_tree(nested_dict, child_key=child_key)
+ assert_tree_structure_basenode_root(root)
+ assert_tree_structure_basenode_root_attr(root)
+ assert_tree_structure_node_root(root)
+
+ def test_nested_dict_key_to_tree_node_type(self):
+ root = construct.nested_dict_key_to_tree(self.nested_dict, 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_basenode_root_attr(root)
+ assert_tree_structure_node_root(root)
+
+ @staticmethod
+ def test_nested_dict_key_to_tree_custom_node_type():
+ path_dict = {
+ "a": {
+ "custom_field": 90,
+ "custom_field_str": "a",
+ "children": {
+ "b": {
+ "custom_field": 65,
+ "custom_field_str": "b",
+ "children": {
+ "d": {"custom_field": 40, "custom_field_str": "d"},
+ "e": {
+ "custom_field": 35,
+ "custom_field_str": "e",
+ "children": {
+ "g": {
+ "custom_field": 10,
+ "custom_field_str": "g",
+ },
+ "h": {
+ "custom_field": 6,
+ "custom_field_str": "h",
+ },
+ },
+ },
+ },
+ },
+ "c": {
+ "custom_field": 60,
+ "custom_field_str": "c",
+ "children": {
+ "f": {
+ "custom_field": 38,
+ "custom_field_str": "f",
+ }
+ },
+ },
+ },
+ }
+ }
+ root = construct.nested_dict_key_to_tree(path_dict, node_type=CustomNode)
assert isinstance(root, CustomNode), Constants.ERROR_CUSTOM_TYPE.format(
type="CustomNode"
)
diff --git a/tests/tree/export/test_dictionaries.py b/tests/tree/export/test_dictionaries.py
index a044d882..e189621a 100644
--- a/tests/tree/export/test_dictionaries.py
+++ b/tests/tree/export/test_dictionaries.py
@@ -1,3 +1,4 @@
+from bigtree.node import node
from bigtree.tree import export
from tests.node.test_basenode import (
assert_tree_structure_basenode_root,
@@ -178,56 +179,69 @@ def test_tree_to_dict_to_tree(tree_node):
class TestTreeToNestedDict:
@staticmethod
def test_tree_to_nested_dict(tree_node):
+ name_key = "name"
+ child_key = "children"
expected = {
- "name": "a",
- "children": [
+ name_key: "a",
+ child_key: [
{
- "name": "b",
- "children": [
- {"name": "d"},
- {"name": "e", "children": [{"name": "g"}, {"name": "h"}]},
+ name_key: "b",
+ child_key: [
+ {name_key: "d"},
+ {name_key: "e", child_key: [{name_key: "g"}, {name_key: "h"}]},
],
},
- {"name": "c", "children": [{"name": "f"}]},
+ {name_key: "c", child_key: [{name_key: "f"}]},
],
}
actual = export.tree_to_nested_dict(tree_node)
assert actual == expected, f"Expected\n{expected}\nReceived\n{actual}"
+ @staticmethod
+ def test_tree_to_nested_dict_empty():
+ root = node.Node("a")
+ expected = {"name": "a"}
+ actual = export.tree_to_nested_dict(root)
+ assert actual == expected, f"Expected\n{expected}\nReceived\n{actual}"
+
@staticmethod
def test_tree_to_nested_dict_name_key(tree_node):
+ name_key = "NAME"
+ child_key = "children"
expected = {
- "NAME": "a",
- "children": [
+ name_key: "a",
+ child_key: [
{
- "NAME": "b",
- "children": [
- {"NAME": "d"},
- {"NAME": "e", "children": [{"NAME": "g"}, {"NAME": "h"}]},
+ name_key: "b",
+ child_key: [
+ {name_key: "d"},
+ {name_key: "e", child_key: [{name_key: "g"}, {name_key: "h"}]},
],
},
- {"NAME": "c", "children": [{"NAME": "f"}]},
+ {name_key: "c", child_key: [{name_key: "f"}]},
],
}
- actual = export.tree_to_nested_dict(tree_node, name_key="NAME")
+ actual = export.tree_to_nested_dict(tree_node, name_key=name_key)
assert actual == expected, f"Expected\n{expected}\nReceived\n{actual}"
@staticmethod
def test_tree_to_nested_dict_child_key(tree_node):
+ name_key = "name"
+ child_key = "CHILDREN"
expected = {
- "name": "a",
- "CHILDREN": [
+ name_key: "a",
+ child_key: [
{
- "name": "b",
- "CHILDREN": [
- {"name": "d"},
- {"name": "e", "CHILDREN": [{"name": "g"}, {"name": "h"}]},
+ name_key: "b",
+ child_key: [
+ {name_key: "d"},
+ {name_key: "e", child_key: [{name_key: "g"}, {name_key: "h"}]},
],
},
- {"name": "c", "CHILDREN": [{"name": "f"}]},
+ {name_key: "c", child_key: [{name_key: "f"}]},
],
}
- actual = export.tree_to_nested_dict(tree_node, child_key="CHILDREN")
+ actual = export.tree_to_nested_dict(tree_node, child_key=child_key)
assert actual == expected, f"Expected\n{expected}\nReceived\n{actual}"
@staticmethod
@@ -350,3 +364,178 @@ def test_tree_to_nested_dict_to_tree(tree_node):
assert_tree_structure_basenode_root(tree)
assert_tree_structure_basenode_root_attr(tree)
assert_tree_structure_node_root(tree)
+
+
+class TestTreeToNestedDictKey:
+ @staticmethod
+ def test_tree_to_nested_dict_key(tree_node):
+ child_key = "children"
+ expected = {
+ "a": {
+ child_key: {
+ "b": {
+ child_key: {
+ "d": {},
+ "e": {child_key: {"g": {}, "h": {}}},
+ },
+ },
+ "c": {child_key: {"f": {}}},
+ }
+ }
+ }
+ actual = export.tree_to_nested_dict_key(tree_node)
+ assert actual == expected, f"Expected\n{expected}\nReceived\n{actual}"
+
+ @staticmethod
+ def test_tree_to_nested_dict_key_empty():
+ root = node.Node("a")
+ expected = {"a": {}}
+ actual = export.tree_to_nested_dict_key(root)
+ assert actual == expected, f"Expected\n{expected}\nReceived\n{actual}"
+
+ @staticmethod
+ def test_tree_to_nested_dict_key_child_key(tree_node):
+ child_key = "CHILDREN"
+ expected = {
+ "a": {
+ child_key: {
+ "b": {
+ child_key: {
+ "d": {},
+ "e": {child_key: {"g": {}, "h": {}}},
+ },
+ },
+ "c": {child_key: {"f": {}}},
+ }
+ }
+ }
+ actual = export.tree_to_nested_dict_key(tree_node, child_key=child_key)
+ assert actual == expected, f"Expected\n{expected}\nReceived\n{actual}"
+
+ @staticmethod
+ def test_tree_to_nested_dict_key_attr_dict(tree_node):
+ child_key = "children"
+ age_key = "AGE"
+ expected = {
+ "a": {
+ age_key: 90,
+ child_key: {
+ "b": {
+ age_key: 65,
+ child_key: {
+ "d": {age_key: 40},
+ "e": {
+ age_key: 35,
+ child_key: {
+ "g": {age_key: 10},
+ "h": {age_key: 6},
+ },
+ },
+ },
+ },
+ "c": {age_key: 60, child_key: {"f": {age_key: 38}}},
+ },
+ }
+ }
+ actual = export.tree_to_nested_dict_key(tree_node, attr_dict={"age": "AGE"})
+ assert actual == expected, f"Expected\n{expected}\nReceived\n{actual}"
+
+ @staticmethod
+ def test_tree_to_nested_dict_key_all_attr(tree_node):
+ child_key = "children"
+ age_key = "age"
+ expected = {
+ "a": {
+ age_key: 90,
+ child_key: {
+ "b": {
+ age_key: 65,
+ child_key: {
+ "d": {age_key: 40},
+ "e": {
+ age_key: 35,
+ child_key: {
+ "g": {age_key: 10},
+ "h": {age_key: 6},
+ },
+ },
+ },
+ },
+ "c": {age_key: 60, child_key: {"f": {age_key: 38}}},
+ },
+ }
+ }
+ actual = export.tree_to_nested_dict_key(tree_node, all_attrs=True)
+ assert actual == expected, f"Expected\n{expected}\nReceived\n{actual}"
+
+ @staticmethod
+ def test_tree_to_nested_dict_key_max_depth(tree_node):
+ expected = {"a": {"children": {"b": {}, "c": {}}}}
+ actual = export.tree_to_nested_dict_key(tree_node, max_depth=2)
+ assert actual == expected, f"Expected\n{expected}\nReceived\n{actual}"
+
+ @staticmethod
+ def test_tree_to_nested_dict_key_multiple_keys(tree_node):
+ child_key = "CHILDREN"
+ age_key = "AGE"
+ expected = {
+ "a": {
+ age_key: 90,
+ child_key: {
+ "b": {
+ age_key: 65,
+ child_key: {
+ "d": {age_key: 40},
+ "e": {
+ age_key: 35,
+ child_key: {
+ "g": {age_key: 10},
+ "h": {age_key: 6},
+ },
+ },
+ },
+ },
+ "c": {age_key: 60, child_key: {"f": {age_key: 38}}},
+ },
+ }
+ }
+ actual = export.tree_to_nested_dict_key(
+ tree_node, child_key=child_key, attr_dict={"age": age_key}
+ )
+ assert actual == expected, f"Expected\n{expected}\nReceived\n{actual}"
+
+ @staticmethod
+ def test_tree_to_nested_dict_key_multiple_keys_subset_tree(tree_node):
+ child_key = "CHILDREN"
+ age_key = "AGE"
+ expected = {
+ "b": {
+ age_key: 65,
+ child_key: {
+ "d": {age_key: 40},
+ "e": {
+ age_key: 35,
+ child_key: {
+ "g": {age_key: 10},
+ "h": {age_key: 6},
+ },
+ },
+ },
+ },
+ }
+ actual = export.tree_to_nested_dict_key(
+ tree_node.children[0],
+ child_key=child_key,
+ attr_dict={"age": age_key},
+ )
+ assert actual == expected, f"Expected\n{expected}\nReceived\n{actual}"
+
+ @staticmethod
+ def test_tree_to_nested_dict_key_to_tree(tree_node):
+ from bigtree.tree.construct import nested_dict_key_to_tree
+
+ d = export.tree_to_nested_dict_key(tree_node, all_attrs=True)
+ tree = nested_dict_key_to_tree(d)
+ assert_tree_structure_basenode_root(tree)
+ assert_tree_structure_basenode_root_attr(tree)
+ assert_tree_structure_node_root(tree)