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)