1357 lines
50 KiB
Python
1357 lines
50 KiB
Python
import logging
|
|
from typing import List, Optional
|
|
|
|
from bigtree.node.node import Node
|
|
from bigtree.tree.construct import add_path_to_tree
|
|
from bigtree.tree.search import find_full_path, find_path
|
|
from bigtree.utils.exceptions import NotFoundError, TreeError
|
|
|
|
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
|
|
__all__ = [
|
|
"shift_nodes",
|
|
"copy_nodes",
|
|
"shift_and_replace_nodes",
|
|
"copy_nodes_from_tree_to_tree",
|
|
"copy_and_replace_nodes_from_tree_to_tree",
|
|
"copy_or_shift_logic",
|
|
"replace_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,
|
|
with_full_path: bool = False,
|
|
) -> 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).
|
|
- 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.
|
|
|
|
For paths in `from_paths`,
|
|
|
|
- Path name can be partial path (trailing part of path) or node name.
|
|
- If ``with_full_path=True``, path name must be full path.
|
|
- Path name must be unique to one node.
|
|
|
|
For paths in `to_paths`,
|
|
|
|
- Path name must be full path.
|
|
- 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 non-leaf nodes in `from_path` are retained.
|
|
|
|
Examples:
|
|
>>> from bigtree import list_to_tree, str_to_tree, shift_nodes
|
|
>>> root = list_to_tree(["Downloads/photo1.jpg", "Downloads/file1.doc"])
|
|
>>> root.show()
|
|
Downloads
|
|
├── photo1.jpg
|
|
└── file1.doc
|
|
|
|
>>> shift_nodes(
|
|
... tree=root,
|
|
... from_paths=["Downloads/photo1.jpg", "Downloads/file1.doc"],
|
|
... to_paths=["Downloads/Pictures/photo1.jpg", "Downloads/Files/file1.doc"],
|
|
... )
|
|
>>> root.show()
|
|
Downloads
|
|
├── Pictures
|
|
│ └── photo1.jpg
|
|
└── Files
|
|
└── file1.doc
|
|
|
|
To delete node,
|
|
|
|
>>> root = list_to_tree(["Downloads/photo1.jpg", "Downloads/file1.doc"])
|
|
>>> root.show()
|
|
Downloads
|
|
├── photo1.jpg
|
|
└── file1.doc
|
|
|
|
>>> shift_nodes(root, ["Downloads/photo1.jpg"], [None])
|
|
>>> root.show()
|
|
Downloads
|
|
└── file1.doc
|
|
|
|
In overriding case,
|
|
|
|
>>> root = str_to_tree(
|
|
... "Downloads\\n"
|
|
... "├── Misc\\n"
|
|
... "│ └── Pictures\\n"
|
|
... "│ └── photo1.jpg\\n"
|
|
... "└── Pictures\\n"
|
|
... " └── photo2.jpg"
|
|
... )
|
|
>>> root.show()
|
|
Downloads
|
|
├── Misc
|
|
│ └── Pictures
|
|
│ └── photo1.jpg
|
|
└── Pictures
|
|
└── photo2.jpg
|
|
|
|
>>> shift_nodes(root, ["Downloads/Misc/Pictures"], ["Downloads/Pictures"], overriding=True)
|
|
>>> root.show()
|
|
Downloads
|
|
├── Misc
|
|
└── Pictures
|
|
└── photo1.jpg
|
|
|
|
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.
|
|
|
|
>>> root = str_to_tree(
|
|
... "Downloads\\n"
|
|
... "├── Misc\\n"
|
|
... "│ ├── Pictures\\n"
|
|
... "│ │ └── photo2.jpg\\n"
|
|
... "│ └── Applications\\n"
|
|
... "│ └── Chrome.exe\\n"
|
|
... "├── Pictures\\n"
|
|
... "│ └── photo1.jpg\\n"
|
|
... "└── dummy\\n"
|
|
... " └── Files\\n"
|
|
... " └── file1.doc"
|
|
... )
|
|
>>> root.show()
|
|
Downloads
|
|
├── Misc
|
|
│ ├── Pictures
|
|
│ │ └── photo2.jpg
|
|
│ └── Applications
|
|
│ └── Chrome.exe
|
|
├── Pictures
|
|
│ └── photo1.jpg
|
|
└── dummy
|
|
└── Files
|
|
└── file1.doc
|
|
|
|
>>> shift_nodes(
|
|
... root,
|
|
... ["Downloads/Misc/Pictures", "Applications", "Downloads/dummy"],
|
|
... ["Downloads/Pictures", "Downloads/Applications", "Downloads/dummy"],
|
|
... merge_children=True,
|
|
... )
|
|
>>> root.show()
|
|
Downloads
|
|
├── Misc
|
|
├── Pictures
|
|
│ ├── photo1.jpg
|
|
│ └── photo2.jpg
|
|
├── Chrome.exe
|
|
└── Files
|
|
└── file1.doc
|
|
|
|
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.
|
|
|
|
>>> root = str_to_tree(
|
|
... "Downloads\\n"
|
|
... "├── Misc\\n"
|
|
... "│ ├── Pictures\\n"
|
|
... "│ │ └── photo2.jpg\\n"
|
|
... "│ └── Applications\\n"
|
|
... "│ └── Chrome.exe\\n"
|
|
... "├── Pictures\\n"
|
|
... "│ └── photo1.jpg\\n"
|
|
... "└── dummy\\n"
|
|
... " └── Files\\n"
|
|
... " └── file1.doc"
|
|
... )
|
|
>>> root.show()
|
|
Downloads
|
|
├── Misc
|
|
│ ├── Pictures
|
|
│ │ └── photo2.jpg
|
|
│ └── Applications
|
|
│ └── Chrome.exe
|
|
├── Pictures
|
|
│ └── photo1.jpg
|
|
└── dummy
|
|
└── Files
|
|
└── file1.doc
|
|
|
|
>>> shift_nodes(
|
|
... root,
|
|
... ["Downloads/Misc/Pictures", "Applications", "Downloads/dummy"],
|
|
... ["Downloads/Pictures", "Downloads/Applications", "Downloads/dummy"],
|
|
... merge_leaves=True,
|
|
... )
|
|
>>> root.show()
|
|
Downloads
|
|
├── Misc
|
|
│ ├── Pictures
|
|
│ └── Applications
|
|
├── Pictures
|
|
│ ├── photo1.jpg
|
|
│ └── photo2.jpg
|
|
├── dummy
|
|
│ └── Files
|
|
├── Chrome.exe
|
|
└── file1.doc
|
|
|
|
In ``delete_children=True`` case, only the node is shifted without its accompanying children/descendants.
|
|
|
|
>>> root = str_to_tree(
|
|
... "Downloads\\n"
|
|
... "├── Misc\\n"
|
|
... "│ └── Applications\\n"
|
|
... "│ └── Chrome.exe\\n"
|
|
... "└── Pictures\\n"
|
|
... " └── photo1.jpg"
|
|
... )
|
|
>>> root.show()
|
|
Downloads
|
|
├── Misc
|
|
│ └── Applications
|
|
│ └── Chrome.exe
|
|
└── Pictures
|
|
└── photo1.jpg
|
|
|
|
>>> shift_nodes(root, ["Applications"], ["Downloads/Applications"], delete_children=True)
|
|
>>> root.show()
|
|
Downloads
|
|
├── Misc
|
|
├── Pictures
|
|
│ └── photo1.jpg
|
|
└── Applications
|
|
|
|
Args:
|
|
tree (Node): tree to modify
|
|
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
|
|
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,
|
|
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,
|
|
with_full_path=with_full_path,
|
|
) # 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,
|
|
with_full_path: bool = False,
|
|
) -> 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).
|
|
- 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.
|
|
|
|
For paths in `from_paths`,
|
|
|
|
- Path name can be partial path (trailing part of path) or node name.
|
|
- If ``with_full_path=True``, path name must be full path.
|
|
- Path name must be unique to one node.
|
|
|
|
For paths in `to_paths`,
|
|
|
|
- Path name must be full path.
|
|
|
|
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.
|
|
|
|
Examples:
|
|
>>> from bigtree import list_to_tree, str_to_tree, copy_nodes
|
|
>>> root = list_to_tree(["Downloads/Pictures", "Downloads/photo1.jpg", "Downloads/file1.doc"])
|
|
>>> root.show()
|
|
Downloads
|
|
├── Pictures
|
|
├── photo1.jpg
|
|
└── file1.doc
|
|
|
|
>>> copy_nodes(
|
|
... tree=root,
|
|
... from_paths=["Downloads/photo1.jpg", "Downloads/file1.doc"],
|
|
... to_paths=["Downloads/Pictures/photo1.jpg", "Downloads/Files/file1.doc"],
|
|
... )
|
|
>>> root.show()
|
|
Downloads
|
|
├── Pictures
|
|
│ └── photo1.jpg
|
|
├── photo1.jpg
|
|
├── file1.doc
|
|
└── Files
|
|
└── file1.doc
|
|
|
|
In overriding case,
|
|
|
|
>>> root = str_to_tree(
|
|
... "Downloads\\n"
|
|
... "├── Misc\\n"
|
|
... "│ └── Pictures\\n"
|
|
... "│ └── photo1.jpg\\n"
|
|
... "└── Pictures\\n"
|
|
... " └── photo2.jpg"
|
|
... )
|
|
>>> root.show()
|
|
Downloads
|
|
├── Misc
|
|
│ └── Pictures
|
|
│ └── photo1.jpg
|
|
└── Pictures
|
|
└── photo2.jpg
|
|
|
|
>>> copy_nodes(root, ["Downloads/Misc/Pictures"], ["Downloads/Pictures"], overriding=True)
|
|
>>> root.show()
|
|
Downloads
|
|
├── Misc
|
|
│ └── Pictures
|
|
│ └── photo1.jpg
|
|
└── Pictures
|
|
└── photo1.jpg
|
|
|
|
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.
|
|
|
|
>>> root = str_to_tree(
|
|
... "Downloads\\n"
|
|
... "├── Misc\\n"
|
|
... "│ ├── Pictures\\n"
|
|
... "│ │ └── photo2.jpg\\n"
|
|
... "│ └── Applications\\n"
|
|
... "│ └── Chrome.exe\\n"
|
|
... "├── Pictures\\n"
|
|
... "│ └── photo1.jpg\\n"
|
|
... "└── dummy\\n"
|
|
... " └── Files\\n"
|
|
... " └── file1.doc"
|
|
... )
|
|
>>> root.show()
|
|
Downloads
|
|
├── Misc
|
|
│ ├── Pictures
|
|
│ │ └── photo2.jpg
|
|
│ └── Applications
|
|
│ └── Chrome.exe
|
|
├── Pictures
|
|
│ └── photo1.jpg
|
|
└── dummy
|
|
└── Files
|
|
└── file1.doc
|
|
|
|
>>> copy_nodes(
|
|
... root,
|
|
... ["Downloads/Misc/Pictures", "Applications", "Downloads/dummy"],
|
|
... ["Downloads/Pictures", "Downloads/Applications", "Downloads/dummy"],
|
|
... merge_children=True,
|
|
... )
|
|
>>> root.show()
|
|
Downloads
|
|
├── Misc
|
|
│ ├── Pictures
|
|
│ │ └── photo2.jpg
|
|
│ └── Applications
|
|
│ └── Chrome.exe
|
|
├── Pictures
|
|
│ ├── photo1.jpg
|
|
│ └── photo2.jpg
|
|
├── Chrome.exe
|
|
└── Files
|
|
└── file1.doc
|
|
|
|
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.
|
|
|
|
>>> root = str_to_tree(
|
|
... "Downloads\\n"
|
|
... "├── Misc\\n"
|
|
... "│ ├── Pictures\\n"
|
|
... "│ │ └── photo2.jpg\\n"
|
|
... "│ └── Applications\\n"
|
|
... "│ └── Chrome.exe\\n"
|
|
... "├── Pictures\\n"
|
|
... "│ └── photo1.jpg\\n"
|
|
... "└── dummy\\n"
|
|
... " └── Files\\n"
|
|
... " └── file1.doc"
|
|
... )
|
|
>>> root.show()
|
|
Downloads
|
|
├── Misc
|
|
│ ├── Pictures
|
|
│ │ └── photo2.jpg
|
|
│ └── Applications
|
|
│ └── Chrome.exe
|
|
├── Pictures
|
|
│ └── photo1.jpg
|
|
└── dummy
|
|
└── Files
|
|
└── file1.doc
|
|
|
|
>>> copy_nodes(
|
|
... root,
|
|
... ["Downloads/Misc/Pictures", "Applications", "Downloads/dummy"],
|
|
... ["Downloads/Pictures", "Downloads/Applications", "Downloads/dummy"],
|
|
... merge_leaves=True,
|
|
... )
|
|
>>> root.show()
|
|
Downloads
|
|
├── Misc
|
|
│ ├── Pictures
|
|
│ │ └── photo2.jpg
|
|
│ └── Applications
|
|
│ └── Chrome.exe
|
|
├── Pictures
|
|
│ ├── photo1.jpg
|
|
│ └── photo2.jpg
|
|
├── dummy
|
|
│ └── Files
|
|
│ └── file1.doc
|
|
├── Chrome.exe
|
|
└── file1.doc
|
|
|
|
In ``delete_children=True`` case, only the node is copied without its accompanying children/descendants.
|
|
|
|
>>> root = str_to_tree(
|
|
... "Downloads\\n"
|
|
... "├── Misc\\n"
|
|
... "│ └── Applications\\n"
|
|
... "│ └── Chrome.exe\\n"
|
|
... "└── Pictures\\n"
|
|
... " └── photo1.jpg"
|
|
... )
|
|
>>> root.show()
|
|
Downloads
|
|
├── Misc
|
|
│ └── Applications
|
|
│ └── Chrome.exe
|
|
└── Pictures
|
|
└── photo1.jpg
|
|
|
|
>>> copy_nodes(root, ["Applications"], ["Downloads/Applications"], delete_children=True)
|
|
>>> root.show()
|
|
Downloads
|
|
├── Misc
|
|
│ └── Applications
|
|
│ └── Chrome.exe
|
|
├── Pictures
|
|
│ └── photo1.jpg
|
|
└── Applications
|
|
|
|
Args:
|
|
tree (Node): tree to modify
|
|
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
|
|
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,
|
|
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,
|
|
with_full_path=with_full_path,
|
|
) # pragma: no cover
|
|
|
|
|
|
def shift_and_replace_nodes(
|
|
tree: Node,
|
|
from_paths: List[str],
|
|
to_paths: List[str],
|
|
sep: str = "/",
|
|
skippable: bool = False,
|
|
delete_children: bool = False,
|
|
with_full_path: bool = False,
|
|
) -> 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).
|
|
- 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.
|
|
|
|
For paths in `from_paths`,
|
|
|
|
- Path name can be partial path (trailing part of path) or node name.
|
|
- If ``with_full_path=True``, path name must be full path.
|
|
- Path name must be unique to one node.
|
|
|
|
For paths in `to_paths`,
|
|
|
|
- Path name must be full path.
|
|
- Path must exist, node-to-be-replaced must be present.
|
|
|
|
Examples:
|
|
>>> from bigtree import str_to_tree, shift_and_replace_nodes
|
|
>>> root = str_to_tree(
|
|
... "Downloads\\n"
|
|
... "├── Pictures\\n"
|
|
... "│ └── photo1.jpg\\n"
|
|
... "└── Misc\\n"
|
|
... " └── dummy"
|
|
... )
|
|
>>> root.show()
|
|
Downloads
|
|
├── Pictures
|
|
│ └── photo1.jpg
|
|
└── Misc
|
|
└── dummy
|
|
|
|
>>> shift_and_replace_nodes(root, ["Downloads/Pictures"], ["Downloads/Misc/dummy"])
|
|
>>> root.show()
|
|
Downloads
|
|
└── Misc
|
|
└── Pictures
|
|
└── photo1.jpg
|
|
|
|
In ``delete_children=True`` case, only the node is shifted without its accompanying children/descendants.
|
|
|
|
>>> root = str_to_tree(
|
|
... "Downloads\\n"
|
|
... "├── Pictures\\n"
|
|
... "│ └── photo1.jpg\\n"
|
|
... "└── Misc\\n"
|
|
... " └── dummy"
|
|
... )
|
|
>>> root.show()
|
|
Downloads
|
|
├── Pictures
|
|
│ └── photo1.jpg
|
|
└── Misc
|
|
└── dummy
|
|
|
|
>>> shift_and_replace_nodes(root, ["Downloads/Pictures"], ["Downloads/Misc/dummy"], delete_children=True)
|
|
>>> root.show()
|
|
Downloads
|
|
└── Misc
|
|
└── Pictures
|
|
|
|
Args:
|
|
tree (Node): tree to modify
|
|
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
|
|
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,
|
|
defaults to False
|
|
"""
|
|
return replace_logic(
|
|
tree=tree,
|
|
from_paths=from_paths,
|
|
to_paths=to_paths,
|
|
sep=sep,
|
|
copy=False,
|
|
skippable=skippable,
|
|
delete_children=delete_children,
|
|
to_tree=None,
|
|
with_full_path=with_full_path,
|
|
) # 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,
|
|
with_full_path: bool = False,
|
|
) -> 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).
|
|
- 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.
|
|
|
|
For paths in `from_paths`,
|
|
|
|
- Path name can be partial path (trailing part of path) or node name.
|
|
- If ``with_full_path=True``, path name must be full path.
|
|
- Path name must be unique to one node.
|
|
|
|
For paths in `to_paths`,
|
|
|
|
- Path name must be full path.
|
|
|
|
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.
|
|
|
|
Examples:
|
|
>>> from bigtree import Node, str_to_tree, copy_nodes_from_tree_to_tree
|
|
>>> root = str_to_tree(
|
|
... "Downloads\\n"
|
|
... "├── file1.doc\\n"
|
|
... "├── Pictures\\n"
|
|
... "│ └── photo1.jpg\\n"
|
|
... "└── Misc\\n"
|
|
... " └── dummy\\n"
|
|
... " └── photo2.jpg"
|
|
... )
|
|
>>> root.show()
|
|
Downloads
|
|
├── file1.doc
|
|
├── Pictures
|
|
│ └── photo1.jpg
|
|
└── Misc
|
|
└── dummy
|
|
└── photo2.jpg
|
|
|
|
>>> root_other = Node("Documents")
|
|
>>> copy_nodes_from_tree_to_tree(
|
|
... from_tree=root,
|
|
... to_tree=root_other,
|
|
... from_paths=["Downloads/Pictures", "Downloads/Misc"],
|
|
... to_paths=["Documents/Pictures", "Documents/New Misc/Misc"],
|
|
... )
|
|
>>> root_other.show()
|
|
Documents
|
|
├── Pictures
|
|
│ └── photo1.jpg
|
|
└── New Misc
|
|
└── Misc
|
|
└── dummy
|
|
└── photo2.jpg
|
|
|
|
In overriding case,
|
|
|
|
>>> root_other = str_to_tree(
|
|
... "Documents\\n"
|
|
... "└── Pictures\\n"
|
|
... " └── photo3.jpg"
|
|
... )
|
|
>>> root_other.show()
|
|
Documents
|
|
└── Pictures
|
|
└── photo3.jpg
|
|
|
|
>>> copy_nodes_from_tree_to_tree(
|
|
... root,
|
|
... root_other,
|
|
... ["Downloads/Pictures", "Downloads/Misc"],
|
|
... ["Documents/Pictures", "Documents/Misc"],
|
|
... overriding=True,
|
|
... )
|
|
>>> root_other.show()
|
|
Documents
|
|
├── Pictures
|
|
│ └── photo1.jpg
|
|
└── Misc
|
|
└── dummy
|
|
└── photo2.jpg
|
|
|
|
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.
|
|
|
|
>>> root_other = str_to_tree(
|
|
... "Documents\\n"
|
|
... "└── Pictures\\n"
|
|
... " └── photo3.jpg"
|
|
... )
|
|
>>> root_other.show()
|
|
Documents
|
|
└── Pictures
|
|
└── photo3.jpg
|
|
|
|
>>> copy_nodes_from_tree_to_tree(
|
|
... root,
|
|
... root_other,
|
|
... ["Downloads/Pictures", "Downloads/Misc"],
|
|
... ["Documents/Pictures", "Documents/Misc"],
|
|
... merge_children=True,
|
|
... )
|
|
>>> root_other.show()
|
|
Documents
|
|
├── Pictures
|
|
│ ├── photo3.jpg
|
|
│ └── photo1.jpg
|
|
└── dummy
|
|
└── photo2.jpg
|
|
|
|
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.
|
|
|
|
>>> root_other = str_to_tree(
|
|
... "Documents\\n"
|
|
... "└── Pictures\\n"
|
|
... " └── photo3.jpg"
|
|
... )
|
|
>>> root_other.show()
|
|
Documents
|
|
└── Pictures
|
|
└── photo3.jpg
|
|
|
|
>>> copy_nodes_from_tree_to_tree(
|
|
... root,
|
|
... root_other,
|
|
... ["Downloads/Pictures", "Downloads/Misc"],
|
|
... ["Documents/Pictures", "Documents/Misc"],
|
|
... merge_leaves=True,
|
|
... )
|
|
>>> root_other.show()
|
|
Documents
|
|
├── Pictures
|
|
│ ├── photo3.jpg
|
|
│ └── photo1.jpg
|
|
└── photo2.jpg
|
|
|
|
In ``delete_children=True`` case, only the node is copied without its accompanying children/descendants.
|
|
|
|
>>> root_other = Node("Documents")
|
|
>>> root_other.show()
|
|
Documents
|
|
|
|
>>> copy_nodes_from_tree_to_tree(
|
|
... root,
|
|
... root_other,
|
|
... ["Downloads/Pictures", "Downloads/Misc"],
|
|
... ["Documents/Pictures", "Documents/Misc"],
|
|
... delete_children=True,
|
|
... )
|
|
>>> root_other.show()
|
|
Documents
|
|
├── Pictures
|
|
└── Misc
|
|
|
|
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
|
|
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
|
|
with_full_path (bool): indicator to shift/copy node with full path in `from_paths`, results in faster search,
|
|
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,
|
|
with_full_path=with_full_path,
|
|
) # pragma: no cover
|
|
|
|
|
|
def copy_and_replace_nodes_from_tree_to_tree(
|
|
from_tree: Node,
|
|
to_tree: Node,
|
|
from_paths: List[str],
|
|
to_paths: List[str],
|
|
sep: str = "/",
|
|
skippable: bool = False,
|
|
delete_children: bool = False,
|
|
with_full_path: bool = False,
|
|
) -> 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).
|
|
- 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.
|
|
|
|
For paths in `from_paths`,
|
|
|
|
- Path name can be partial path (trailing part of path) or node name.
|
|
- If ``with_full_path=True``, path name must be full path.
|
|
- Path name must be unique to one node.
|
|
|
|
For paths in `to_paths`,
|
|
|
|
- Path name must be full path.
|
|
- Path must exist, node-to-be-replaced must be present.
|
|
|
|
Examples:
|
|
>>> from bigtree import str_to_tree, copy_and_replace_nodes_from_tree_to_tree
|
|
>>> root = str_to_tree(
|
|
... "Downloads\\n"
|
|
... "├── file1.doc\\n"
|
|
... "├── Pictures\\n"
|
|
... "│ └── photo1.jpg\\n"
|
|
... "└── Misc\\n"
|
|
... " └── dummy\\n"
|
|
... " └── photo2.jpg"
|
|
... )
|
|
>>> root.show()
|
|
Downloads
|
|
├── file1.doc
|
|
├── Pictures
|
|
│ └── photo1.jpg
|
|
└── Misc
|
|
└── dummy
|
|
└── photo2.jpg
|
|
|
|
>>> root_other = str_to_tree(
|
|
... "Documents\\n"
|
|
... "├── Pictures2\\n"
|
|
... "│ └── photo2.jpg\\n"
|
|
... "└── Misc2"
|
|
... )
|
|
>>> root_other.show()
|
|
Documents
|
|
├── Pictures2
|
|
│ └── photo2.jpg
|
|
└── Misc2
|
|
|
|
>>> copy_and_replace_nodes_from_tree_to_tree(
|
|
... from_tree=root,
|
|
... to_tree=root_other,
|
|
... from_paths=["Downloads/Pictures", "Downloads/Misc"],
|
|
... to_paths=["Documents/Pictures2/photo2.jpg", "Documents/Misc2"],
|
|
... )
|
|
>>> root_other.show()
|
|
Documents
|
|
├── Pictures2
|
|
│ └── Pictures
|
|
│ └── photo1.jpg
|
|
└── Misc
|
|
└── dummy
|
|
└── photo2.jpg
|
|
|
|
In ``delete_children=True`` case, only the node is copied without its accompanying children/descendants.
|
|
|
|
>>> root_other = str_to_tree(
|
|
... "Documents\\n"
|
|
... "├── Pictures2\\n"
|
|
... "│ └── photo2.jpg\\n"
|
|
... "└── Misc2"
|
|
... )
|
|
>>> root_other.show()
|
|
Documents
|
|
├── Pictures2
|
|
│ └── photo2.jpg
|
|
└── Misc2
|
|
|
|
>>> copy_and_replace_nodes_from_tree_to_tree(
|
|
... from_tree=root,
|
|
... to_tree=root_other,
|
|
... from_paths=["Downloads/Pictures", "Downloads/Misc"],
|
|
... to_paths=["Documents/Pictures2/photo2.jpg", "Documents/Misc2"],
|
|
... delete_children=True,
|
|
... )
|
|
>>> root_other.show()
|
|
Documents
|
|
├── Pictures2
|
|
│ └── Pictures
|
|
└── Misc
|
|
|
|
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
|
|
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,
|
|
defaults to False
|
|
"""
|
|
return replace_logic(
|
|
tree=from_tree,
|
|
from_paths=from_paths,
|
|
to_paths=to_paths,
|
|
sep=sep,
|
|
copy=True,
|
|
skippable=skippable,
|
|
delete_children=delete_children,
|
|
to_tree=to_tree,
|
|
with_full_path=with_full_path,
|
|
) # 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,
|
|
with_full_path: bool = False,
|
|
) -> 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.
|
|
|
|
For paths in `from_paths`,
|
|
|
|
- Path name can be partial path (trailing part of path) or node name.
|
|
- If ``with_full_path=True``, path name must be full path.
|
|
- Path name must be unique to one node.
|
|
|
|
For paths in `to_paths`,
|
|
|
|
- Path name must be full path.
|
|
- 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[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`
|
|
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
|
|
with_full_path (bool): indicator to shift/copy node with full path in `from_paths`, results in faster search,
|
|
defaults to False
|
|
"""
|
|
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"
|
|
)
|
|
if copy and (None in to_paths or "" in to_paths):
|
|
raise ValueError(
|
|
"Deletion of node will not happen if `copy=True`, check your `copy` parameter."
|
|
)
|
|
|
|
# Modify `sep` of from_paths and to_paths
|
|
if not to_tree:
|
|
to_tree = tree
|
|
tree_sep = to_tree.sep
|
|
from_paths = [path.rstrip(sep).replace(sep, tree.sep) for path in from_paths]
|
|
to_paths = [
|
|
path.rstrip(sep).replace(sep, tree_sep) if path else None for path in to_paths
|
|
]
|
|
|
|
for from_path, to_path in zip(from_paths, to_paths):
|
|
if to_path:
|
|
if from_path.split(tree.sep)[-1] != to_path.split(tree_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."
|
|
)
|
|
|
|
if with_full_path:
|
|
if not all(
|
|
[
|
|
path.lstrip(tree.sep).split(tree.sep)[0] == tree.root.node_name
|
|
for path in from_paths
|
|
]
|
|
):
|
|
raise ValueError(
|
|
"Invalid path in `from_paths` not starting with the root node. "
|
|
"Check your `from_paths` parameter, alternatively set `with_full_path=False` to shift "
|
|
"partial path instead of full path."
|
|
)
|
|
if not all(
|
|
[
|
|
path.lstrip(tree_sep).split(tree_sep)[0] == to_tree.root.node_name
|
|
for path in to_paths
|
|
if path
|
|
]
|
|
):
|
|
raise ValueError(
|
|
"Invalid path in `to_paths` not starting with the root node. Check your `to_paths` parameter."
|
|
)
|
|
|
|
# Perform shifting/copying
|
|
for from_path, to_path in zip(from_paths, to_paths):
|
|
if with_full_path:
|
|
from_node = find_full_path(tree, from_path)
|
|
else:
|
|
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_node = find_full_path(to_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, create intermediate parent node if applicable
|
|
to_path_parent = tree_sep.join(to_path.split(tree_sep)[:-1])
|
|
to_node = add_path_to_tree(to_tree, to_path_parent, sep=tree_sep)
|
|
|
|
# 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
|
|
|
|
|
|
def replace_logic(
|
|
tree: Node,
|
|
from_paths: List[str],
|
|
to_paths: List[str],
|
|
sep: str = "/",
|
|
copy: bool = False,
|
|
skippable: bool = False,
|
|
delete_children: bool = False,
|
|
to_tree: Optional[Node] = None,
|
|
with_full_path: bool = False,
|
|
) -> None:
|
|
"""Shift or copy nodes from `from_paths` to *replace* `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 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)
|
|
|
|
For paths in `from_paths` and `to_paths`,
|
|
|
|
- Path name can be with or without leading tree path separator symbol.
|
|
|
|
For paths in `from_paths`,
|
|
|
|
- Path name can be partial path (trailing part of path) or node name.
|
|
- If ``with_full_path=True``, path name must be full path.
|
|
- Path name must be unique to one node.
|
|
|
|
For paths in `to_paths`,
|
|
|
|
- Path name must be full path.
|
|
- Path must exist, node-to-be-replaced must be present.
|
|
|
|
Args:
|
|
tree (Node): tree to modify
|
|
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`
|
|
copy (bool): indicator to copy node, defaults to False
|
|
skippable (bool): indicator to skip if from path is not found, 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,
|
|
defaults to False
|
|
"""
|
|
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"
|
|
)
|
|
|
|
# Modify `sep` of from_paths and to_paths
|
|
if not to_tree:
|
|
to_tree = tree
|
|
tree_sep = to_tree.sep
|
|
from_paths = [path.rstrip(sep).replace(sep, tree.sep) for path in from_paths]
|
|
to_paths = [
|
|
path.rstrip(sep).replace(sep, tree_sep) if path else None for path in to_paths
|
|
]
|
|
|
|
if with_full_path:
|
|
if not all(
|
|
[
|
|
path.lstrip(tree.sep).split(tree.sep)[0] == tree.root.node_name
|
|
for path in from_paths
|
|
]
|
|
):
|
|
raise ValueError(
|
|
"Invalid path in `from_paths` not starting with the root node. "
|
|
"Check your `from_paths` parameter, alternatively set `with_full_path=False` to shift "
|
|
"partial path instead of full path."
|
|
)
|
|
if not all(
|
|
[
|
|
path.lstrip(tree_sep).split(tree_sep)[0] == to_tree.root.node_name
|
|
for path in to_paths
|
|
if path
|
|
]
|
|
):
|
|
raise ValueError(
|
|
"Invalid path in `to_paths` not starting with the root node. Check your `to_paths` parameter."
|
|
)
|
|
|
|
# Perform shifting/copying to replace destination node
|
|
for from_path, to_path in zip(from_paths, to_paths):
|
|
if with_full_path:
|
|
from_node = find_full_path(tree, from_path)
|
|
else:
|
|
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:
|
|
to_node = find_full_path(to_tree, to_path)
|
|
|
|
# To node found
|
|
if to_node:
|
|
if from_node == to_node:
|
|
raise TreeError(
|
|
f"Attempting to replace the same node {from_node.node_name}\n"
|
|
f"Check from path {from_path} and to path {to_path}"
|
|
)
|
|
|
|
# To node not found
|
|
else:
|
|
raise NotFoundError(f"Unable to find to_path {to_path}")
|
|
|
|
# Replace to_node with from_node
|
|
if copy:
|
|
logging.debug(f"Copying {from_node.node_name}")
|
|
from_node = from_node.copy()
|
|
if delete_children:
|
|
del from_node.children
|
|
parent = to_node.parent
|
|
to_node_siblings = parent.children
|
|
to_node_idx = to_node_siblings.index(to_node)
|
|
for node in to_node_siblings[to_node_idx:]:
|
|
if node == to_node:
|
|
to_node.parent = None
|
|
from_node.parent = parent
|
|
else:
|
|
node.parent = None
|
|
node.parent = parent
|