857 lines
31 KiB
Python
857 lines
31 KiB
Python
import logging
|
|
from typing import List, Optional
|
|
|
|
from bigtree.node.node import Node
|
|
from bigtree.tree.search import find_path
|
|
from bigtree.utils.exceptions import NotFoundError, TreeError
|
|
|
|
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
|
|
|
|
__all__ = [
|
|
"shift_nodes",
|
|
"copy_nodes",
|
|
"copy_nodes_from_tree_to_tree",
|
|
"copy_or_shift_logic",
|
|
]
|
|
|
|
|
|
def shift_nodes(
|
|
tree: Node,
|
|
from_paths: List[str],
|
|
to_paths: List[str],
|
|
sep: str = "/",
|
|
skippable: bool = False,
|
|
overriding: bool = False,
|
|
merge_children: bool = False,
|
|
merge_leaves: bool = False,
|
|
delete_children: bool = False,
|
|
):
|
|
"""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).
|
|
- 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 shift node only and delete children, defaults to False (nodes are shifted together with children).
|
|
|
|
For paths in `from_paths` and `to_paths`,
|
|
- Path name can be with or without leading tree path separator symbol.
|
|
- Path name can be partial path (trailing part of path) or node name.
|
|
- Path name must be unique to one node.
|
|
|
|
For paths in `to_paths`,
|
|
- Can set to empty string or None to delete the path in `from_paths`, note that ``copy`` must be set to False.
|
|
|
|
If ``merge_children=True``,
|
|
- If `to_path` is not present, it shifts children of `from_path`.
|
|
- If `to_path` is present, and ``overriding=False``, original and new children are merged.
|
|
- If `to_path` is present and ``overriding=True``, it behaves like overriding and only new children are retained.
|
|
|
|
If ``merge_leaves=True``,
|
|
- If `to_path` is not present, it shifts leaves of `from_path`.
|
|
- 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,
|
|
original node in `from_path` is retained.
|
|
|
|
>>> from bigtree import Node, shift_nodes, print_tree
|
|
>>> root = Node("a")
|
|
>>> b = Node("b", parent=root)
|
|
>>> c = Node("c", parent=root)
|
|
>>> d = Node("d", parent=root)
|
|
>>> print_tree(root)
|
|
a
|
|
├── b
|
|
├── c
|
|
└── d
|
|
|
|
>>> shift_nodes(root, ["a/c", "a/d"], ["a/b/c", "a/dummy/d"])
|
|
>>> print_tree(root)
|
|
a
|
|
├── b
|
|
│ └── c
|
|
└── dummy
|
|
└── d
|
|
|
|
To delete node,
|
|
|
|
>>> root = Node("a")
|
|
>>> b = Node("b", parent=root)
|
|
>>> c = Node("c", parent=root)
|
|
>>> print_tree(root)
|
|
a
|
|
├── b
|
|
└── c
|
|
|
|
>>> shift_nodes(root, ["a/b"], [None])
|
|
>>> print_tree(root)
|
|
a
|
|
└── c
|
|
|
|
In overriding case,
|
|
|
|
>>> root = Node("a")
|
|
>>> b = Node("b", parent=root)
|
|
>>> c = Node("c", parent=root)
|
|
>>> d = Node("d", parent=c)
|
|
>>> c2 = Node("c", parent=b)
|
|
>>> e = Node("e", parent=c2)
|
|
>>> print_tree(root)
|
|
a
|
|
├── b
|
|
│ └── c
|
|
│ └── e
|
|
└── c
|
|
└── d
|
|
|
|
>>> shift_nodes(root, ["a/b/c"], ["a/c"], overriding=True)
|
|
>>> print_tree(root)
|
|
a
|
|
├── b
|
|
└── c
|
|
└── e
|
|
|
|
In ``merge_children`` 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.
|
|
|
|
>>> root = Node("a")
|
|
>>> b = Node("b", parent=root)
|
|
>>> c = Node("c", parent=root)
|
|
>>> d = Node("d", parent=c)
|
|
>>> c2 = Node("c", parent=b)
|
|
>>> e = Node("e", parent=c2)
|
|
>>> z = Node("z", parent=b)
|
|
>>> y = Node("y", parent=z)
|
|
>>> f = Node("f", parent=root)
|
|
>>> g = Node("g", parent=f)
|
|
>>> h = Node("h", parent=g)
|
|
>>> print_tree(root)
|
|
a
|
|
├── b
|
|
│ ├── c
|
|
│ │ └── e
|
|
│ └── z
|
|
│ └── y
|
|
├── c
|
|
│ └── d
|
|
└── f
|
|
└── g
|
|
└── h
|
|
|
|
>>> shift_nodes(root, ["a/b/c", "z", "a/f"], ["a/c", "a/z", "a/f"], merge_children=True)
|
|
>>> print_tree(root)
|
|
a
|
|
├── b
|
|
├── c
|
|
│ ├── d
|
|
│ └── e
|
|
├── y
|
|
└── g
|
|
└── h
|
|
|
|
In ``merge_leaves`` 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.
|
|
|
|
>>> root = Node("a")
|
|
>>> b = Node("b", parent=root)
|
|
>>> c = Node("c", parent=root)
|
|
>>> d = Node("d", parent=c)
|
|
>>> c2 = Node("c", parent=b)
|
|
>>> e = Node("e", parent=c2)
|
|
>>> z = Node("z", parent=b)
|
|
>>> y = Node("y", parent=z)
|
|
>>> f = Node("f", parent=root)
|
|
>>> g = Node("g", parent=f)
|
|
>>> h = Node("h", parent=g)
|
|
>>> print_tree(root)
|
|
a
|
|
├── b
|
|
│ ├── c
|
|
│ │ └── e
|
|
│ └── z
|
|
│ └── y
|
|
├── c
|
|
│ └── d
|
|
└── f
|
|
└── g
|
|
└── h
|
|
|
|
>>> shift_nodes(root, ["a/b/c", "z", "a/f"], ["a/c", "a/z", "a/f"], merge_leaves=True)
|
|
>>> print_tree(root)
|
|
a
|
|
├── b
|
|
│ ├── c
|
|
│ └── z
|
|
├── c
|
|
│ ├── d
|
|
│ └── e
|
|
├── f
|
|
│ └── g
|
|
├── y
|
|
└── h
|
|
|
|
In ``delete_children`` case, only the node is shifted without its accompanying children/descendants.
|
|
|
|
>>> root = Node("a")
|
|
>>> b = Node("b", parent=root)
|
|
>>> c = Node("c", parent=root)
|
|
>>> d = Node("d", parent=c)
|
|
>>> c2 = Node("c", parent=b)
|
|
>>> e = Node("e", parent=c2)
|
|
>>> z = Node("z", parent=b)
|
|
>>> y = Node("y", parent=z)
|
|
>>> print_tree(root)
|
|
a
|
|
├── b
|
|
│ ├── c
|
|
│ │ └── e
|
|
│ └── z
|
|
│ └── y
|
|
└── c
|
|
└── d
|
|
|
|
>>> shift_nodes(root, ["a/b/z"], ["a/z"], delete_children=True)
|
|
>>> print_tree(root)
|
|
a
|
|
├── b
|
|
│ └── c
|
|
│ └── e
|
|
├── c
|
|
│ └── d
|
|
└── z
|
|
|
|
Args:
|
|
tree (Node): tree to modify
|
|
from_paths (list): original paths to shift nodes from
|
|
to_paths (list): 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
|
|
delete_children (bool): indicator to shift node only without children, defaults to False
|
|
"""
|
|
return copy_or_shift_logic(
|
|
tree=tree,
|
|
from_paths=from_paths,
|
|
to_paths=to_paths,
|
|
sep=sep,
|
|
copy=False,
|
|
skippable=skippable,
|
|
overriding=overriding,
|
|
merge_children=merge_children,
|
|
merge_leaves=merge_leaves,
|
|
delete_children=delete_children,
|
|
to_tree=None,
|
|
) # pragma: no cover
|
|
|
|
|
|
def copy_nodes(
|
|
tree: Node,
|
|
from_paths: List[str],
|
|
to_paths: List[str],
|
|
sep: str = "/",
|
|
skippable: bool = False,
|
|
overriding: bool = False,
|
|
merge_children: bool = False,
|
|
merge_leaves: bool = False,
|
|
delete_children: bool = False,
|
|
):
|
|
"""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).
|
|
- 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 copy node only and delete children, defaults to False (nodes are copied together with children).
|
|
|
|
For paths in `from_paths` and `to_paths`,
|
|
- Path name can be with or without leading tree path separator symbol.
|
|
- Path name can be partial path (trailing part of path) or node name.
|
|
- Path name must be unique to one node.
|
|
|
|
If ``merge_children=True``,
|
|
- If `to_path` is not present, it copies children of `from_path`.
|
|
- If `to_path` is present, and ``overriding=False``, original and new children are merged.
|
|
- If `to_path` is present and ``overriding=True``, it behaves like overriding and only new children are retained.
|
|
|
|
If ``merge_leaves=True``,
|
|
- If `to_path` is not present, it copies leaves of `from_path`.
|
|
- 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.
|
|
|
|
>>> from bigtree import Node, copy_nodes, print_tree
|
|
>>> root = Node("a")
|
|
>>> b = Node("b", parent=root)
|
|
>>> c = Node("c", parent=root)
|
|
>>> d = Node("d", parent=root)
|
|
>>> print_tree(root)
|
|
a
|
|
├── b
|
|
├── c
|
|
└── d
|
|
|
|
>>> copy_nodes(root, ["a/c", "a/d"], ["a/b/c", "a/dummy/d"])
|
|
>>> print_tree(root)
|
|
a
|
|
├── b
|
|
│ └── c
|
|
├── c
|
|
├── d
|
|
└── dummy
|
|
└── d
|
|
|
|
In overriding case,
|
|
|
|
>>> root = Node("a")
|
|
>>> b = Node("b", parent=root)
|
|
>>> c = Node("c", parent=root)
|
|
>>> d = Node("d", parent=c)
|
|
>>> c2 = Node("c", parent=b)
|
|
>>> e = Node("e", parent=c2)
|
|
>>> print_tree(root)
|
|
a
|
|
├── b
|
|
│ └── c
|
|
│ └── e
|
|
└── c
|
|
└── d
|
|
|
|
>>> copy_nodes(root, ["a/b/c"], ["a/c"], overriding=True)
|
|
>>> print_tree(root)
|
|
a
|
|
├── b
|
|
│ └── c
|
|
│ └── e
|
|
└── c
|
|
└── e
|
|
|
|
In ``merge_children`` 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.
|
|
|
|
>>> root = Node("a")
|
|
>>> b = Node("b", parent=root)
|
|
>>> c = Node("c", parent=root)
|
|
>>> d = Node("d", parent=c)
|
|
>>> c2 = Node("c", parent=b)
|
|
>>> e = Node("e", parent=c2)
|
|
>>> z = Node("z", parent=b)
|
|
>>> y = Node("y", parent=z)
|
|
>>> f = Node("f", parent=root)
|
|
>>> g = Node("g", parent=f)
|
|
>>> h = Node("h", parent=g)
|
|
>>> print_tree(root)
|
|
a
|
|
├── b
|
|
│ ├── c
|
|
│ │ └── e
|
|
│ └── z
|
|
│ └── y
|
|
├── c
|
|
│ └── d
|
|
└── f
|
|
└── g
|
|
└── h
|
|
|
|
>>> copy_nodes(root, ["a/b/c", "z", "a/f"], ["a/c", "a/z", "a/f"], merge_children=True)
|
|
>>> print_tree(root)
|
|
a
|
|
├── b
|
|
│ ├── c
|
|
│ │ └── e
|
|
│ └── z
|
|
│ └── y
|
|
├── c
|
|
│ ├── d
|
|
│ └── e
|
|
├── y
|
|
└── g
|
|
└── h
|
|
|
|
In ``merge_leaves`` 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.
|
|
|
|
>>> root = Node("a")
|
|
>>> b = Node("b", parent=root)
|
|
>>> c = Node("c", parent=root)
|
|
>>> d = Node("d", parent=c)
|
|
>>> c2 = Node("c", parent=b)
|
|
>>> e = Node("e", parent=c2)
|
|
>>> z = Node("z", parent=b)
|
|
>>> y = Node("y", parent=z)
|
|
>>> f = Node("f", parent=root)
|
|
>>> g = Node("g", parent=f)
|
|
>>> h = Node("h", parent=g)
|
|
>>> print_tree(root)
|
|
a
|
|
├── b
|
|
│ ├── c
|
|
│ │ └── e
|
|
│ └── z
|
|
│ └── y
|
|
├── c
|
|
│ └── d
|
|
└── f
|
|
└── g
|
|
└── h
|
|
|
|
>>> copy_nodes(root, ["a/b/c", "z", "a/f"], ["a/c", "a/z", "a/f"], merge_leaves=True)
|
|
>>> print_tree(root)
|
|
a
|
|
├── b
|
|
│ ├── c
|
|
│ │ └── e
|
|
│ └── z
|
|
│ └── y
|
|
├── c
|
|
│ ├── d
|
|
│ └── e
|
|
├── f
|
|
│ └── g
|
|
│ └── h
|
|
├── y
|
|
└── h
|
|
|
|
In ``delete_children`` case, only the node is copied without its accompanying children/descendants.
|
|
|
|
>>> root = Node("a")
|
|
>>> b = Node("b", parent=root)
|
|
>>> c = Node("c", parent=root)
|
|
>>> d = Node("d", parent=c)
|
|
>>> c2 = Node("c", parent=b)
|
|
>>> e = Node("e", parent=c2)
|
|
>>> z = Node("z", parent=b)
|
|
>>> y = Node("y", parent=z)
|
|
>>> print_tree(root)
|
|
a
|
|
├── b
|
|
│ ├── c
|
|
│ │ └── e
|
|
│ └── z
|
|
│ └── y
|
|
└── c
|
|
└── d
|
|
|
|
>>> copy_nodes(root, ["a/b/z"], ["a/z"], delete_children=True)
|
|
>>> print_tree(root)
|
|
a
|
|
├── b
|
|
│ ├── c
|
|
│ │ └── e
|
|
│ └── z
|
|
│ └── y
|
|
├── c
|
|
│ └── d
|
|
└── z
|
|
|
|
Args:
|
|
tree (Node): tree to modify
|
|
from_paths (list): original paths to shift nodes from
|
|
to_paths (list): 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
|
|
delete_children (bool): indicator to copy node only without children, defaults to False
|
|
"""
|
|
return copy_or_shift_logic(
|
|
tree=tree,
|
|
from_paths=from_paths,
|
|
to_paths=to_paths,
|
|
sep=sep,
|
|
copy=True,
|
|
skippable=skippable,
|
|
overriding=overriding,
|
|
merge_children=merge_children,
|
|
merge_leaves=merge_leaves,
|
|
delete_children=delete_children,
|
|
to_tree=None,
|
|
) # pragma: no cover
|
|
|
|
|
|
def copy_nodes_from_tree_to_tree(
|
|
from_tree: Node,
|
|
to_tree: Node,
|
|
from_paths: List[str],
|
|
to_paths: List[str],
|
|
sep: str = "/",
|
|
skippable: bool = False,
|
|
overriding: bool = False,
|
|
merge_children: bool = False,
|
|
merge_leaves: bool = False,
|
|
delete_children: bool = False,
|
|
):
|
|
"""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).
|
|
- 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 copy node only and delete children, defaults to False (nodes are copied together with children).
|
|
|
|
For paths in `from_paths` and `to_paths`,
|
|
- Path name can be with or without leading tree path separator symbol.
|
|
- Path name can be partial path (trailing part of path) or node name.
|
|
- Path name must be unique to one node.
|
|
|
|
If ``merge_children=True``,
|
|
- If `to_path` is not present, it copies children of `from_path`
|
|
- If `to_path` is present, and ``overriding=False``, original and new children are merged
|
|
- If `to_path` is present and ``overriding=True``, it behaves like overriding and only new leaves are retained.
|
|
|
|
If ``merge_leaves=True``,
|
|
- If `to_path` is not present, it copies leaves of `from_path`.
|
|
- 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.
|
|
|
|
>>> from bigtree import Node, copy_nodes_from_tree_to_tree, print_tree
|
|
>>> root = Node("a")
|
|
>>> b = Node("b", parent=root)
|
|
>>> c = Node("c", parent=root)
|
|
>>> d = Node("d", parent=c)
|
|
>>> e = Node("e", parent=root)
|
|
>>> f = Node("f", parent=e)
|
|
>>> g = Node("g", parent=f)
|
|
>>> print_tree(root)
|
|
a
|
|
├── b
|
|
├── c
|
|
│ └── d
|
|
└── e
|
|
└── f
|
|
└── g
|
|
|
|
>>> root_other = Node("aa")
|
|
>>> copy_nodes_from_tree_to_tree(root, root_other, ["a/b", "a/c", "a/e"], ["aa/b", "aa/b/c", "aa/dummy/e"])
|
|
>>> print_tree(root_other)
|
|
aa
|
|
├── b
|
|
│ └── c
|
|
│ └── d
|
|
└── dummy
|
|
└── e
|
|
└── f
|
|
└── g
|
|
|
|
In overriding case,
|
|
|
|
>>> root_other = Node("aa")
|
|
>>> c = Node("c", parent=root_other)
|
|
>>> e = Node("e", parent=c)
|
|
>>> print_tree(root_other)
|
|
aa
|
|
└── c
|
|
└── e
|
|
|
|
>>> copy_nodes_from_tree_to_tree(root, root_other, ["a/b", "a/c"], ["aa/b", "aa/c"], overriding=True)
|
|
>>> print_tree(root_other)
|
|
aa
|
|
├── b
|
|
└── c
|
|
└── d
|
|
|
|
In ``merge_children`` case, child nodes are copied instead of the parent node.
|
|
- If the path already exists, child nodes are merged with existing children.
|
|
|
|
>>> root_other = Node("aa")
|
|
>>> c = Node("c", parent=root_other)
|
|
>>> e = Node("e", parent=c)
|
|
>>> print_tree(root_other)
|
|
aa
|
|
└── c
|
|
└── e
|
|
|
|
>>> copy_nodes_from_tree_to_tree(root, root_other, ["a/c", "e"], ["a/c", "a/e"], merge_children=True)
|
|
>>> print_tree(root_other)
|
|
aa
|
|
├── c
|
|
│ ├── e
|
|
│ └── d
|
|
└── f
|
|
└── g
|
|
|
|
In ``merge_leaves`` case, leaf nodes are copied instead of the parent node.
|
|
- If the path already exists, leaf nodes are merged with existing children.
|
|
|
|
>>> root_other = Node("aa")
|
|
>>> c = Node("c", parent=root_other)
|
|
>>> e = Node("e", parent=c)
|
|
>>> print_tree(root_other)
|
|
aa
|
|
└── c
|
|
└── e
|
|
|
|
>>> copy_nodes_from_tree_to_tree(root, root_other, ["a/c", "e"], ["a/c", "a/e"], merge_leaves=True)
|
|
>>> print_tree(root_other)
|
|
aa
|
|
├── c
|
|
│ ├── e
|
|
│ └── d
|
|
└── g
|
|
|
|
In ``delete_children`` case, only the node is copied without its accompanying children/descendants.
|
|
|
|
>>> root_other = Node("aa")
|
|
>>> print_tree(root_other)
|
|
aa
|
|
|
|
>>> copy_nodes_from_tree_to_tree(root, root_other, ["a/c", "e"], ["a/c", "a/e"], delete_children=True)
|
|
>>> print_tree(root_other)
|
|
aa
|
|
├── c
|
|
└── e
|
|
|
|
Args:
|
|
from_tree (Node): tree to copy nodes from
|
|
to_tree (Node): tree to copy nodes to
|
|
from_paths (list): original paths to shift nodes from
|
|
to_paths (list): 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
|
|
delete_children (bool): indicator to copy node only without children, defaults to False
|
|
"""
|
|
return copy_or_shift_logic(
|
|
tree=from_tree,
|
|
from_paths=from_paths,
|
|
to_paths=to_paths,
|
|
sep=sep,
|
|
copy=True,
|
|
skippable=skippable,
|
|
overriding=overriding,
|
|
merge_children=merge_children,
|
|
merge_leaves=merge_leaves,
|
|
delete_children=delete_children,
|
|
to_tree=to_tree,
|
|
) # pragma: no cover
|
|
|
|
|
|
def copy_or_shift_logic(
|
|
tree: Node,
|
|
from_paths: List[str],
|
|
to_paths: List[str],
|
|
sep: str = "/",
|
|
copy: bool = False,
|
|
skippable: bool = False,
|
|
overriding: bool = False,
|
|
merge_children: bool = False,
|
|
merge_leaves: bool = False,
|
|
delete_children: bool = False,
|
|
to_tree: Optional[Node] = None,
|
|
):
|
|
"""Shift or copy nodes from `from_paths` to `to_paths` *in-place*.
|
|
|
|
- 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 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 shift/copy 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)
|
|
|
|
For paths in `from_paths` and `to_paths`,
|
|
- Path name can be with or without leading tree path separator symbol.
|
|
- Path name can be partial path (trailing part of path) or node name.
|
|
- Path name must be unique to one node.
|
|
|
|
For paths in `to_paths`,
|
|
- Can set to empty string or None to delete the path in `from_paths`, note that ``copy`` must be set to False.
|
|
|
|
If ``merge_children=True``,
|
|
- If `to_path` is not present, it shifts/copies children of `from_path`.
|
|
- If `to_path` is present, and ``overriding=False``, original and new children are merged.
|
|
- If `to_path` is present and ``overriding=True``, it behaves like overriding and only new children are retained.
|
|
|
|
If ``merge_leaves=True``,
|
|
- If `to_path` is not present, it shifts/copies leaves of `from_path`.
|
|
- 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,
|
|
original non-leaf nodes in `from_path` are retained.
|
|
|
|
Args:
|
|
tree (Node): tree to modify
|
|
from_paths (list): original paths to shift nodes from
|
|
to_paths (list): new paths to shift nodes to
|
|
sep (str): path separator for input paths, applies to `from_path` and `to_path`
|
|
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
|
|
delete_children (bool): indicator to shift/copy node only without children, defaults to False
|
|
to_tree (Node): tree to copy to, defaults to None
|
|
"""
|
|
if merge_children and merge_leaves:
|
|
raise ValueError(
|
|
"Invalid shifting, can only specify one type of merging, check `merge_children` and `merge_leaves`"
|
|
)
|
|
if not (isinstance(from_paths, list) and isinstance(to_paths, list)):
|
|
raise ValueError(
|
|
"Invalid type, `from_paths` and `to_paths` should be list type"
|
|
)
|
|
if len(from_paths) != len(to_paths):
|
|
raise ValueError(
|
|
f"Paths are different length, input `from_paths` have {len(from_paths)} entries, "
|
|
f"while output `to_paths` have {len(to_paths)} entries"
|
|
)
|
|
for from_path, to_path in zip(from_paths, to_paths):
|
|
if to_path:
|
|
if from_path.split(sep)[-1] != to_path.split(sep)[-1]:
|
|
raise ValueError(
|
|
f"Unable to assign from_path {from_path} to to_path {to_path}\n"
|
|
f"Verify that `sep` is defined correctly for path\n"
|
|
f"Alternatively, check that `from_path` and `to_path` is reassigning the same node"
|
|
)
|
|
|
|
transfer_indicator = False
|
|
node_type = tree.__class__
|
|
tree_sep = tree.sep
|
|
if to_tree:
|
|
transfer_indicator = True
|
|
node_type = to_tree.__class__
|
|
tree_sep = to_tree.sep
|
|
for from_path, to_path in zip(from_paths, to_paths):
|
|
from_path = from_path.replace(sep, tree.sep)
|
|
from_node = find_path(tree, from_path)
|
|
|
|
# From node not found
|
|
if not from_node:
|
|
if not skippable:
|
|
raise NotFoundError(
|
|
f"Unable to find from_path {from_path}\n"
|
|
f"Set `skippable` to True to skip shifting for nodes not found"
|
|
)
|
|
else:
|
|
logging.info(f"Unable to find from_path {from_path}")
|
|
|
|
# From node found
|
|
else:
|
|
# Node to be deleted
|
|
if not to_path:
|
|
to_node = None
|
|
# Node to be copied/shifted
|
|
else:
|
|
to_path = to_path.replace(sep, tree_sep)
|
|
if transfer_indicator:
|
|
to_node = find_path(to_tree, to_path)
|
|
else:
|
|
to_node = find_path(tree, to_path)
|
|
|
|
# To node found
|
|
if to_node:
|
|
if from_node == to_node:
|
|
if merge_children:
|
|
parent = to_node.parent
|
|
to_node.parent = None
|
|
to_node = parent
|
|
elif merge_leaves:
|
|
to_node = to_node.parent
|
|
else:
|
|
raise TreeError(
|
|
f"Attempting to shift the same node {from_node.node_name} back to the same position\n"
|
|
f"Check from path {from_path} and to path {to_path}\n"
|
|
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:
|
|
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:
|
|
logging.info(
|
|
f"Path {to_path} already exists and leaves are merged"
|
|
)
|
|
else:
|
|
logging.info(
|
|
f"Path {to_path} already exists and its leaves be overridden by the merge"
|
|
)
|
|
del to_node.children
|
|
else:
|
|
if not overriding:
|
|
raise TreeError(
|
|
f"Path {to_path} already exists and unable to override\n"
|
|
f"Set `overriding` to True to perform overrides\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
|
|
|
|
# To node not found
|
|
else:
|
|
# Find parent node
|
|
to_path_list = to_path.split(tree_sep)
|
|
idx = 1
|
|
to_path_parent = tree_sep.join(to_path_list[:-idx])
|
|
if transfer_indicator:
|
|
to_node = find_path(to_tree, to_path_parent)
|
|
else:
|
|
to_node = find_path(tree, to_path_parent)
|
|
|
|
# Create intermediate parent node, if applicable
|
|
while (not to_node) & (idx + 1 < len(to_path_list)):
|
|
idx += 1
|
|
to_path_parent = sep.join(to_path_list[:-idx])
|
|
if transfer_indicator:
|
|
to_node = find_path(to_tree, to_path_parent)
|
|
else:
|
|
to_node = find_path(tree, to_path_parent)
|
|
if not to_node:
|
|
raise NotFoundError(
|
|
f"Unable to find to_path {to_path}\n"
|
|
f"Please specify valid path to shift node to"
|
|
)
|
|
for depth in range(len(to_path_list) - idx, len(to_path_list) - 1):
|
|
intermediate_child_node = node_type(to_path_list[depth])
|
|
intermediate_child_node.parent = to_node
|
|
to_node = intermediate_child_node
|
|
|
|
# Reassign from_node to new parent
|
|
if copy:
|
|
logging.debug(f"Copying {from_node.node_name}")
|
|
from_node = from_node.copy()
|
|
if merge_children:
|
|
logging.debug(
|
|
f"Reassigning children from {from_node.node_name} to {to_node.node_name}"
|
|
)
|
|
for children in from_node.children:
|
|
if delete_children:
|
|
del children.children
|
|
children.parent = to_node
|
|
from_node.parent = None
|
|
elif merge_leaves:
|
|
logging.debug(
|
|
f"Reassigning leaf nodes from {from_node.node_name} to {to_node.node_name}"
|
|
)
|
|
for children in from_node.leaves:
|
|
children.parent = to_node
|
|
else:
|
|
if delete_children:
|
|
del from_node.children
|
|
from_node.parent = to_node
|