|
11 | 11 | __all__ = [ |
12 | 12 | "shift_nodes", |
13 | 13 | "copy_nodes", |
| 14 | + "shift_and_replace_nodes", |
14 | 15 | "copy_nodes_from_tree_to_tree", |
| 16 | + "copy_and_replace_nodes_from_tree_to_tree", |
15 | 17 | "copy_or_shift_logic", |
| 18 | + "replace_logic", |
16 | 19 | ] |
17 | 20 |
|
18 | 21 |
|
@@ -494,6 +497,97 @@ def copy_nodes( |
494 | 497 | ) # pragma: no cover |
495 | 498 |
|
496 | 499 |
|
| 500 | +def shift_and_replace_nodes( |
| 501 | + tree: Node, |
| 502 | + from_paths: List[str], |
| 503 | + to_paths: List[str], |
| 504 | + sep: str = "/", |
| 505 | + skippable: bool = False, |
| 506 | + delete_children: bool = False, |
| 507 | + with_full_path: bool = False, |
| 508 | +) -> None: |
| 509 | + """Shift nodes from `from_paths` to *replace* `to_paths` *in-place*. |
| 510 | +
|
| 511 | + - Creates intermediate nodes if to path is not present |
| 512 | + - Able to skip nodes if from path is not found, defaults to False (from-nodes must be found; not skippable). |
| 513 | + - Able to shift node only and delete children, defaults to False (nodes are shifted together with children). |
| 514 | +
|
| 515 | + For paths in `from_paths` and `to_paths`, |
| 516 | + - Path name can be with or without leading tree path separator symbol. |
| 517 | +
|
| 518 | + For paths in `from_paths`, |
| 519 | + - Path name can be partial path (trailing part of path) or node name. |
| 520 | + - If ``with_full_path=True``, path name must be full path. |
| 521 | + - Path name must be unique to one node. |
| 522 | +
|
| 523 | + For paths in `to_paths`, |
| 524 | + - Path name must be full path. |
| 525 | + - Path must exist, node-to-be-replaced must be present. |
| 526 | +
|
| 527 | + >>> from bigtree import Node, shift_and_replace_nodes |
| 528 | + >>> root = Node("a") |
| 529 | + >>> b = Node("b", parent=root) |
| 530 | + >>> c = Node("c", parent=b) |
| 531 | + >>> d = Node("d", parent=root) |
| 532 | + >>> e = Node("e", parent=d) |
| 533 | + >>> root.show() |
| 534 | + a |
| 535 | + ├── b |
| 536 | + │ └── c |
| 537 | + └── d |
| 538 | + └── e |
| 539 | +
|
| 540 | + >>> shift_and_replace_nodes(root, ["a/b"], ["a/d/e"]) |
| 541 | + >>> root.show() |
| 542 | + a |
| 543 | + └── d |
| 544 | + └── b |
| 545 | + └── c |
| 546 | +
|
| 547 | + In ``delete_children=True`` case, only the node is shifted without its accompanying children/descendants. |
| 548 | +
|
| 549 | + >>> from bigtree import Node, shift_and_replace_nodes |
| 550 | + >>> root = Node("a") |
| 551 | + >>> b = Node("b", parent=root) |
| 552 | + >>> c = Node("c", parent=b) |
| 553 | + >>> d = Node("d", parent=root) |
| 554 | + >>> e = Node("e", parent=d) |
| 555 | + >>> root.show() |
| 556 | + a |
| 557 | + ├── b |
| 558 | + │ └── c |
| 559 | + └── d |
| 560 | + └── e |
| 561 | +
|
| 562 | + >>> shift_and_replace_nodes(root, ["a/b"], ["a/d/e"], delete_children=True) |
| 563 | + >>> root.show() |
| 564 | + a |
| 565 | + └── d |
| 566 | + └── b |
| 567 | +
|
| 568 | + Args: |
| 569 | + tree (Node): tree to modify |
| 570 | + from_paths (List[str]): original paths to shift nodes from |
| 571 | + to_paths (List[str]): new paths to shift nodes to |
| 572 | + sep (str): path separator for input paths, applies to `from_path` and `to_path` |
| 573 | + skippable (bool): indicator to skip if from path is not found, defaults to False |
| 574 | + delete_children (bool): indicator to shift node only without children, defaults to False |
| 575 | + with_full_path (bool): indicator to shift/copy node with full path in `from_paths`, results in faster search, |
| 576 | + defaults to False |
| 577 | + """ |
| 578 | + return replace_logic( |
| 579 | + tree=tree, |
| 580 | + from_paths=from_paths, |
| 581 | + to_paths=to_paths, |
| 582 | + sep=sep, |
| 583 | + copy=False, |
| 584 | + skippable=skippable, |
| 585 | + delete_children=delete_children, |
| 586 | + to_tree=None, |
| 587 | + with_full_path=with_full_path, |
| 588 | + ) # pragma: no cover |
| 589 | + |
| 590 | + |
497 | 591 | def copy_nodes_from_tree_to_tree( |
498 | 592 | from_tree: Node, |
499 | 593 | to_tree: Node, |
@@ -664,6 +758,114 @@ def copy_nodes_from_tree_to_tree( |
664 | 758 | ) # pragma: no cover |
665 | 759 |
|
666 | 760 |
|
| 761 | +def copy_and_replace_nodes_from_tree_to_tree( |
| 762 | + from_tree: Node, |
| 763 | + to_tree: Node, |
| 764 | + from_paths: List[str], |
| 765 | + to_paths: List[str], |
| 766 | + sep: str = "/", |
| 767 | + skippable: bool = False, |
| 768 | + delete_children: bool = False, |
| 769 | + with_full_path: bool = False, |
| 770 | +) -> None: |
| 771 | + """Copy nodes from `from_paths` to *replace* `to_paths` *in-place*. |
| 772 | +
|
| 773 | + - Creates intermediate nodes if to path is not present |
| 774 | + - Able to skip nodes if from path is not found, defaults to False (from-nodes must be found; not skippable). |
| 775 | + - Able to copy node only and delete children, defaults to False (nodes are copied together with children). |
| 776 | +
|
| 777 | + For paths in `from_paths` and `to_paths`, |
| 778 | + - Path name can be with or without leading tree path separator symbol. |
| 779 | +
|
| 780 | + For paths in `from_paths`, |
| 781 | + - Path name can be partial path (trailing part of path) or node name. |
| 782 | + - If ``with_full_path=True``, path name must be full path. |
| 783 | + - Path name must be unique to one node. |
| 784 | +
|
| 785 | + For paths in `to_paths`, |
| 786 | + - Path name must be full path. |
| 787 | + - Path must exist, node-to-be-replaced must be present. |
| 788 | +
|
| 789 | + >>> from bigtree import Node, copy_and_replace_nodes_from_tree_to_tree |
| 790 | + >>> root = Node("a") |
| 791 | + >>> b = Node("b", parent=root) |
| 792 | + >>> c = Node("c", parent=root) |
| 793 | + >>> d = Node("d", parent=c) |
| 794 | + >>> e = Node("e", parent=root) |
| 795 | + >>> f = Node("f", parent=e) |
| 796 | + >>> g = Node("g", parent=f) |
| 797 | + >>> root.show() |
| 798 | + a |
| 799 | + ├── b |
| 800 | + ├── c |
| 801 | + │ └── d |
| 802 | + └── e |
| 803 | + └── f |
| 804 | + └── g |
| 805 | +
|
| 806 | + >>> root_other = Node("aa") |
| 807 | + >>> bb = Node("bb", parent=root_other) |
| 808 | + >>> cc = Node("cc", parent=bb) |
| 809 | + >>> dd = Node("dd", parent=root_other) |
| 810 | + >>> root_other.show() |
| 811 | + aa |
| 812 | + ├── bb |
| 813 | + │ └── cc |
| 814 | + └── dd |
| 815 | +
|
| 816 | + >>> copy_and_replace_nodes_from_tree_to_tree(root, root_other, ["a/c", "a/e"], ["aa/bb/cc", "aa/dd"]) |
| 817 | + >>> root_other.show() |
| 818 | + aa |
| 819 | + ├── bb |
| 820 | + │ └── c |
| 821 | + │ └── d |
| 822 | + └── e |
| 823 | + └── f |
| 824 | + └── g |
| 825 | +
|
| 826 | + In ``delete_children=True`` case, only the node is copied without its accompanying children/descendants. |
| 827 | +
|
| 828 | + >>> root_other = Node("aa") |
| 829 | + >>> bb = Node("bb", parent=root_other) |
| 830 | + >>> cc = Node("cc", parent=bb) |
| 831 | + >>> dd = Node("dd", parent=root_other) |
| 832 | + >>> root_other.show() |
| 833 | + aa |
| 834 | + ├── bb |
| 835 | + │ └── cc |
| 836 | + └── dd |
| 837 | +
|
| 838 | + >>> copy_and_replace_nodes_from_tree_to_tree(root, root_other, ["a/c", "a/e"], ["aa/bb/cc", "aa/dd"], delete_children=True) |
| 839 | + >>> root_other.show() |
| 840 | + aa |
| 841 | + ├── bb |
| 842 | + │ └── c |
| 843 | + └── e |
| 844 | +
|
| 845 | + Args: |
| 846 | + from_tree (Node): tree to copy nodes from |
| 847 | + to_tree (Node): tree to copy nodes to |
| 848 | + from_paths (List[str]): original paths to shift nodes from |
| 849 | + to_paths (List[str]): new paths to shift nodes to |
| 850 | + sep (str): path separator for input paths, applies to `from_path` and `to_path` |
| 851 | + skippable (bool): indicator to skip if from path is not found, defaults to False |
| 852 | + delete_children (bool): indicator to copy node only without children, defaults to False |
| 853 | + with_full_path (bool): indicator to shift/copy node with full path in `from_paths`, results in faster search, |
| 854 | + defaults to False |
| 855 | + """ |
| 856 | + return replace_logic( |
| 857 | + tree=from_tree, |
| 858 | + from_paths=from_paths, |
| 859 | + to_paths=to_paths, |
| 860 | + sep=sep, |
| 861 | + copy=True, |
| 862 | + skippable=skippable, |
| 863 | + delete_children=delete_children, |
| 864 | + to_tree=to_tree, |
| 865 | + with_full_path=with_full_path, |
| 866 | + ) # pragma: no cover |
| 867 | + |
| 868 | + |
667 | 869 | def copy_or_shift_logic( |
668 | 870 | tree: Node, |
669 | 871 | from_paths: List[str], |
@@ -786,7 +988,7 @@ def copy_or_shift_logic( |
786 | 988 | "Invalid path in `to_paths` not starting with the root node. Check your `to_paths` parameter." |
787 | 989 | ) |
788 | 990 |
|
789 | | - # Perform shifting |
| 991 | + # Perform shifting/copying |
790 | 992 | for from_path, to_path in zip(from_paths, to_paths): |
791 | 993 | if with_full_path: |
792 | 994 | from_node = find_full_path(tree, from_path) |
@@ -895,3 +1097,139 @@ def copy_or_shift_logic( |
895 | 1097 | if delete_children: |
896 | 1098 | del from_node.children |
897 | 1099 | from_node.parent = to_node |
| 1100 | + |
| 1101 | + |
| 1102 | +def replace_logic( |
| 1103 | + tree: Node, |
| 1104 | + from_paths: List[str], |
| 1105 | + to_paths: List[str], |
| 1106 | + sep: str = "/", |
| 1107 | + copy: bool = False, |
| 1108 | + skippable: bool = False, |
| 1109 | + delete_children: bool = False, |
| 1110 | + to_tree: Optional[Node] = None, |
| 1111 | + with_full_path: bool = False, |
| 1112 | +) -> None: |
| 1113 | + """Shift or copy nodes from `from_paths` to *replace* `to_paths` *in-place*. |
| 1114 | +
|
| 1115 | + - Creates intermediate nodes if to path is not present |
| 1116 | + - Able to copy node, defaults to False (nodes are shifted; not copied). |
| 1117 | + - Able to skip nodes if from path is not found, defaults to False (from-nodes must be found; not skippable) |
| 1118 | + - Able to replace node only and delete children, defaults to False (nodes are shifted/copied together with children). |
| 1119 | + - Able to shift/copy nodes from one tree to another tree, defaults to None (shifting/copying happens within same tree) |
| 1120 | +
|
| 1121 | + For paths in `from_paths` and `to_paths`, |
| 1122 | + - Path name can be with or without leading tree path separator symbol. |
| 1123 | +
|
| 1124 | + For paths in `from_paths`, |
| 1125 | + - Path name can be partial path (trailing part of path) or node name. |
| 1126 | + - If ``with_full_path=True``, path name must be full path. |
| 1127 | + - Path name must be unique to one node. |
| 1128 | +
|
| 1129 | + For paths in `to_paths`, |
| 1130 | + - Path name must be full path. |
| 1131 | + - Path must exist, node-to-be-replaced must be present. |
| 1132 | +
|
| 1133 | + Args: |
| 1134 | + tree (Node): tree to modify |
| 1135 | + from_paths (List[str]): original paths to shift nodes from |
| 1136 | + to_paths (List[str]): new paths to shift nodes to |
| 1137 | + sep (str): path separator for input paths, applies to `from_path` and `to_path` |
| 1138 | + copy (bool): indicator to copy node, defaults to False |
| 1139 | + skippable (bool): indicator to skip if from path is not found, defaults to False |
| 1140 | + delete_children (bool): indicator to shift/copy node only without children, defaults to False |
| 1141 | + to_tree (Node): tree to copy to, defaults to None |
| 1142 | + with_full_path (bool): indicator to shift/copy node with full path in `from_paths`, results in faster search, |
| 1143 | + defaults to False |
| 1144 | + """ |
| 1145 | + if not (isinstance(from_paths, list) and isinstance(to_paths, list)): |
| 1146 | + raise ValueError( |
| 1147 | + "Invalid type, `from_paths` and `to_paths` should be list type" |
| 1148 | + ) |
| 1149 | + if len(from_paths) != len(to_paths): |
| 1150 | + raise ValueError( |
| 1151 | + f"Paths are different length, input `from_paths` have {len(from_paths)} entries, " |
| 1152 | + f"while output `to_paths` have {len(to_paths)} entries" |
| 1153 | + ) |
| 1154 | + |
| 1155 | + # Modify `sep` of from_paths and to_paths |
| 1156 | + if not to_tree: |
| 1157 | + to_tree = tree |
| 1158 | + tree_sep = to_tree.sep |
| 1159 | + from_paths = [path.rstrip(sep).replace(sep, tree.sep) for path in from_paths] |
| 1160 | + to_paths = [ |
| 1161 | + path.rstrip(sep).replace(sep, tree_sep) if path else None for path in to_paths |
| 1162 | + ] |
| 1163 | + |
| 1164 | + if with_full_path: |
| 1165 | + if not all( |
| 1166 | + [ |
| 1167 | + path.lstrip(tree.sep).split(tree.sep)[0] == tree.root.node_name |
| 1168 | + for path in from_paths |
| 1169 | + ] |
| 1170 | + ): |
| 1171 | + raise ValueError( |
| 1172 | + "Invalid path in `from_paths` not starting with the root node. " |
| 1173 | + "Check your `from_paths` parameter, alternatively set `with_full_path=False` to shift " |
| 1174 | + "partial path instead of full path." |
| 1175 | + ) |
| 1176 | + if not all( |
| 1177 | + [ |
| 1178 | + path.lstrip(tree_sep).split(tree_sep)[0] == to_tree.root.node_name |
| 1179 | + for path in to_paths |
| 1180 | + if path |
| 1181 | + ] |
| 1182 | + ): |
| 1183 | + raise ValueError( |
| 1184 | + "Invalid path in `to_paths` not starting with the root node. Check your `to_paths` parameter." |
| 1185 | + ) |
| 1186 | + |
| 1187 | + # Perform shifting/copying to replace destination node |
| 1188 | + for from_path, to_path in zip(from_paths, to_paths): |
| 1189 | + if with_full_path: |
| 1190 | + from_node = find_full_path(tree, from_path) |
| 1191 | + else: |
| 1192 | + from_node = find_path(tree, from_path) |
| 1193 | + |
| 1194 | + # From node not found |
| 1195 | + if not from_node: |
| 1196 | + if not skippable: |
| 1197 | + raise NotFoundError( |
| 1198 | + f"Unable to find from_path {from_path}\n" |
| 1199 | + f"Set `skippable` to True to skip shifting for nodes not found" |
| 1200 | + ) |
| 1201 | + else: |
| 1202 | + logging.info(f"Unable to find from_path {from_path}") |
| 1203 | + |
| 1204 | + # From node found |
| 1205 | + else: |
| 1206 | + to_node = find_full_path(to_tree, to_path) |
| 1207 | + |
| 1208 | + # To node found |
| 1209 | + if to_node: |
| 1210 | + if from_node == to_node: |
| 1211 | + raise TreeError( |
| 1212 | + f"Attempting to replace the same node {from_node.node_name}\n" |
| 1213 | + f"Check from path {from_path} and to path {to_path}" |
| 1214 | + ) |
| 1215 | + |
| 1216 | + # To node not found |
| 1217 | + else: |
| 1218 | + raise NotFoundError(f"Unable to find to_path {to_path}") |
| 1219 | + |
| 1220 | + # Replace to_node with from_node |
| 1221 | + if copy: |
| 1222 | + logging.debug(f"Copying {from_node.node_name}") |
| 1223 | + from_node = from_node.copy() |
| 1224 | + if delete_children: |
| 1225 | + del from_node.children |
| 1226 | + parent = to_node.parent |
| 1227 | + to_node_siblings = parent.children |
| 1228 | + to_node_idx = to_node_siblings.index(to_node) |
| 1229 | + for node in to_node_siblings[to_node_idx:]: |
| 1230 | + if node == to_node: |
| 1231 | + to_node.parent = None |
| 1232 | + from_node.parent = parent |
| 1233 | + else: |
| 1234 | + node.parent = None |
| 1235 | + node.parent = parent |
0 commit comments