diff --git a/CHANGELOG.md b/CHANGELOG.md
index e08870d9..7ac5efa3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,12 @@ 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]
+### Changed:
+- Tree Modify: Update documentation and docstring with some rephrasing.
+### Added:
+- Tree Modify: Add parameter `merge_attribute` to allow from-node and to-node attributes to be merged if there are clashes.
+### Fixed:
+- Tree Modify: Fixed bug when `merge_children` is used with `overriding` as the `merge_children` value is changed in for-loop (bad move, literally).
## [0.22.3] - 2024-11-14
### Added:
diff --git a/bigtree/tree/modify.py b/bigtree/tree/modify.py
index b53e36f0..98b1e098 100644
--- a/bigtree/tree/modify.py
+++ b/bigtree/tree/modify.py
@@ -27,6 +27,7 @@ def shift_nodes(
sep: str = "/",
skippable: bool = False,
overriding: bool = False,
+ merge_attribute: bool = False,
merge_children: bool = False,
merge_leaves: bool = False,
delete_children: bool = False,
@@ -34,11 +35,12 @@ def shift_nodes(
) -> None:
"""Shift nodes from `from_paths` to `to_paths` *in-place*.
- - Creates intermediate nodes if to path is not present
- - Able to skip nodes if from path is not found, defaults to False (from-nodes must be found; not skippable).
+ - Creates intermediate nodes if to-path is not present
+ - Able to skip nodes if from-path is not found, defaults to False (from-nodes must be found; not skippable).
- Able to override existing node if it exists, defaults to False (to-nodes must not exist; not overridden).
+ - Able to merge attributes of node if it exists, defaults to False (to-nodes must not exist; no attributes to merge).
- Able to merge children and remove intermediate parent node, defaults to False (nodes are shifted; not merged).
- - Able to merge only leaf nodes and remove all intermediate nodes, defaults to False (nodes are shifted; not merged)
+ - Able to merge leaf nodes and remove all intermediate nodes, defaults to False (nodes are shifted; not merged)
- Able to shift node only and delete children, defaults to False (nodes are shifted together with children).
For paths in `from_paths` and `to_paths`,
@@ -69,6 +71,11 @@ def shift_nodes(
- If `to_path` is present and ``overriding=True``, it behaves like overriding and only new leaves are retained,
original non-leaf nodes in `from_path` are retained.
+ Note:
+
+ - `merge_children` and `merge_leaves` cannot be both True at the same time.
+ - `overriding` and `merge_attribute` cannot be both True at the same time.
+
Examples:
>>> from bigtree import list_to_tree, str_to_tree, shift_nodes
>>> root = list_to_tree(["Downloads/photo1.jpg", "Downloads/file1.doc"])
@@ -130,7 +137,7 @@ def shift_nodes(
In ``merge_children=True`` case, child nodes are shifted instead of the parent node.
- If the path already exists, child nodes are merged with existing children.
- - If same node is shifted, the child nodes of the node are merged with the node's parent.
+ - Otherwise, the child nodes of the node are merged with the node's parent.
>>> root = str_to_tree(
... "Downloads\\n"
@@ -174,10 +181,10 @@ def shift_nodes(
└── Files
└── file1.doc
- In ``merge_leaves=True`` case, leaf nodes are copied instead of the parent node.
+ In ``merge_leaves=True`` case, leaf nodes are shifted instead of the parent node.
- If the path already exists, leaf nodes are merged with existing children.
- - If same node is copied, the leaf nodes of the node are merged with the node's parent.
+ - Otherwise, the leaf nodes of the node are merged with the node's parent.
>>> root = str_to_tree(
... "Downloads\\n"
@@ -255,12 +262,16 @@ def shift_nodes(
from_paths (List[str]): original paths to shift nodes from
to_paths (List[str]): new paths to shift nodes to
sep (str): path separator for input paths, applies to `from_path` and `to_path`
- skippable (bool): indicator to skip if from path is not found, defaults to False
- overriding (bool): indicator to override existing to path if there is clashes, defaults to False
- merge_children (bool): indicator to merge children and remove intermediate parent node, defaults to False
- merge_leaves (bool): indicator to merge leaf nodes and remove intermediate parent node(s), defaults to False
+ skippable (bool): indicator to skip if from-path is not found, defaults to False
+ overriding (bool): indicator to override existing to-path if there are clashes, defaults to False
+ merge_attribute (bool): indicator to merge attributes of from-path and to-path if there are clashes, defaults
+ to False
+ merge_children (bool): indicator to merge from-path's children and remove intermediate parent node, defaults to
+ False
+ merge_leaves (bool): indicator to merge from-path's leaf nodes and remove intermediate parent node(s), defaults
+ to False
delete_children (bool): indicator to shift node only without children, defaults to False
- with_full_path (bool): indicator to shift/copy node with full path in `from_paths`, results in faster search,
+ with_full_path (bool): indicator to shift node with full path in `from_paths`, results in faster search,
defaults to False
"""
return copy_or_shift_logic(
@@ -271,6 +282,7 @@ def shift_nodes(
copy=False,
skippable=skippable,
overriding=overriding,
+ merge_attribute=merge_attribute,
merge_children=merge_children,
merge_leaves=merge_leaves,
delete_children=delete_children,
@@ -286,6 +298,7 @@ def copy_nodes(
sep: str = "/",
skippable: bool = False,
overriding: bool = False,
+ merge_attribute: bool = False,
merge_children: bool = False,
merge_leaves: bool = False,
delete_children: bool = False,
@@ -293,11 +306,12 @@ def copy_nodes(
) -> None:
"""Copy nodes from `from_paths` to `to_paths` *in-place*.
- - Creates intermediate nodes if to path is not present
- - Able to skip nodes if from path is not found, defaults to False (from-nodes must be found; not skippable).
+ - Creates intermediate nodes if to-path is not present
+ - Able to skip nodes if from-path is not found, defaults to False (from-nodes must be found; not skippable).
- Able to override existing node if it exists, defaults to False (to-nodes must not exist; not overridden).
- - Able to merge children and remove intermediate parent node, defaults to False (nodes are shifted; not merged).
- - Able to merge only leaf nodes and remove all intermediate nodes, defaults to False (nodes are shifted; not merged)
+ - Able to merge attributes of node if it exists, defaults to False (to-nodes must not exist; no attributes to merge).
+ - Able to merge children and remove intermediate parent node, defaults to False (nodes are copied; not merged).
+ - Able to merge only leaf nodes and remove all intermediate nodes, defaults to False (nodes are copied; not merged)
- Able to copy node only and delete children, defaults to False (nodes are copied together with children).
For paths in `from_paths` and `to_paths`,
@@ -326,6 +340,11 @@ def copy_nodes(
- If `to_path` is present, and ``overriding=False``, original children and leaves are merged.
- If `to_path` is present and ``overriding=True``, it behaves like overriding and only new leaves are retained.
+ Note:
+
+ - `merge_children` and `merge_leaves` cannot be both True at the same time.
+ - `overriding` and `merge_attribute` cannot be both True at the same time.
+
Examples:
>>> from bigtree import list_to_tree, str_to_tree, copy_nodes
>>> root = list_to_tree(["Downloads/Pictures", "Downloads/photo1.jpg", "Downloads/file1.doc"])
@@ -379,7 +398,7 @@ def copy_nodes(
In ``merge_children=True`` case, child nodes are copied instead of the parent node.
- If the path already exists, child nodes are merged with existing children.
- - If same node is copied, the child nodes of the node are merged with the node's parent.
+ - Otherwise, the child nodes of the node are merged with the node's parent.
>>> root = str_to_tree(
... "Downloads\\n"
@@ -430,7 +449,7 @@ def copy_nodes(
In ``merge_leaves=True`` case, leaf nodes are copied instead of the parent node.
- If the path already exists, leaf nodes are merged with existing children.
- - If same node is copied, the leaf nodes of the node are merged with the node's parent.
+ - Otherwise, the leaf nodes of the node are merged with the node's parent.
>>> root = str_to_tree(
... "Downloads\\n"
@@ -515,10 +534,14 @@ def copy_nodes(
sep (str): path separator for input paths, applies to `from_path` and `to_path`
skippable (bool): indicator to skip if from path is not found, defaults to False
overriding (bool): indicator to override existing to path if there is clashes, defaults to False
- merge_children (bool): indicator to merge children and remove intermediate parent node, defaults to False
- merge_leaves (bool): indicator to merge leaf nodes and remove intermediate parent node(s), defaults to False
+ merge_attribute (bool): indicator to merge attributes of from-path and to-path if there are clashes, defaults
+ to False
+ merge_children (bool): indicator to merge from-path's children and remove intermediate parent node, defaults to
+ False
+ merge_leaves (bool): indicator to merge from-path's leaf nodes and remove intermediate parent node(s), defaults
+ to False
delete_children (bool): indicator to copy node only without children, defaults to False
- with_full_path (bool): indicator to shift/copy node with full path in `from_paths`, results in faster search,
+ with_full_path (bool): indicator to copy node with full path in `from_paths`, results in faster search,
defaults to False
"""
return copy_or_shift_logic(
@@ -529,6 +552,7 @@ def copy_nodes(
copy=True,
skippable=skippable,
overriding=overriding,
+ merge_attribute=merge_attribute,
merge_children=merge_children,
merge_leaves=merge_leaves,
delete_children=delete_children,
@@ -548,8 +572,8 @@ def shift_and_replace_nodes(
) -> None:
"""Shift nodes from `from_paths` to *replace* `to_paths` *in-place*.
- - Creates intermediate nodes if to path is not present
- - Able to skip nodes if from path is not found, defaults to False (from-nodes must be found; not skippable).
+ - Creates intermediate nodes if to-path is not present
+ - Able to skip nodes if from-path is not found, defaults to False (from-nodes must be found; not skippable).
- Able to shift node only and delete children, defaults to False (nodes are shifted together with children).
For paths in `from_paths` and `to_paths`,
@@ -619,7 +643,7 @@ def shift_and_replace_nodes(
sep (str): path separator for input paths, applies to `from_path` and `to_path`
skippable (bool): indicator to skip if from path is not found, defaults to False
delete_children (bool): indicator to shift node only without children, defaults to False
- with_full_path (bool): indicator to shift/copy node with full path in `from_paths`, results in faster search,
+ with_full_path (bool): indicator to shift node with full path in `from_paths`, results in faster search,
defaults to False
"""
return replace_logic(
@@ -643,6 +667,7 @@ def copy_nodes_from_tree_to_tree(
sep: str = "/",
skippable: bool = False,
overriding: bool = False,
+ merge_attribute: bool = False,
merge_children: bool = False,
merge_leaves: bool = False,
delete_children: bool = False,
@@ -650,9 +675,10 @@ def copy_nodes_from_tree_to_tree(
) -> None:
"""Copy nodes from `from_paths` to `to_paths` *in-place*.
- - Creates intermediate nodes if to path is not present
- - Able to skip nodes if from path is not found, defaults to False (from-nodes must be found; not skippable).
+ - Creates intermediate nodes if to-path is not present
+ - Able to skip nodes if from-path is not found, defaults to False (from-nodes must be found; not skippable).
- Able to override existing node if it exists, defaults to False (to-nodes must not exist; not overridden).
+ - Able to merge attributes of node if it exists, defaults to False (to-nodes must not exist; no attributes to merge).
- Able to merge children and remove intermediate parent node, defaults to False (nodes are shifted; not merged).
- Able to merge only leaf nodes and remove all intermediate nodes, defaults to False (nodes are shifted; not merged)
- Able to copy node only and delete children, defaults to False (nodes are copied together with children).
@@ -683,6 +709,11 @@ def copy_nodes_from_tree_to_tree(
- If `to_path` is present, and ``overriding=False``, original children and leaves are merged.
- If `to_path` is present and ``overriding=True``, it behaves like overriding and only new leaves are retained.
+ Note:
+
+ - `merge_children` and `merge_leaves` cannot be both True at the same time.
+ - `overriding` and `merge_attribute` cannot be both True at the same time.
+
Examples:
>>> from bigtree import Node, str_to_tree, copy_nodes_from_tree_to_tree
>>> root = str_to_tree(
@@ -749,6 +780,7 @@ def copy_nodes_from_tree_to_tree(
In ``merge_children=True`` case, child nodes are copied instead of the parent node.
- If the path already exists, child nodes are merged with existing children.
+ - Otherwise, the child nodes of the node are merged with the node's parent.
>>> root_other = str_to_tree(
... "Documents\\n"
@@ -778,6 +810,7 @@ def copy_nodes_from_tree_to_tree(
In ``merge_leaves=True`` case, leaf nodes are copied instead of the parent node.
- If the path already exists, leaf nodes are merged with existing children.
+ - Otherwise, the leaf nodes of the node are merged with the node's parent.
>>> root_other = str_to_tree(
... "Documents\\n"
@@ -829,10 +862,14 @@ def copy_nodes_from_tree_to_tree(
sep (str): path separator for input paths, applies to `from_path` and `to_path`
skippable (bool): indicator to skip if from path is not found, defaults to False
overriding (bool): indicator to override existing to path if there is clashes, defaults to False
- merge_children (bool): indicator to merge children and remove intermediate parent node, defaults to False
- merge_leaves (bool): indicator to merge leaf nodes and remove intermediate parent node(s), defaults to False
+ merge_attribute (bool): indicator to merge attributes of from-path and to-path if there are clashes, defaults
+ to False
+ merge_children (bool): indicator to merge from-path's children and remove intermediate parent node, defaults to
+ False
+ merge_leaves (bool): indicator to merge from-path's leaf nodes and remove intermediate parent node(s), defaults
+ to False
delete_children (bool): indicator to copy node only without children, defaults to False
- with_full_path (bool): indicator to shift/copy node with full path in `from_paths`, results in faster search,
+ with_full_path (bool): indicator to copy node with full path in `from_paths`, results in faster search,
defaults to False
"""
return copy_or_shift_logic(
@@ -843,6 +880,7 @@ def copy_nodes_from_tree_to_tree(
copy=True,
skippable=skippable,
overriding=overriding,
+ merge_attribute=merge_attribute,
merge_children=merge_children,
merge_leaves=merge_leaves,
delete_children=delete_children,
@@ -863,8 +901,8 @@ def copy_and_replace_nodes_from_tree_to_tree(
) -> None:
"""Copy nodes from `from_paths` to *replace* `to_paths` *in-place*.
- - Creates intermediate nodes if to path is not present
- - Able to skip nodes if from path is not found, defaults to False (from-nodes must be found; not skippable).
+ - Creates intermediate nodes if to-path is not present
+ - Able to skip nodes if from-path is not found, defaults to False (from-nodes must be found; not skippable).
- Able to copy node only and delete children, defaults to False (nodes are copied together with children).
For paths in `from_paths` and `to_paths`,
@@ -959,12 +997,12 @@ def copy_and_replace_nodes_from_tree_to_tree(
Args:
from_tree (Node): tree to copy nodes from
to_tree (Node): tree to copy nodes to
- from_paths (List[str]): original paths to shift nodes from
- to_paths (List[str]): new paths to shift nodes to
+ from_paths (List[str]): original paths to copy nodes from
+ to_paths (List[str]): new paths to copy nodes to
sep (str): path separator for input paths, applies to `from_path` and `to_path`
skippable (bool): indicator to skip if from path is not found, defaults to False
delete_children (bool): indicator to copy node only without children, defaults to False
- with_full_path (bool): indicator to shift/copy node with full path in `from_paths`, results in faster search,
+ with_full_path (bool): indicator to copy node with full path in `from_paths`, results in faster search,
defaults to False
"""
return replace_logic(
@@ -980,6 +1018,60 @@ def copy_and_replace_nodes_from_tree_to_tree(
) # pragma: no cover
+def _merge_attribute(
+ from_node: T, to_node: T, copy: bool, merge_children: bool, merge_leaves: bool
+) -> T:
+ import pandas as pd
+
+ from bigtree.tree import export
+
+ if copy:
+ from_node = from_node.copy()
+
+ dummy_to_node = to_node.copy()
+ dummy_to_node.parent = None
+ to_node_data = export.tree_to_dataframe(dummy_to_node, all_attrs=True)
+
+ dummy_from_node = from_node
+ dummy_from_node.parent = None
+ if merge_leaves:
+ dummy_from_node.children = list(dummy_from_node.leaves) # type: ignore
+ from_node_data = export.tree_to_dataframe(dummy_from_node, all_attrs=True)
+
+ common_attributes = set(to_node_data.columns).intersection(
+ set(from_node_data.columns)
+ ) - {"path", "name"}
+ if merge_children or merge_leaves:
+ merged_data = pd.merge(
+ to_node_data,
+ from_node_data,
+ on=["path", "name"],
+ how="outer",
+ suffixes=("_x", "_y"),
+ )
+ else:
+ merged_data = pd.merge(
+ to_node_data,
+ from_node_data,
+ on=["path", "name"],
+ how="left",
+ suffixes=("_x", "_y"),
+ )
+
+ # Upsert data
+ for common_attribute in common_attributes:
+ merged_data[common_attribute] = merged_data[common_attribute + "_y"].fillna(
+ merged_data[common_attribute + "_x"]
+ )
+ merged_data = merged_data.drop(
+ columns=[
+ common_attribute + "_x",
+ common_attribute + "_y",
+ ]
+ )
+ return construct.dataframe_to_tree(merged_data)
+
+
def copy_or_shift_logic(
tree: T,
from_paths: List[str],
@@ -988,6 +1080,7 @@ def copy_or_shift_logic(
copy: bool = False,
skippable: bool = False,
overriding: bool = False,
+ merge_attribute: bool = False,
merge_children: bool = False,
merge_leaves: bool = False,
delete_children: bool = False,
@@ -996,10 +1089,11 @@ def copy_or_shift_logic(
) -> None:
"""Shift or copy nodes from `from_paths` to `to_paths` *in-place*.
- - Creates intermediate nodes if to path is not present
+ - Creates intermediate nodes if to-path is not present
- Able to copy node, defaults to False (nodes are shifted; not copied).
- - Able to skip nodes if from path is not found, defaults to False (from-nodes must be found; not skippable)
+ - Able to skip nodes if from-path is not found, defaults to False (from-nodes must be found; not skippable)
- Able to override existing node if it exists, defaults to False (to-nodes must not exist; not overridden)
+ - Able to merge attributes of node if it exists, defaults to False (to-nodes must not exist; no attributes to merge).
- Able to merge children and remove intermediate parent node, defaults to False (nodes are shifted; not merged)
- Able to merge only leaf nodes and remove all intermediate nodes, defaults to False (nodes are shifted; not merged)
- Able to shift/copy node only and delete children, defaults to False (nodes are shifted/copied together with children).
@@ -1033,6 +1127,11 @@ def copy_or_shift_logic(
- If `to_path` is present and ``overriding=True``, it behaves like overriding and only new leaves are retained,
original non-leaf nodes in `from_path` are retained.
+ Note:
+
+ - `merge_children` and `merge_leaves` cannot be both True at the same time.
+ - `overriding` and `merge_attribute` cannot be both True at the same time.
+
Args:
tree (Node): tree to modify
from_paths (List[str]): original paths to shift nodes from
@@ -1041,8 +1140,12 @@ def copy_or_shift_logic(
copy (bool): indicator to copy node, defaults to False
skippable (bool): indicator to skip if from path is not found, defaults to False
overriding (bool): indicator to override existing to path if there is clashes, defaults to False
- merge_children (bool): indicator to merge children and remove intermediate parent node, defaults to False
- merge_leaves (bool): indicator to merge leaf nodes and remove intermediate parent node(s), defaults to False
+ merge_attribute (bool): indicator to merge attributes of from-path and to-path if there are clashes, defaults
+ to False
+ merge_children (bool): indicator to merge from-path's children and remove intermediate parent node, defaults to
+ False
+ merge_leaves (bool): indicator to merge from-path's leaf nodes and remove intermediate parent node(s), defaults
+ to False
delete_children (bool): indicator to shift/copy node only without children, defaults to False
to_tree (Node): tree to copy to, defaults to None
with_full_path (bool): indicator to shift/copy node with full path in `from_paths`, results in faster search,
@@ -1052,6 +1155,10 @@ def copy_or_shift_logic(
raise ValueError(
"Invalid shifting, can only specify one type of merging, check `merge_children` and `merge_leaves`"
)
+ if overriding and merge_attribute:
+ raise ValueError(
+ "Invalid shifting, can only specify one type of merging, check `overriding` and `merge_attribute`"
+ )
if not (isinstance(from_paths, list) and isinstance(to_paths, list)):
raise ValueError(
"Invalid type, `from_paths` and `to_paths` should be list type"
@@ -1109,6 +1216,10 @@ def copy_or_shift_logic(
# Perform shifting/copying
for from_path, to_path in zip(from_paths, to_paths):
+ # Reset parameters
+ merge_children2 = merge_children
+ merge_leaves2 = merge_leaves
+
if with_full_path:
from_node = search.find_full_path(tree, from_path)
else:
@@ -1149,43 +1260,86 @@ def copy_or_shift_logic(
f"Alternatively, set `merge_children` or `merge_leaves` to True if intermediate node is to be removed"
)
elif merge_children:
- # Specify override to remove existing node, else children are merged
- if not overriding:
- logging.info(
- f"Path {to_path} already exists and children are merged"
- )
- else:
+ if overriding:
logging.info(
f"Path {to_path} already exists and its children be overridden by the merge"
)
parent = to_node.parent
to_node.parent = None
to_node = parent
- merge_children = False
- elif merge_leaves:
- # Specify override to remove existing node, else leaves are merged
- if not overriding:
+ merge_children2 = False
+ elif merge_attribute:
logging.info(
- f"Path {to_path} already exists and leaves are merged"
+ f"Path {to_path} already exists and their attributes will be merged"
+ )
+ from_node = _merge_attribute(
+ from_node,
+ to_node,
+ copy=copy,
+ merge_children=merge_children,
+ merge_leaves=merge_leaves,
)
+ parent = to_node.parent
+ to_node.parent = None
+ to_node = parent
+ merge_children2 = False
else:
+ logging.info(
+ f"Path {to_path} already exists and children are merged"
+ )
+ elif merge_leaves:
+ if overriding:
logging.info(
f"Path {to_path} already exists and its leaves be overridden by the merge"
)
del to_node.children
+ elif merge_attribute:
+ logging.info(
+ f"Path {to_path} already exists and their attributes will be merged"
+ )
+ from_node = _merge_attribute(
+ from_node,
+ to_node,
+ copy=copy,
+ merge_children=merge_children,
+ merge_leaves=merge_leaves,
+ )
+ parent = to_node.parent
+ to_node.parent = None
+ to_node = parent
+ merge_leaves2 = False
+ else:
+ logging.info(
+ f"Path {to_path} already exists and leaves are merged"
+ )
else:
- if not overriding:
+ if not (overriding or merge_attribute):
raise exceptions.TreeError(
f"Path {to_path} already exists and unable to override\n"
- f"Set `overriding` to True to perform overrides\n"
+ f"Set `overriding` or `merge_attribute` to True to handle node name clashes\n"
f"Alternatively, set `merge_children` to True if nodes are to be merged"
)
- logging.info(
- f"Path {to_path} already exists and will be overridden"
- )
- parent = to_node.parent
- to_node.parent = None
- to_node = parent
+ if overriding:
+ logging.info(
+ f"Path {to_path} already exists and will be overridden"
+ )
+ parent = to_node.parent
+ to_node.parent = None
+ to_node = parent
+ elif merge_attribute:
+ logging.info(
+ f"Path {to_path} already exists and attributes will be upserted"
+ )
+ from_node = _merge_attribute(
+ from_node,
+ to_node,
+ copy=copy,
+ merge_children=merge_children,
+ merge_leaves=merge_leaves,
+ )
+ parent = to_node.parent
+ to_node.parent = None
+ to_node = parent
# To node not found
else:
@@ -1196,10 +1350,15 @@ def copy_or_shift_logic(
)
# Reassign from_node to new parent
+ # if merge_children and (overriding or merge_attribute):
+ # merge_children = False
+ # if merge_leaves and merge_attribute:
+ # merge_leaves = False
if copy:
logging.debug(f"Copying {from_node.node_name}")
from_node = from_node.copy()
- if merge_children:
+ if merge_children2:
+ # overriding / merge_attribute handled merge_children, set merge_children=False
logging.debug(
f"Reassigning children from {from_node.node_name} to {to_node.node_name}"
)
@@ -1208,7 +1367,7 @@ def copy_or_shift_logic(
del children.children
children.parent = to_node
from_node.parent = None
- elif merge_leaves:
+ elif merge_leaves2:
logging.debug(
f"Reassigning leaf nodes from {from_node.node_name} to {to_node.node_name}"
)
@@ -1233,9 +1392,9 @@ def replace_logic(
) -> None:
"""Shift or copy nodes from `from_paths` to *replace* `to_paths` *in-place*.
- - Creates intermediate nodes if to path is not present
+ - Creates intermediate nodes if to-path is not present
- Able to copy node, defaults to False (nodes are shifted; not copied).
- - Able to skip nodes if from path is not found, defaults to False (from-nodes must be found; not skippable)
+ - Able to skip nodes if from-path is not found, defaults to False (from-nodes must be found; not skippable)
- Able to replace node only and delete children, defaults to False (nodes are shifted/copied together with children).
- Able to shift/copy nodes from one tree to another tree, defaults to None (shifting/copying happens within same tree)
diff --git a/docs/bigtree/tree/modify.md b/docs/bigtree/tree/modify.md
index 58c8b562..b158788b 100644
--- a/docs/bigtree/tree/modify.md
+++ b/docs/bigtree/tree/modify.md
@@ -14,19 +14,26 @@ There are two types of modification available
In **non-replacing scenario**, there are several configurations available for customization.
-| Configuration | Description | Default Value |
-|-------------------|----------------------------------------------------------------------------------------|---------------------------------------------------------|
-| `copy` | Indicates whether it is to shift the nodes, or copy the nodes | False (nodes are shifted, not copied) |
-| `to_tree` | Indicates whether shifting/copying is within the same tree, or between different trees | None (nodes are shifted/copied within the same tree) |
-| `skippable` | Skip shifting/copying of nodes if from_path cannot be found | False (from-node must be found) |
-| `overriding` | Override existing node if it exists | False (to-node must not exist) |
-| `merge_children` | Shift/copy children of from-node and remove intermediate parent node | False (children are not merged) |
- | `merge_leaves` | Shift/copy leaves of from-node and remove all intermediate nodes | False (leaves are not merged) |
-| `delete_children` | Shift/copy node only and delete its children | False (nodes are shifted/copied together with children) |
-
-In **replacing scenario**, all the configurations are also available except `overriding`, `merge_children`, and `merge_leaves` as it is doing a one-to-one replacement.
+| Configuration | Description | Default Value |
+|-------------------|----------------------------------------------------------------------------------------|-----------------------------------------------------------|
+| `copy` | Indicates whether it is to shift the nodes, or copy the nodes | False (nodes are shifted, not copied) |
+| `to_tree` | Indicates whether shifting/copying is within the same tree, or between different trees | None (nodes are shifted/copied within the same tree) |
+| `skippable` | Skip shifting/copying of nodes if from_path cannot be found | False (from-node must be found) |
+| `overriding` | Override existing node if it exists | False (to-node must not exist) |
+| `merge_attribute` | Merge attributes of existing node if it exists | False (to-node must not exist, attributes are not merged) |
+| `merge_children` | Shift/copy children of from-node and remove intermediate parent node | False (children are not merged) |
+| `merge_leaves` | Shift/copy leaves of from-node and remove all intermediate nodes | False (leaves are not merged) |
+| `delete_children` | Shift/copy node only and delete its children | False (nodes are shifted/copied together with children) |
+
+In **replacing scenario**, all the configurations are also available except `overriding`, `merge_attribute`,
+`merge_children`, and `merge_leaves` as it is doing a one-to-one replacement.
It is by default overriding, and there is nothing to merge.
+!!! note
+
+ `overriding` and `merge_attribute` cannot be simultaneously set to `True`. One deals with clashing nodes by
+ overriding, another deals with it by merging attributes of both nodes.
+
!!! note
`merge_children` and `merge_leaves` cannot be simultaneously set to `True`.
@@ -71,14 +78,49 @@ All other methods calls these 2 methods directly.
### Sample Tree Modification (Advanced)
-| Setting | Sample path in `from_paths` | Sample path in `to_paths` | Description |
-|-----------------------------|-----------------------------|---------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| overriding | "a/b/c" | "a/d/c" | Shift/copy node `c`, override if "a/d/c" exists |
-| merge_children | "a/b/c" | "a/d/c" | **If path not present**: Shift/copy children of node `c` to be children of node `d`, removing node `c`
**If path present**: Shift/copy children of node `c` to be merged with existing "a/d/c" children |
-| merge_children + overriding | "a/b/c" | "a/d/c" | **If path not present**: Behaves like merge_children
**If path present**: Behaves like overriding |
-| merge_leaves | "a/b/c" | "a/d/c" | **If path not present**: Shift/copy leaves of node `c` to be children of node `d`
**If path present**: Shift/copy leaves of node `c` to be merged with existing "a/d/c" children |
-| merge_leaves + overriding | "a/b/c" | "a/d/c" | **If path not present**: Behaves like merge_leaves
**If path present**: Behaves like overriding, but original node `c` remains |
-| delete_children | "a/b" | "a/d/b" | Shift/copy node `b` only without any node `b` children |
+| Setting | Sample path in `from_paths` | Sample path in `to_paths` | Description |
+|---------------------------------------------|-----------------------------|---------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| overriding | "a/b/c" | "a/d/c" | Shift/copy node `c`, override if "a/d/c" exists |
+| merge_children | "a/b/c" | "a/d/c" | **If path not present**: Shift/copy children of node `c` to be children of node `d`, removing node `c`
**If path present**: Shift/copy children of node `c` to be merged with existing "a/d/c" children |
+| merge_children + overriding/merge_attribute | "a/b/c" | "a/d/c" | **If path not present**: Behaves like merge_children
**If path present**: Behaves like overriding/merge_attribute |
+| merge_leaves | "a/b/c" | "a/d/c" | **If path not present**: Shift/copy leaves of node `c` to be children of node `d`
**If path present**: Shift/copy leaves of node `c` to be merged with existing "a/d/c" children |
+| merge_leaves + overriding/merge_attribute | "a/b/c" | "a/d/c" | **If path not present**: Behaves like merge_leaves
**If path present**: Behaves like overriding/merge_attribute, but original node `c` remains |
+| delete_children | "a/b" | "a/d/b" | Shift/copy node `b` only without any node `b` children |
+
+## Guideline
+
+If you're still feeling lost over the parameters, here are some guiding questions to ask yourself.
+
+- Do I want to retain the original node where they are?
+ - Yes: Set `copy=True`
+ - Default performs a shift instead of copy
+- Am I unsure of what nodes I am going to shift, they may or may not exist and this is perfectly fine?
+ - Yes: Set `skippable=True`
+ - Default throws error if origin node is not found
+- The origin node (and its descendants) may clash with the destination node(s), how do I want to handle it?
+ - Set `overriding=True` to overwrite origin node
+ - Set `merge_attribute=True` to combine both nodes' attributes
+ - Default throws error about the clash in node name
+- I want to shift everything under the node, but not the node itself
+ - Set `merge_children=True` or `merge_leaves=True` to shift the children and leaf nodes respectively
+ - Default shifts the node itself, and everything under it
+- I want to shift the node and only the node, and not everything under it
+ - Yes: Set `delete_children=True`
+ - Default shifts the node itself, and everything under it
+- I want to shift things from one tree to another tree
+ - Specify `to_tree`
+ - Default shifts nodes within the same tree
+
+What about the permutations between the parameters?
+
+- These parameters are standalone and does not produce any interaction effect
+ - `copy`, `skippable`, `delete_children`
+- These parameters have some interaction:
+ - `overriding` and `merge_attribute` with `merge_children` and `merge_leaves`
+ - `overriding` + `merge_children`: Behaves like `merge_children` when there is no clash in node name, otherwise behaves like `overriding`. Note that clashes will preserve destination nodes' children only.
+ - `overriding` + `merge_leaves`: Behaves like `merge_leaves` when there is no clash in node name, otherwise behaves like `overriding`. Note that clashes will preserve destination nodes' leaves only.
+ - `merge_attribute` + `merge_children`: Behaves like `merge_children` when there is no clash in node name, otherwise behaves like `merge_attribute`. Note that attributes will be merged for node and all descendants, and will preserve origin and destination nodes' children.
+ - `merge_attribute` + `merge_leaves`: Behaves like `merge_leaves` when there is no clash in node name, otherwise behaves like `merge_attribute`. Note that attributes will be merged for node and all descendants, and will preserve origin nodes' children and destination nodes' leaves.
-----
diff --git a/tests/test_constants.py b/tests/test_constants.py
index 20b3428d..7484d925 100644
--- a/tests/test_constants.py
+++ b/tests/test_constants.py
@@ -179,6 +179,7 @@ class Constants:
"Paths are different length, input `from_paths` have {n1} entries, "
"while output `to_paths` have {n2} entries"
)
+ ERROR_MODIFY_PARAM_OVERRIDING_OR_MERGE_ATTRIBUTE = "Invalid shifting, can only specify one type of merging, check `overriding` and `merge_attribute`"
ERROR_MODIFY_PARAM_MERGE_CHILDREN_OR_LEAVES = "Invalid shifting, can only specify one type of merging, check `merge_children` and `merge_leaves`"
ERROR_MODIFY_PARAM_DELETE_AND_COPY = (
"Deletion of node will not happen if `copy=True`, check your `copy` parameter."
@@ -196,7 +197,7 @@ class Constants:
)
ERROR_MODIFY_OVERRIDING = (
"Path {to_path} already exists and unable to override\n"
- "Set `overriding` to True to perform overrides\n"
+ "Set `overriding` or `merge_attribute` to True to handle node name clashes\n"
"Alternatively, set `merge_children` to True if nodes are to be merged"
)
diff --git a/tests/tree/test_modify.py b/tests/tree/test_modify.py
index c3bd74f1..0297bc98 100644
--- a/tests/tree/test_modify.py
+++ b/tests/tree/test_modify.py
@@ -267,7 +267,7 @@ def test_copy_nodes_skippable_error(self):
assert_tree_structure_node_root(self.root)
# overriding
- def test_copy_nodes_delete_and_overriding_error(self):
+ def test_copy_nodes_overriding_error(self):
to_path = "a/b/d"
new_aa = node.Node("aa", parent=self.root)
new_d = node.Node("d")
@@ -280,7 +280,7 @@ def test_copy_nodes_delete_and_overriding_error(self):
to_path=to_path
)
- def test_copy_nodes_delete_and_overriding(self):
+ def test_copy_nodes_overriding(self):
new_aa = node.Node("aa", parent=self.root)
new_d = node.Node("d", age=1)
new_d.parent = new_aa
@@ -297,7 +297,7 @@ def test_copy_nodes_delete_and_overriding(self):
assert_tree_structure_basenode_root_attr(self.root, d=("d", 1))
assert_tree_structure_node_root(self.root)
- def test_copy_nodes_overriding(self):
+ def test_copy_nodes_overriding_manual_check(self):
from_paths = ["a/aa/bb"]
to_paths = ["/a/bb"]
modify.copy_nodes(
@@ -322,6 +322,95 @@ def test_copy_nodes_overriding(self):
list(search.find_path(self.root_overriding, "/a/aa").children)
), "Children of node parent deleted (not copied)"
+ # merge_attribute
+ def test_copy_nodes_merge_attribute(self):
+ from_paths = ["a/aa/bb"]
+ to_paths = ["/a/bb"]
+
+ # Set attribute for node
+ self.root_overriding["aa"]["bb"].set_attrs({"gender": "b"})
+ self.root_overriding["bb"].set_attrs({"age": 1})
+ modify.copy_nodes(
+ self.root_overriding,
+ from_paths,
+ to_paths,
+ merge_attribute=True,
+ )
+ assert (
+ self.root_overriding["bb"].get_attr("age") == 1
+ ), "Original attribute not present"
+ assert (
+ self.root_overriding["bb"].get_attr("gender") == "b"
+ ), "Merge attribute not present"
+ assert (
+ len(self.root_overriding["bb"].children) == 1
+ ), "Original node children changed"
+
+ def test_copy_nodes_merge_attribute_update(self):
+ from_paths = ["a/aa/bb"]
+ to_paths = ["/a/bb"]
+
+ # Set attribute for node
+ self.root_overriding["aa"]["bb"].set_attrs({"age": 2, "gender": "b"})
+ self.root_overriding["bb"].set_attrs({"age": 1, "hello": "world"})
+ modify.copy_nodes(
+ self.root_overriding,
+ from_paths,
+ to_paths,
+ merge_attribute=True,
+ )
+ assert (
+ self.root_overriding["bb"].get_attr("age") == 2
+ ), "Original attribute not updated"
+ assert (
+ self.root_overriding["bb"].get_attr("hello") == "world"
+ ), "Original attribute not present"
+ assert (
+ self.root_overriding["bb"].get_attr("gender") == "b"
+ ), "Merge attribute not present"
+ assert (
+ len(self.root_overriding["bb"].children) == 1
+ ), "Original node children changed"
+
+ def test_copy_nodes_merge_attribute_children(self):
+ from_paths = ["a/aa/bb"]
+ to_paths = ["/a/bb"]
+
+ # Set attribute for node and child node
+ self.root_overriding["aa"]["bb"]["cc"].set_attrs({"age": 2, "gender": "c"})
+ self.root_overriding["aa"]["bb"].set_attrs({"age": 2, "gender": "b"})
+ self.root_overriding["bb"].set_attrs({"age": 1, "hello": "world"})
+ self.root_overriding["bb"]["cc2"].set_attrs(
+ {"name": "cc", "age": 1, "hello": "world"}
+ )
+ modify.copy_nodes(
+ self.root_overriding,
+ from_paths,
+ to_paths,
+ merge_attribute=True,
+ )
+ assert (
+ self.root_overriding["bb"].get_attr("age") == 2
+ ), "Original attribute not updated"
+ assert (
+ self.root_overriding["bb"].get_attr("hello") == "world"
+ ), "Original attribute not present"
+ assert (
+ self.root_overriding["bb"].get_attr("gender") == "b"
+ ), "Merge attribute not present"
+ assert (
+ len(self.root_overriding["bb"].children) == 1
+ ), "Original node children changed"
+ assert (
+ self.root_overriding["bb"]["cc"].get_attr("age") == 2
+ ), "Original attribute of children not updated"
+ assert (
+ self.root_overriding["bb"]["cc"].get_attr("hello") == "world"
+ ), "Original attribute of children not present"
+ assert (
+ self.root_overriding["bb"]["cc"].get_attr("gender") == "c"
+ ), "Merge attribute of children not present"
+
# merge_children
def test_copy_nodes_merge_children(self):
from_paths = ["aa/bb/cc"]
@@ -521,6 +610,71 @@ def test_copy_nodes_merge_children_overriding(self):
list(search.find_path(self.root, "/a/aa").children)
), "Node parent deleted"
+ def test_copy_nodes_merge_children_overriding_multiple(self):
+ new_aa = node.Node("aa", parent=self.root)
+ new_bb = node.Node("bb", parent=new_aa)
+ new_cc = node.Node("cc", age=1)
+ new_cc.parent = new_bb
+ new_dd = node.Node("dd", parent=new_aa)
+ new_ee = node.Node("ee", age=1)
+ new_ee.parent = new_dd
+ bb2 = node.Node("bb", parent=self.root)
+ cc2 = node.Node("cc2")
+ cc2.parent = bb2
+
+ from_paths = ["/d", "/e", "/g", "/h", "/f"]
+ to_paths = ["a/b/d", "a/b/e", "a/b/e/g", "a/b/e/h", "a/c/f"]
+ modify.shift_nodes(self.root, from_paths, to_paths)
+
+ from_paths = ["a/aa/bb", "a/aa/dd"]
+ to_paths = ["/a/bb", "a/dd"]
+ modify.copy_nodes(
+ self.root, from_paths, to_paths, overriding=True, merge_children=True
+ )
+ assert search.find_path(
+ self.root, "/a/bb/cc"
+ ), "Node children not merged, new children not present"
+ assert not search.find_path(
+ self.root, "/a/bb/cc2"
+ ), "Node children not merged, original children not overridden"
+ assert (
+ search.find_path(self.root, "/a/bb/cc").get_attr("age") == 1
+ ), f"Merged children not overridden, {export.print_tree(self.root)}"
+ assert len(
+ list(search.find_path(self.root, "/a/aa").children)
+ ), "Node parent deleted"
+ assert search.find_path(
+ self.root, "/a/ee"
+ ), "Node children not merged, new children not present"
+
+ # merge_children, merge_attribute
+ def test_copy_nodes_merge_children_merge_attribute(self):
+ from_paths = ["a/aa/bb"]
+ to_paths = ["/a/bb"]
+
+ # Set attribute for node
+ self.root_overriding["aa"]["bb"].set_attrs({"age": 2, "gender": "b"})
+ self.root_overriding["bb"].set_attrs({"age": 1, "hello": "world"})
+ modify.copy_nodes(
+ self.root_overriding,
+ from_paths,
+ to_paths,
+ merge_attribute=True,
+ merge_children=True,
+ )
+ assert (
+ self.root_overriding["bb"].get_attr("age") == 2
+ ), "Original attribute not updated"
+ assert (
+ self.root_overriding["bb"].get_attr("hello") == "world"
+ ), "Original attribute not present"
+ assert (
+ self.root_overriding["bb"].get_attr("gender") == "b"
+ ), "Merge attribute not present"
+ assert (
+ len(self.root_overriding["bb"].children) == 2
+ ), "Original node children not combined with copied node"
+
# merge_leaves, overriding
def test_copy_nodes_merge_leaves_overriding(self):
new_aa = node.Node("aa", parent=self.root)
@@ -557,6 +711,70 @@ def test_copy_nodes_merge_leaves_overriding(self):
list(search.find_path(self.root, "/a/aa").children)
), "Node parent deleted"
+ # merge_leaves, merge_attribute
+ def test_copy_nodes_merge_leaves_merge_attribute(self):
+ from_paths = ["a/aa/bb"]
+ to_paths = ["/a/bb"]
+
+ # Set attribute for node
+ self.root_overriding["aa"]["bb"].set_attrs({"age": 2, "gender": "b"})
+ self.root_overriding["aa"]["bb"]["cc"].extend(
+ [node.Node("cc2", age=2, gender="c")]
+ )
+ self.root_overriding["bb"].set_attrs({"age": 1, "hello": "world"})
+ self.root_overriding["bb"]["cc2"].set_attrs({"age": 1, "hello": "world"})
+ modify.copy_nodes(
+ self.root_overriding,
+ from_paths,
+ to_paths,
+ merge_attribute=True,
+ merge_leaves=True,
+ )
+ assert (
+ self.root_overriding["bb"].get_attr("age") == 2
+ ), "Original attribute not updated"
+ assert (
+ self.root_overriding["bb"].get_attr("hello") == "world"
+ ), "Original attribute not present"
+ assert (
+ self.root_overriding["bb"].get_attr("gender") == "b"
+ ), "Merge attribute not present"
+ assert (
+ len(self.root_overriding["bb"].children) == 2
+ ), "Original node children not combined with copied node"
+ assert (
+ self.root_overriding["bb"]["cc2"].get_attr("age") == 2
+ ), "Original attribute of leaf not updated"
+ assert (
+ self.root_overriding["bb"]["cc2"].get_attr("hello") == "world"
+ ), "Original attribute of leaf not present"
+ assert (
+ self.root_overriding["bb"]["cc2"].get_attr("gender") == "c"
+ ), "Merge attribute of leaf not present"
+ assert (
+ self.root_overriding["bb"]["dd"].get_attr("age") == 2
+ ), "New leaf not copied over"
+
+ # overriding, merge_attribute
+ def test_copy_nodes_overriding_and_merge_attribute_error(self):
+ a = node.Node("a", age=90)
+ self.root_other.parent = a
+
+ from_paths = ["a/aa/d", "a/aa/e", "a/aa/g", "a/aa/h", "a/aa/f"]
+ to_paths = ["a/aa/b/d", "a/aa/b/e", "a/aa/b/e/g", "a/aa/b/e/h", "a/aa/c/f"]
+ modify.shift_nodes(a, from_paths, to_paths)
+
+ from_paths = ["a/aa"]
+ to_paths = ["a/bb/aa"]
+ with pytest.raises(ValueError) as exc_info:
+ modify.copy_nodes(
+ a, from_paths, to_paths, overriding=True, merge_attribute=True
+ )
+ assert (
+ str(exc_info.value)
+ == Constants.ERROR_MODIFY_PARAM_OVERRIDING_OR_MERGE_ATTRIBUTE
+ )
+
# merge_children, merge_leaves
def test_copy_nodes_merge_children_and_leaf_error(self):
a = node.Node("a", age=90)
@@ -839,7 +1057,7 @@ def test_shift_nodes_skippable_error(self):
assert_tree_structure_node_root(self.root)
# overriding
- def test_shift_nodes_delete_and_overriding_error(self):
+ def test_shift_nodes_overriding_error(self):
to_path = "a/b/d"
new_aa = node.Node("aa", parent=self.root)
new_d = node.Node("d")
@@ -852,7 +1070,7 @@ def test_shift_nodes_delete_and_overriding_error(self):
to_path=to_path
)
- def test_shift_nodes_delete_and_overriding(self):
+ def test_shift_nodes_overriding(self):
new_aa = node.Node("aa", parent=self.root)
new_d = node.Node("d", age=1)
new_d.parent = new_aa
@@ -863,7 +1081,7 @@ def test_shift_nodes_delete_and_overriding(self):
assert_tree_structure_basenode_root_attr(self.root, d=("d", 1))
assert_tree_structure_node_root(self.root)
- def test_shift_nodes_overriding(self):
+ def test_shift_nodes_overriding_manual_check(self):
from_paths = ["a/aa/bb"]
to_paths = ["/a/bb"]
modify.shift_nodes(
@@ -888,6 +1106,106 @@ def test_shift_nodes_overriding(self):
list(search.find_path(self.root_overriding, "a/aa").children)
), "Children of node parent not deleted"
+ # merge_attribute
+ def test_shift_nodes_merge_attribute(self):
+ from_paths = ["a/aa/bb"]
+ to_paths = ["/a/bb"]
+
+ # Set attribute for node
+ self.root_overriding["aa"]["bb"].set_attrs({"gender": "b"})
+ self.root_overriding["bb"].set_attrs({"age": 1})
+
+ modify.shift_nodes(
+ self.root_overriding,
+ from_paths,
+ to_paths,
+ merge_attribute=True,
+ )
+ assert (
+ self.root_overriding["bb"].get_attr("age") == 1
+ ), "Original attribute not present"
+ assert (
+ self.root_overriding["bb"].get_attr("gender") == "b"
+ ), "Merge attribute not present"
+ assert search.find_path(
+ self.root_overriding, "a/bb/cc2"
+ ), "Original node children changed"
+ assert not len(
+ list(search.find_path(self.root_overriding, "a/aa").children)
+ ), "Original node not deleted"
+
+ def test_shift_nodes_merge_attribute_update(self):
+ from_paths = ["a/aa/bb"]
+ to_paths = ["/a/bb"]
+
+ # Set attribute for node
+ self.root_overriding["aa"]["bb"].set_attrs({"age": 2, "gender": "b"})
+ self.root_overriding["bb"].set_attrs({"age": 1, "hello": "world"})
+
+ modify.shift_nodes(
+ self.root_overriding,
+ from_paths,
+ to_paths,
+ merge_attribute=True,
+ )
+ assert (
+ self.root_overriding["bb"].get_attr("age") == 2
+ ), "Original attribute not updated"
+ assert (
+ self.root_overriding["bb"].get_attr("hello") == "world"
+ ), "Original attribute not present"
+ assert (
+ self.root_overriding["bb"].get_attr("gender") == "b"
+ ), "Merge attribute not present"
+ assert search.find_path(
+ self.root_overriding, "a/bb/cc2"
+ ), "Original node children changed"
+ assert not len(
+ list(search.find_path(self.root_overriding, "a/aa").children)
+ ), "Original node not deleted"
+
+ def test_shift_nodes_merge_attribute_children(self):
+ from_paths = ["a/aa/bb"]
+ to_paths = ["/a/bb"]
+
+ # Set attribute for node
+ self.root_overriding["aa"]["bb"]["cc"].set_attrs({"age": 2, "gender": "c"})
+ self.root_overriding["aa"]["bb"].set_attrs({"age": 2, "gender": "b"})
+ self.root_overriding["bb"].set_attrs({"age": 1, "hello": "world"})
+ self.root_overriding["bb"]["cc2"].set_attrs(
+ {"name": "cc", "age": 1, "hello": "world"}
+ )
+ modify.shift_nodes(
+ self.root_overriding,
+ from_paths,
+ to_paths,
+ merge_attribute=True,
+ )
+ assert (
+ self.root_overriding["bb"].get_attr("age") == 2
+ ), "Original attribute not updated"
+ assert (
+ self.root_overriding["bb"].get_attr("hello") == "world"
+ ), "Original attribute not present"
+ assert (
+ self.root_overriding["bb"].get_attr("gender") == "b"
+ ), "Merge attribute not present"
+ assert search.find_path(
+ self.root_overriding, "a/bb/cc"
+ ), "Original node children changed"
+ assert not len(
+ list(search.find_path(self.root_overriding, "a/aa").children)
+ ), "Original node not deleted"
+ assert (
+ self.root_overriding["bb"]["cc"].get_attr("age") == 2
+ ), "Original attribute of children not updated"
+ assert (
+ self.root_overriding["bb"]["cc"].get_attr("hello") == "world"
+ ), "Original attribute of children not present"
+ assert (
+ self.root_overriding["bb"]["cc"].get_attr("gender") == "c"
+ ), "Merge attribute of children not present"
+
# merge_children
def test_shift_nodes_merge_children(self):
from_paths = ["aa/bb/cc"]
@@ -1088,6 +1406,37 @@ def test_shift_nodes_merge_children_overriding(self):
list(search.find_path(self.root, "a/aa").children)
), "Node parent not deleted"
+ # merge_children, merge_attribute
+ def test_shift_nodes_merge_children_merge_attribute(self):
+ from_paths = ["a/aa/bb"]
+ to_paths = ["/a/bb"]
+
+ # Set attribute for node
+ self.root_overriding["aa"]["bb"].set_attrs({"age": 2, "gender": "b"})
+ self.root_overriding["bb"].set_attrs({"age": 1, "hello": "world"})
+ modify.shift_nodes(
+ self.root_overriding,
+ from_paths,
+ to_paths,
+ merge_attribute=True,
+ merge_children=True,
+ )
+ assert (
+ self.root_overriding["bb"].get_attr("age") == 2
+ ), "Original attribute not updated"
+ assert (
+ self.root_overriding["bb"].get_attr("hello") == "world"
+ ), "Original attribute not present"
+ assert (
+ self.root_overriding["bb"].get_attr("gender") == "b"
+ ), "Merge attribute not present"
+ assert (
+ len(self.root_overriding["bb"].children) == 2
+ ), "Original node children not combined with copied node"
+ assert not len(
+ list(search.find_path(self.root_overriding, "a/aa").children)
+ ), "Original node not deleted"
+
# merge_leaves, overriding
def test_shift_nodes_merge_leaves_overriding(self):
new_aa = node.Node("aa", parent=self.root)
@@ -1124,6 +1473,53 @@ def test_shift_nodes_merge_leaves_overriding(self):
list(search.find_path(self.root, "a/aa").children)
), "Node parent deleted"
+ # merge_leaves, merge_attribute
+ def test_shift_nodes_merge_leaves_merge_attribute(self):
+ from_paths = ["a/aa/bb"]
+ to_paths = ["/a/bb"]
+
+ # Set attribute for node
+ self.root_overriding["aa"]["bb"].set_attrs({"age": 2, "gender": "b"})
+ self.root_overriding["aa"]["bb"]["cc"].extend(
+ [node.Node("cc2", age=2, gender="c")]
+ )
+ self.root_overriding["bb"].set_attrs({"age": 1, "hello": "world"})
+ self.root_overriding["bb"]["cc2"].set_attrs({"age": 1, "hello": "world"})
+ modify.shift_nodes(
+ self.root_overriding,
+ from_paths,
+ to_paths,
+ merge_attribute=True,
+ merge_leaves=True,
+ )
+ assert (
+ self.root_overriding["bb"].get_attr("age") == 2
+ ), "Original attribute not updated"
+ assert (
+ self.root_overriding["bb"].get_attr("hello") == "world"
+ ), "Original attribute not present"
+ assert (
+ self.root_overriding["bb"].get_attr("gender") == "b"
+ ), "Merge attribute not present"
+ assert (
+ len(self.root_overriding["bb"].children) == 2
+ ), "Original node children not combined with copied node"
+ assert not len(
+ list(search.find_path(self.root_overriding, "a/aa").children)
+ ), "Original node not deleted"
+ assert (
+ self.root_overriding["bb"]["cc2"].get_attr("age") == 2
+ ), "Original attribute of leaf not updated"
+ assert (
+ self.root_overriding["bb"]["cc2"].get_attr("hello") == "world"
+ ), "Original attribute of leaf not present"
+ assert (
+ self.root_overriding["bb"]["cc2"].get_attr("gender") == "c"
+ ), "Merge attribute of leaf not present"
+ assert (
+ self.root_overriding["bb"]["dd"].get_attr("age") == 2
+ ), "New leaf not copied over"
+
# merge_children, merge_leaves
def test_shift_nodes_merge_children_and_leaf_error(self):
a = node.Node("a", age=90)
@@ -1748,7 +2144,7 @@ def test_copy_nodes_skippable_error(self):
assert_tree_structure_node_root(self.root_other)
# overriding
- def test_copy_nodes_from_tree_to_tree_delete_and_overriding_error(self):
+ def test_copy_nodes_from_tree_to_tree_overriding_error(self):
new_aa = node.Node("aa", parent=self.root)
new_d = node.Node("d")
new_d.parent = new_aa
@@ -1766,7 +2162,7 @@ def test_copy_nodes_from_tree_to_tree_delete_and_overriding_error(self):
to_path=to_path
)
- def test_copy_nodes_from_tree_to_tree_delete_and_overriding(self):
+ def test_copy_nodes_from_tree_to_tree_overriding(self):
new_aa = node.Node("aa", parent=self.root)
new_d = node.Node("d", age=1)
new_d.parent = new_aa
@@ -1792,7 +2188,7 @@ def test_copy_nodes_from_tree_to_tree_delete_and_overriding(self):
assert_tree_structure_basenode_root_attr(self.root_other, d=("d", 1))
assert_tree_structure_node_root(self.root_other)
- def test_copy_nodes_from_tree_to_tree_overriding(self):
+ def test_copy_nodes_from_tree_to_tree_overriding_manual_check(self):
from_paths = ["d", "e", "g", "h", "f"]
to_paths = ["a/b/d", "a/b/e", "a/b/e/g", "a/b/e/h", "a/c/f"]
modify.shift_nodes(self.root, from_paths, to_paths)
@@ -1810,6 +2206,67 @@ def test_copy_nodes_from_tree_to_tree_overriding(self):
assert_tree_structure_basenode_root_attr(self.root_other_full_wrong)
assert_tree_structure_node_root(self.root_other_full_wrong)
+ # merge_attribute
+ def test_copy_nodes_from_tree_to_tree_merge_attribute(self):
+ from_paths = ["d", "e", "g", "h", "f"]
+ to_paths = ["a/b/d", "a/b/e", "a/b/e/g", "a/b/e/h", "a/c/f"]
+ modify.shift_nodes(self.root, from_paths, to_paths)
+
+ from_paths = ["a/b", "a/c"]
+ to_paths = ["a/b", "a/c"]
+
+ # Set attribute for node
+ self.root_other_full_wrong["b"].set_attrs({"hello": "world"})
+ self.root["b"].set_attrs({"gender": "b"})
+
+ modify.copy_nodes_from_tree_to_tree(
+ from_tree=self.root,
+ to_tree=self.root_other_full_wrong,
+ from_paths=from_paths,
+ to_paths=to_paths,
+ merge_attribute=True,
+ )
+ assert (
+ self.root_other_full_wrong["b"].get_attr("hello") == "world"
+ ), "Original attribute not present"
+ assert (
+ self.root_other_full_wrong["b"].get_attr("gender") == "b"
+ ), "Merge attribute not present"
+
+ def test_copy_nodes_from_tree_to_tree_merge_attribute_children(self):
+ from_paths = ["d", "e", "g", "h", "f"]
+ to_paths = ["a/b/d", "a/b/e", "a/b/e/g", "a/b/e/h", "a/c/f"]
+ modify.shift_nodes(self.root, from_paths, to_paths)
+
+ from_paths = ["a/b", "a/c"]
+ to_paths = ["a/b", "a/c"]
+
+ # Set attribute for node
+ self.root_other_full_wrong["b"].set_attrs({"hello": "world"})
+ self.root_other_full_wrong["c"]["f"].set_attrs({"hello": "world"})
+ self.root["b"].set_attrs({"gender": "b"})
+ self.root["c"]["f"].set_attrs({"gender": "b"})
+
+ modify.copy_nodes_from_tree_to_tree(
+ from_tree=self.root,
+ to_tree=self.root_other_full_wrong,
+ from_paths=from_paths,
+ to_paths=to_paths,
+ merge_attribute=True,
+ )
+ assert (
+ self.root_other_full_wrong["b"].get_attr("hello") == "world"
+ ), "Original attribute not present"
+ assert (
+ self.root_other_full_wrong["b"].get_attr("gender") == "b"
+ ), "Merge attribute not present"
+ assert (
+ self.root_other_full_wrong["c"]["f"].get_attr("hello") == "world"
+ ), "Original attribute of children not present"
+ assert (
+ self.root_other_full_wrong["c"]["f"].get_attr("gender") == "b"
+ ), "Merge attribute of children not present"
+
# merge_children
def test_copy_nodes_from_tree_to_tree_merge_children(self):
from_paths = ["e", "g", "h"]
@@ -1971,10 +2428,40 @@ def test_copy_nodes_from_tree_to_tree_merge_children_overriding(self):
assert_tree_structure_basenode_root_attr(self.root_other_full_wrong)
assert_tree_structure_node_root(self.root_other_full_wrong)
+ # merge_children, merge_attribute
+ def test_copy_nodes_from_tree_to_tree_merge_children_merge_attribute(self):
+ from_paths = ["d", "e", "g", "h", "f"]
+ to_paths = ["a/b/d", "a/b/e", "a/b/e/g", "a/b/e/h", "a/c/f"]
+ modify.shift_nodes(self.root, from_paths, to_paths)
+
+ from_paths = ["a/b", "a/c"]
+ to_paths = ["a/b", "a/c"]
+
+ # Set attribute for node
+ self.root_other_full_wrong["b"].set_attrs({"hello": "world"})
+ self.root["b"].set_attrs({"gender": "b"})
+
+ modify.copy_nodes_from_tree_to_tree(
+ from_tree=self.root,
+ to_tree=self.root_other_full_wrong,
+ from_paths=from_paths,
+ to_paths=to_paths,
+ merge_attribute=True,
+ merge_children=True,
+ )
+ assert_tree_structure_basenode_root(self.root_other_full_wrong)
+ assert_tree_structure_node_root(self.root_other_full_wrong)
+ assert (
+ self.root_other_full_wrong["b"].get_attr("hello") == "world"
+ ), "Original attribute not present"
+ assert (
+ self.root_other_full_wrong["b"].get_attr("gender") == "b"
+ ), "Merge attribute not present"
+
# merge_leaves, overriding
def test_copy_nodes_from_tree_to_tree_merge_leaves_overriding(self):
from_paths = ["d", "e", "g", "h", "f"]
- to_paths = ["a/bb/d", "a/bb/e", "a/cc/g", "a/cc/h", "a/c/f"]
+ to_paths = ["a/bb/bb2/d", "a/bb/e", "a/cc/g", "a/cc/cc2/h", "a/c/f"]
modify.shift_nodes(self.root, from_paths, to_paths)
from_paths = ["a/bb", "a/cc", "a/c"]
@@ -1991,6 +2478,46 @@ def test_copy_nodes_from_tree_to_tree_merge_leaves_overriding(self):
assert_tree_structure_basenode_root_attr(self.root_other_full_wrong, c=("c", 1))
assert_tree_structure_node_root(self.root_other_full_wrong)
+ # merge_leaves, merge_attribute
+ def test_copy_nodes_from_tree_to_tree_merge_leaves_merge_attribute(self):
+ from_paths = ["d", "e", "g", "h", "f"]
+ to_paths = ["a/b/b2/d", "a/b/e", "a/cc/e/g", "a/cc/e/cc2/h", "a/c/f"]
+ modify.shift_nodes(self.root, from_paths, to_paths)
+
+ from_paths = ["a/b", "a/cc/e", "a/c"]
+ to_paths = ["a/b", "a/b/e", "a/c"]
+
+ # Set attribute for node
+ self.root_other_full_wrong["b"].set_attrs({"hello": "world"})
+ self.root_other_full_wrong["b"].extend([node.Node("e")])
+ self.root["b"].set_attrs({"gender": "b"})
+ self.root_other_full_wrong["c"]["f"].set_attrs({"hello": "world"})
+ self.root["c"]["f"].set_attrs({"gender": "b"})
+
+ modify.copy_nodes_from_tree_to_tree(
+ from_tree=self.root,
+ to_tree=self.root_other_full_wrong,
+ from_paths=from_paths,
+ to_paths=to_paths,
+ merge_leaves=True,
+ merge_attribute=True,
+ )
+ assert_tree_structure_basenode_root(self.root_other_full_wrong)
+ assert_tree_structure_basenode_root_attr(self.root_other_full_wrong)
+ assert_tree_structure_node_root(self.root_other_full_wrong)
+ assert (
+ self.root_other_full_wrong["b"].get_attr("hello") == "world"
+ ), "Original attribute not present"
+ assert (
+ self.root_other_full_wrong["b"].get_attr("gender") == "b"
+ ), "Merge attribute not present"
+ assert (
+ self.root_other_full_wrong["c"]["f"].get_attr("hello") == "world"
+ ), "Original attribute of leaf not present"
+ assert (
+ self.root_other_full_wrong["c"]["f"].get_attr("gender") == "b"
+ ), "Merge attribute of leaf not present"
+
# merge_children, merge_leaves
def test_copy_nodes_from_tree_to_tree_merge_children_and_leaf_error(self):
from_paths = ["a"]