python-library-tree-model 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ .env
9
+ .pytest_cache/
10
+ config.yaml
11
+ logs/
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-library-tree-model
3
+ Version: 0.1.0
4
+ Requires-Python: >=3.10
5
+ Requires-Dist: pydantic>=2.0.0
6
+ Requires-Dist: python-library-reactive-model
@@ -0,0 +1,18 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "python-library-tree-model"
7
+ version = "0.1.0"
8
+ requires-python = ">=3.10"
9
+ dependencies = [
10
+ "python-library-reactive-model",
11
+ "pydantic>=2.0.0",
12
+ ]
13
+
14
+ [tool.hatch.metadata]
15
+ allow-direct-references = true
16
+
17
+ [tool.hatch.build.targets.wheel]
18
+ packages = ["tree_model"]
@@ -0,0 +1,10 @@
1
+ @echo off
2
+ cd /d %~dp0
3
+
4
+ if not exist .venv (
5
+ python -m venv .venv
6
+ )
7
+
8
+ call .venv\Scripts\activate.bat
9
+ python -m pip install -e .
10
+ python -m unittest discover -s tests -p "test_*.py"
@@ -0,0 +1,165 @@
1
+ import unittest
2
+
3
+ from tree_model import TreeModel
4
+
5
+
6
+ class TreeModelTests(unittest.TestCase):
7
+ def test_full_name_and_add_child(self) -> None:
8
+ root = TreeModel(name="root")
9
+ a = TreeModel(name="a")
10
+ b = TreeModel(name="b")
11
+
12
+ root.add_child(a)
13
+ a.add_child(b)
14
+
15
+ self.assertIs(a.parent, root)
16
+ self.assertIs(b.parent, a)
17
+
18
+ self.assertEqual(root.full_name, "root")
19
+ self.assertEqual(a.full_name, "root.a")
20
+ self.assertEqual(b.full_name, "root.a.b")
21
+
22
+ def test_len_and_iter_direct_children(self) -> None:
23
+ root = TreeModel(name="root")
24
+ a = TreeModel(name="a")
25
+ b = TreeModel(name="b")
26
+ root.add_child(a)
27
+ root.add_child(b)
28
+
29
+ self.assertEqual(len(root), 2)
30
+ self.assertEqual(list(root), [a, b])
31
+
32
+ def test_exists_non_recursive_and_recursive(self) -> None:
33
+ root = TreeModel(name="root")
34
+ a = TreeModel(name="a")
35
+ b = TreeModel(name="b")
36
+
37
+ root.add_child(a)
38
+ a.add_child(b)
39
+
40
+ self.assertTrue(root.exists_child(lambda n: n.name == "a"))
41
+ self.assertFalse(root.exists_child(lambda n: n.name == "b"))
42
+ self.assertTrue(root.exists_child(lambda n: n.name == "b", recursive=True))
43
+ self.assertFalse(root.exists_child(lambda n: n.name == "missing", recursive=True))
44
+
45
+ def test_find_by_name(self) -> None:
46
+ root = TreeModel(name="root")
47
+ a = TreeModel(name="a")
48
+ b = TreeModel(name="b")
49
+ root.add_child(a)
50
+ a.add_child(b)
51
+
52
+ self.assertIs(root.find_child_by_name("a"), a)
53
+ self.assertIsNone(root.find_child_by_name("b"))
54
+ self.assertIs(root.find_child_by_name("b", recursive=True), b)
55
+
56
+ def test_get_raises_when_missing(self) -> None:
57
+ root = TreeModel(name="root")
58
+
59
+ with self.assertRaises(ValueError) as ctx:
60
+ root.get_child(lambda n: n.name == "missing")
61
+ self.assertIn("未找到", str(ctx.exception))
62
+
63
+ def test_get_recursive(self) -> None:
64
+ root = TreeModel(name="root")
65
+ a = TreeModel(name="a")
66
+ b = TreeModel(name="b")
67
+ root.add_child(a)
68
+ a.add_child(b)
69
+
70
+ self.assertIs(root.get_child(lambda n: n.name == "a"), a)
71
+ self.assertIs(root.get_child(lambda n: n.name == "b", recursive=True), b)
72
+
73
+ def test_find_returns_none_when_missing(self) -> None:
74
+ root = TreeModel(name="root")
75
+ a = TreeModel(name="a")
76
+ root.add_child(a)
77
+
78
+ self.assertIsNone(root.find_child_by_name("missing"))
79
+ self.assertIsNone(root.find_child_by_name("missing", recursive=True))
80
+
81
+ def test_filter_shallow_and_recursive(self) -> None:
82
+ root = TreeModel(name="root")
83
+ a = TreeModel(name="a")
84
+ b = TreeModel(name="b")
85
+ c = TreeModel(name="b")
86
+ root.add_child(a)
87
+ a.add_child(b)
88
+ root.add_child(c)
89
+
90
+ self.assertEqual(root.filter_child(lambda n: n.name == "b"), [c])
91
+ self.assertEqual(root.filter_child(lambda n: n.name == "b", recursive=True), [b, c])
92
+
93
+ def test_delete_child_removes_from_parent(self) -> None:
94
+ root = TreeModel(name="root")
95
+ child = TreeModel(name="child")
96
+
97
+ root.add_child(child)
98
+ self.assertTrue(root.exists_child(lambda n: n.name == "child"))
99
+
100
+ child.delete()
101
+
102
+ self.assertFalse(root.exists_child(lambda n: n.name == "child"))
103
+ self.assertIsNone(child.parent)
104
+ self.assertEqual(len(root), 0)
105
+
106
+ def test_delete_parent_deletes_descendants(self) -> None:
107
+ root = TreeModel(name="root")
108
+ a = TreeModel(name="a")
109
+ b = TreeModel(name="b")
110
+
111
+ root.add_child(a)
112
+ a.add_child(b)
113
+
114
+ root.delete()
115
+
116
+ self.assertIsNone(root.parent)
117
+ self.assertIsNone(a.parent)
118
+ self.assertIsNone(b.parent)
119
+ self.assertEqual(len(list(root)), 0)
120
+
121
+ def test_add_child_rejects_separator_in_name(self) -> None:
122
+ root = TreeModel(name="root")
123
+ bad = TreeModel(name="a.b")
124
+
125
+ with self.assertRaises(ValueError):
126
+ root.add_child(bad)
127
+
128
+ def test_add_child_rejects_existing_parent(self) -> None:
129
+ root1 = TreeModel(name="root1")
130
+ root2 = TreeModel(name="root2")
131
+ child = TreeModel(name="child")
132
+
133
+ root1.add_child(child)
134
+
135
+ with self.assertRaises(ValueError):
136
+ root2.add_child(child)
137
+
138
+ def test_add_child_rejects_same_child_twice(self) -> None:
139
+ root = TreeModel(name="root")
140
+ child = TreeModel(name="child")
141
+ root.add_child(child)
142
+
143
+ with self.assertRaises(ValueError) as ctx:
144
+ root.add_child(child)
145
+ self.assertIn("parent", str(ctx.exception))
146
+
147
+ def test_on_delete_callback_runs_when_node_deleted(self) -> None:
148
+ root = TreeModel(name="root")
149
+ child = TreeModel(name="child")
150
+ root.add_child(child)
151
+
152
+ called: list[str] = []
153
+
154
+ def mark() -> None:
155
+ called.append("x")
156
+
157
+ child.on_delete(mark)
158
+ child.delete()
159
+
160
+ self.assertEqual(called, ["x"])
161
+ self.assertEqual(len(root), 0)
162
+
163
+
164
+ if __name__ == "__main__":
165
+ unittest.main()
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable, Iterator
4
+ from typing import Optional
5
+
6
+ from pydantic import BaseModel, Field, PrivateAttr
7
+ from reactive_model import ListRefModel
8
+
9
+
10
+ class TreeModel(BaseModel):
11
+ name: str = Field(..., description="节点名称")
12
+ parent: Optional["TreeModel"] = Field(None, description="父节点")
13
+
14
+ _children: ListRefModel["TreeModel"] = PrivateAttr(default_factory=ListRefModel)
15
+ _on_delete: list[Callable[[], None]] = PrivateAttr(default_factory=list)
16
+ _name_separator: str = PrivateAttr(".")
17
+
18
+ @property
19
+ def full_name(self) -> str:
20
+ if self.parent is None:
21
+ return self.name
22
+ return f"{self.parent.full_name}{self._name_separator}{self.name}"
23
+
24
+ def add_child(self, child: "TreeModel") -> None:
25
+ if child.parent is not None:
26
+ raise ValueError(f"child {child.name!r} 已经有 parent")
27
+ if self._name_separator in child.name:
28
+ raise ValueError(f"child {child.name!r} 不允许包含名称分隔符")
29
+ if any(item is child for item in self._children.value):
30
+ raise ValueError("child 已经是子节点")
31
+
32
+ child.parent = self
33
+ self._children.value.append(child)
34
+ child.on_delete(lambda: self._remove_child(child))
35
+
36
+ def _remove_child(self, child: "TreeModel") -> None:
37
+ children = self._children.value
38
+ for index, item in enumerate(children):
39
+ if item is child:
40
+ del children[index]
41
+ break
42
+
43
+ def delete(self) -> None:
44
+ for child in list(self._children.value):
45
+ child.delete()
46
+
47
+ self._children.value.clear()
48
+ self.parent = None
49
+
50
+ for fn in self._on_delete:
51
+ fn()
52
+ self._on_delete.clear()
53
+
54
+ def on_delete(self, callback: Callable[[], None]) -> Callable[[], None]:
55
+ self._on_delete.append(callback)
56
+ return callback
57
+
58
+ def find_child(
59
+ self,
60
+ predicate: Callable[["TreeModel"], bool],
61
+ recursive: bool = False,
62
+ ) -> "TreeModel | None":
63
+ for child in self._children.value:
64
+ if predicate(child):
65
+ return child
66
+ if recursive:
67
+ found = child.find_child(predicate, recursive=True)
68
+ if found is not None:
69
+ return found
70
+ return None
71
+
72
+ def filter_child(
73
+ self,
74
+ predicate: Callable[["TreeModel"], bool],
75
+ recursive: bool = False,
76
+ ) -> list["TreeModel"]:
77
+ result: list[TreeModel] = []
78
+ for child in self._children.value:
79
+ if predicate(child):
80
+ result.append(child)
81
+ if recursive:
82
+ result.extend(child.filter_child(predicate, recursive=True))
83
+ return result
84
+
85
+ def exists_child(
86
+ self,
87
+ predicate: Callable[["TreeModel"], bool],
88
+ recursive: bool = False,
89
+ ) -> bool:
90
+ return self.find_child(predicate, recursive=recursive) is not None
91
+
92
+ def get_child(
93
+ self,
94
+ predicate: Callable[["TreeModel"], bool],
95
+ recursive: bool = False,
96
+ ) -> "TreeModel":
97
+ child = self.find_child(predicate, recursive=recursive)
98
+ if child is None:
99
+ raise ValueError("未找到符合条件的子节点")
100
+ return child
101
+
102
+ def find_child_by_name(self, name: str, recursive: bool = False) -> "TreeModel | None":
103
+ return self.find_child(lambda node: node.name == name, recursive=recursive)
104
+
105
+ def __iter__(self) -> Iterator["TreeModel"]:
106
+ return iter(self._children.value)
107
+
108
+ def __len__(self) -> int:
109
+ return len(self._children.value)