diff --git a/nutree/node.py b/nutree/node.py index e30145b..eac856e 100644 --- a/nutree/node.py +++ b/nutree/node.py @@ -636,12 +636,13 @@ def add_child( else: node = factory(child, parent=self, data_id=data_id, node_id=node_id) + if before is True: + before = 0 # prepend + children = self._children if children is None: assert before in (None, True, int, False) self._children = [node] - elif before is True: # prepend - children.insert(0, node) elif isinstance(before, int): children.insert(before, node) elif before: @@ -744,11 +745,9 @@ def move_to( See :meth:`add_child` for a description of `before`. """ assert new_parent is not None - # if new_parent is None: - # new_parent = self._tree._root - # elif if not isinstance(new_parent, Node): # it's a Tree + assert isinstance(new_parent, self.tree.__class__) new_parent = new_parent._root if new_parent._tree is not self._tree: @@ -759,12 +758,13 @@ def move_to( self._parent._children = None self._parent = new_parent + if before is True: + before = 0 # prepend + target_siblings = new_parent._children if target_siblings is None: assert before in (None, True, False, 0), before new_parent._children = [self] # type: ignore - elif before is True: # prepend - target_siblings.insert(0, self) elif isinstance(before, Node): assert before._parent is new_parent, before idx = target_siblings.index(before) # raise ValueError if not found diff --git a/pyproject.toml b/pyproject.toml index f61843a..8ed751d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,7 @@ addopts = "-ra -q --cov=nutree" # branch = true omit = [ "tests/*", + "setup.py", # nutree/leaves_cli.py # nutree/cli_common.py # nutree/monitor/* diff --git a/tests/test_core.py b/tests/test_core.py index dea4573..e47892e 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -214,6 +214,7 @@ def test_basic(self): # assert tree.last_child() is tree["The Little Prince"] assert len(records.children) == 2 + assert records.children == records.get_children() assert records.depth() == 1 with pytest.raises(NotImplementedError): assert tree == tree # __eq__ not implemented @@ -301,97 +302,6 @@ def test_relations(self): assert tree["a11"].get_index() == 0 assert tree["a12"].get_index() == 1 - def test_data_id(self): - """ - Tree<'fixture'> - ├── A - │ ├── a1 - │ │ ├── a11 - │ │ ╰── a12 - │ ╰── a2 - ╰── B - ├── a1 <-- Clone - ╰── b1 - ╰── b11 - """ - tree = fixture.create_tree() - tree["B"].prepend_child("a1") - - print(tree.format(repr="{node.data}")) - - tree["A"].rename("new_A") - - assert fixture.check_content( - tree, - """ - Tree<'fixture'> - +- new_A - | +- a1 - | | +- a11 - | | `- a12 - | `- a2 - `- B - +- a1 - `- b1 - `- b11 - """, - ) - assert tree._self_check() - - # Reset tree - tree = fixture.create_tree() - tree["B"].prepend_child("a1") - - with pytest.raises(AmbiguousMatchError): # not allowed for clones - tree.find_first("a1").rename("new_a1") - - with pytest.raises(ValueError): # missing args - tree.find_first("a1").set_data(None) - - # Only rename first occurrence: - tree.find_first("a1").set_data("new_a1", with_clones=False) - - assert fixture.check_content( - tree, - """ - Tree<'fixture'> - +- A - | +- new_a1 - | | +- a11 - | | `- a12 - | `- a2 - `- B - +- a1 - `- b1 - `- b11 - """, - ) - assert tree._self_check() - - # Reset tree - tree = fixture.create_tree() - tree["B"].prepend_child("a1") - - # Rename all occurences: - tree.find_first("a1").set_data("new_a1", with_clones=True) - - assert fixture.check_content( - tree, - """ - Tree<'fixture'> - +- A - | +- new_a1 - | | +- a11 - | | `- a12 - | `- a2 - `- B - +- new_a1 - `- b1 - `- b11 - """, - ) - assert tree._self_check() - def test_find(self): tree = self.tree @@ -771,6 +681,7 @@ def test_add(self): b = tree["B"] a11 = tree["a11"] b.prepend_child(a11) + b.add("pre_b", before=True) a1 = tree["a1"] a1.prepend_sibling("pre_a1") @@ -792,12 +703,143 @@ def test_add(self): | +- post_a1 | `- a2 `- B + +- pre_b +- a11 `- b1 `- b11 """, ) + def test_add_tree(self): + tree = fixture.create_tree() + + subtree = Tree() + subtree.add("x").add("x1").up(2).add("y").add("y1") + + tree.add(subtree, before=1) + assert fixture.check_content( + tree, + """ + Tree<*> + +- A + | +- a1 + | | +- a11 + | | `- a12 + | `- a2 + +- x + | `- x1 + +- y + | `- y1 + `- B + `- b1 + `- b11 + """, + ) + + def test_set_data(self): + """ + Tree<'fixture'> + ├── A + │ ├── a1 + │ │ ├── a11 + │ │ ╰── a12 + │ ╰── a2 + ╰── B + ├── a1 <-- Clone + ╰── b1 + ╰── b11 + """ + tree = fixture.create_tree() + tree["B"].prepend_child("a1") + + print(tree.format(repr="{node.data}")) + + tree["A"].rename("new_A") + + assert fixture.check_content( + tree, + """ + Tree<'fixture'> + +- new_A + | +- a1 + | | +- a11 + | | `- a12 + | `- a2 + `- B + +- a1 + `- b1 + `- b11 + """, + ) + assert tree._self_check() + + # Reset tree + tree = fixture.create_tree() + tree["B"].prepend_child("a1") + + with pytest.raises(AmbiguousMatchError): # not allowed for clones + tree.find_first("a1").rename("new_a1") + + with pytest.raises(ValueError): # missing args + tree.find_first("a1").set_data(None) + + # Only rename first occurrence: + tree.find_first("a1").set_data("new_a1", with_clones=False) + + assert fixture.check_content( + tree, + """ + Tree<'fixture'> + +- A + | +- new_a1 + | | +- a11 + | | `- a12 + | `- a2 + `- B + +- a1 + `- b1 + `- b11 + """, + ) + assert tree._self_check() + + # Reset tree + tree = fixture.create_tree() + tree["B"].prepend_child("a1") + + # Rename all occurences: + tree.find_first("a1").set_data("new_a1", with_clones=True) + + assert fixture.check_content( + tree, + """ + Tree<'fixture'> + +- A + | +- new_a1 + | | +- a11 + | | `- a12 + | `- a2 + `- B + +- new_a1 + `- b1 + `- b11 + """, + ) + + assert tree["a2"].data_id == hash("a2") + tree.find("a2").set_data(data=None, data_id=123, with_clones=True) + with pytest.raises(KeyError): + _ = tree["a2"].data_id + assert tree.find(data_id=123) + + tree.find(data_id=123).set_data(data="a2_new", data_id=123, with_clones=True) + assert tree.find(data_id=123) + + tree.find(data_id=123).set_data(data="a2_new2", data_id=123, with_clones=False) + assert tree.find(data_id=123) + + assert tree._self_check() + def test_copy_branch(self): # Copy a node tree = fixture.create_tree() @@ -1010,6 +1052,45 @@ def _tm( """, ) + _tm( + source="b1", + target="a1", + before=True, + result=""" + Tree<'fixture'> + +- A + | +- a1 + | | +- b1 + | | | `- b11 + | | +- a11 + | | `- a12 + | `- a2 + `- B + """, + ) + + tree = fixture.create_tree() + tree["b1"].move_to(tree) + + assert fixture.check_content( + tree, + """ + Tree<*> + +- A + | +- a1 + | | +- a11 + | | `- a12 + | `- a2 + +- B + `- b1 + `- b11 + """, + ) + + target_tree = Tree() + with pytest.raises(NotImplementedError): + tree["b1"].move_to(target_tree) + class TestCopy: def test_node_copy(self): @@ -1154,6 +1235,11 @@ def test_filter(self): """ tree = fixture.create_tree() + with pytest.raises(ValueError, match="Predicate is required"): + tree.filter(predicate=None) # type: ignore + with pytest.raises(ValueError, match="Predicate is required"): + tree.system_root.filter(predicate=None) # type: ignore + def pred(node): return "2" not in node.name.lower() @@ -1242,6 +1328,8 @@ def pred(node): # Should use tree.copy() instead: with pytest.raises(ValueError, match="Predicate is required"): tree_2 = tree.filtered(predicate=None) # type: ignore + with pytest.raises(ValueError, match="Predicate is required"): + tree_2 = tree.system_root.filtered(predicate=None) # type: ignore tree_2 = tree.copy() diff --git a/tests/test_typing_concept.py b/tests/test_typing_concept.py index a37344a..96c8c6d 100644 --- a/tests/test_typing_concept.py +++ b/tests/test_typing_concept.py @@ -1,93 +1,83 @@ -# (c) 2021-2024 Martin Wendt; see https://github.com/mar10/nutree -# Licensed under the MIT license: https://www.opensource.org/licenses/mit-license.php -""" """ # ruff: noqa: T201, T203 `print` found -from typing import Generic, Iterator, TypeVar +from __future__ import annotations import pytest -pytest.skip(allow_module_level=True) +# pytest.skip(allow_module_level=True) try: - from typing import Self -except ImportError: + from typing import Any, Generic, List, Self, TypeVar, cast +except ImportError as e: + print(f"ImportError: {e}") # from typing_extensions import Self typing_extensions = pytest.importorskip("typing_extensions") Self = typing_extensions.Self -ElementType = TypeVar("ElementType", bound="Element") +# TData = TypeVar("TData") +TNode = TypeVar("TNode", bound="Node") -class Element: - def __init__(self, parent: Self, name: str): - self.name = name - self.children: list[ElementType] = [] +class Node: + def __init__(self, data: Any, parent: Node): + self.data: Any = data + self.parent: Node = parent + self.children: List[Node] = [] - def __repr__(self): - return f"{self.__class__.__name__}({self.name})" + def add(self, data: Any) -> Node: + node = Node(data, self) + self.children.append(node) + return node - def get_pred(self) -> Self: - return self.parent.children[0] +class Tree(Generic[TNode]): + def __init__(self): + self.root: Node = Node("__root__", cast(TNode, None)) -class AgedElement(Element): - def __init__(self, parent: Self, name: str, age: int): - super().__init__(parent, name) - self.age = age + def add(self, data: Any) -> Node: + node = self.root.add(data) + return node - def __repr__(self): - return f"{self.__class__.__name__}({self.name}, {self.age})" + def first(self) -> TNode: + return self.root.children[0] - def is_adult(self) -> bool: - return self.age >= 18 +# ---------------------------- +# ---------------------------- -class Container(Generic[ElementType]): - def __init__(self, name: str): - # root = Element(None, "__root__") - self.name = name - self.children: ElementType = [] - def add(self, name: ElementType) -> ElementType: - self.children.append(ElementType(self, name)) +class TypedNode(Node): + def __init__(self, data: Any, kind: str, parent: TypedNode): + super().__init__(data, parent) + self.kind: str = kind + # self.children: List[TypedNode] = [] - def __iter__(self) -> Iterator[ElementType]: - return iter(self.children) + def add(self, data: Any, kind: str) -> TypedNode: + node = TypedNode(data, kind, self) + self.children.append(node) + return node - def __repr__(self): - return f"{self.__clas__.__name__}({self.name})" +class TypedTree(Tree[TypedNode]): + def __init__(self): + self.root: TypedNode = TypedNode("__root__", "__root__", cast(TypedNode, None)) -class AgedContainer(Container[AgedElement]): - def __init__(self, name: str): - self.name = name - self.children: ElementType = [] + def add(self, data: Any, kind: str) -> TNode: + node = self.root.add(data, kind) + return node - def add(self, name: str, age: int) -> AgedElement: - self.children.append(AgedElement) - def __iter__(self) -> Iterator[ElementType]: - return iter(self.children) +class TestTypingSelf: + def test_tree(self): + tree = Tree() + n = tree.add("top") + n.add("child") + tree.first().add("child2") + def test_typed_tree(self): + tree = TypedTree() + tree.add("child", kind="child") + tree.add("child2", kind="child") -class TestConcept: - def test_concept(self): - root = Container[Element]("SimpleContainer") - root.add("a") - root.add(Element("b")) - root.add(Element("c")) - root.add(AgedElement("d", 4)) - - for child in root: - print(child) - - root2 = Container[AgedElement]("") - root2.add(AgedElement("a", 1)) - root2.add(AgedElement("b", 2)) - root2.add(AgedElement("c", 3)) - root2.add(Element("d")) - - for child in root2: - print(child) + tree.first().add("child3", kind="child")