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.
- python_library_tree_model-0.1.0/.gitignore +11 -0
- python_library_tree_model-0.1.0/PKG-INFO +6 -0
- python_library_tree_model-0.1.0/pyproject.toml +18 -0
- python_library_tree_model-0.1.0/test.bat +10 -0
- python_library_tree_model-0.1.0/tests/test_tree_node.py +165 -0
- python_library_tree_model-0.1.0/tree_model/__init__.py +109 -0
|
@@ -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,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)
|