Skip to content

Commit f5270ce

Browse files
authored
Add tests for studio (#459)
* fix: query for empty attributes * test: add test for common assemble_attributes and studio * feat: add path information to textual node to allow search function * fix: handle edit display * fix: shorter example context * test: add test for studio * fix: fix data attribute for rename action + more tests * test: add test for studio * docs: update CHANGELOG
1 parent ad76304 commit f5270ce

5 files changed

Lines changed: 386 additions & 32 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
### Added:
99
- Search: Search by name/names to allow regex match.
1010
- Studio: Enhance search to be by name, regex, or attribute query.
11+
- Test: Test for studio.
1112
### Fixed:
1213
- Query: Query method for inequality to cover cases where attribute is not present.
1314

bigtree/tree/studio/utils.py

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
json = MagicMock()
2020

2121
try:
22-
from textual.widgets import Static
22+
from textual.widgets import Static, Tree
2323
from textual.widgets._tree import TreeNode
2424

2525
except ImportError: # pragma: no cover
@@ -50,7 +50,7 @@
5050
[cyan]query:age >= 30 OR node_name LIKE ".*e"[/] Advanced query"""
5151

5252

53-
def _get_textual_node_path(textual_node: TreeNode, sep: str = "/") -> str:
53+
def _get_textual_node_path(textual_node: TreeNode) -> list[str]:
5454
"""Get node path of textual TreeNode, reads the node name recursively until root node.
5555
5656
Args:
@@ -65,7 +65,7 @@ def _get_textual_node_path(textual_node: TreeNode, sep: str = "/") -> str:
6565
while parent:
6666
path.append(str(parent.label))
6767
parent = parent.parent
68-
return sep.join(path[::-1])
68+
return path[::-1]
6969

7070

7171
def _get_corresponding_bt_node(bt_tree: tree.Tree, textual_node: TreeNode) -> node.Node:
@@ -79,7 +79,9 @@ def _get_corresponding_bt_node(bt_tree: tree.Tree, textual_node: TreeNode) -> no
7979
bigtree tree node
8080
"""
8181
node_path = _get_textual_node_path(textual_node)
82-
return bt_tree.find_full_path(node_path) # type: ignore[attr-defined, no-any-return]
82+
for next_path in node_path[1:]:
83+
bt_tree = bt_tree[next_path]
84+
return bt_tree.node
8385

8486

8587
def _get_corresponding_textual_nodes(
@@ -122,9 +124,21 @@ def _get_attr_bt_node(
122124
return common.assemble_attributes(bt_node, **kwargs)
123125

124126

127+
def _assemble_data(bt_node: node.Node) -> dict[str, str]:
128+
"""Assemble data for textual node from bigtree node
129+
130+
Args:
131+
bt_node: bigtree node
132+
133+
Returns:
134+
data dictionary
135+
"""
136+
return {"path_name": bt_node.path_name}
137+
138+
125139
def populate_textual_tree(
126140
bt_tree: tree.Tree,
127-
textual_tree: TreeNode,
141+
textual_tree: Tree,
128142
max_depth: int = 2,
129143
) -> None:
130144
"""Populate textual tree with bigtree tree.
@@ -135,21 +149,18 @@ def populate_textual_tree(
135149
max_depth: maximum depth of tree to expand
136150
"""
137151

138-
def assemble_data(bt_node: node.Node) -> dict[str, str]:
139-
return {"path_name": bt_node.path_name}
140-
141152
def add(bt_parent: node.Node, textual_parent: TreeNode, depth: int = 1) -> None:
142153
# Depth + 1 because we are calculating the depth of children
143154
expand = True if depth + 1 < max_depth else False
144155
for bt_child in bt_parent.children:
145156
textual_child = textual_parent.add(
146-
bt_child.node_name, data=assemble_data(bt_child), expand=expand
157+
bt_child.node_name, data=_assemble_data(bt_child), expand=expand
147158
)
148159
add(bt_child, textual_child, depth + 1)
149160

150161
textual_tree.clear()
151162
textual_tree.root.label = bt_tree.node.name
152-
textual_tree.root.data = assemble_data(bt_tree.node)
163+
textual_tree.root.data = _assemble_data(bt_tree.node)
153164
textual_tree.root.expand()
154165
add(bt_tree.node, textual_tree.root)
155166

@@ -191,7 +202,7 @@ def expand_parents(textual_node: TreeNode) -> None:
191202

192203

193204
def action_add_node(
194-
bt_tree: tree.Tree, textual_node: TreeNode, value: str | None
205+
bt_tree: tree.Tree, textual_node: TreeNode | None, value: str | None
195206
) -> None:
196207
"""Add new child node to existing node, implements for bigtree tree and textual tree in-place.
197208
@@ -203,13 +214,13 @@ def action_add_node(
203214
if not value or not textual_node:
204215
return
205216
bt_node = _get_corresponding_bt_node(bt_tree, textual_node)
206-
node.Node(value, parent=bt_node)
207-
textual_node.add(value)
217+
bt_child = node.Node(value, parent=bt_node)
218+
textual_node.add(value, data=_assemble_data(bt_child))
208219
textual_node.expand()
209220

210221

211222
def action_add_sibling(
212-
bt_tree: tree.Tree, textual_node: TreeNode, value: str | None
223+
bt_tree: tree.Tree, textual_node: TreeNode | None, value: str | None
213224
) -> None:
214225
"""Add new sibling node to existing node, implements for bigtree tree and textual tree in-place.
215226
@@ -218,16 +229,16 @@ def action_add_sibling(
218229
textual_node: textual tree node
219230
value: name of node to add
220231
"""
221-
if not textual_node.parent:
222-
raise ValueError("Cannot add sibling for root node.")
223232
if not value or not textual_node:
224233
return
234+
if not textual_node.parent:
235+
raise ValueError("Cannot add sibling for root node.")
225236
bt_node = _get_corresponding_bt_node(bt_tree, textual_node)
226-
node.Node(value, parent=bt_node.parent)
227-
textual_node.parent.add(value)
237+
bt_child = node.Node(value, parent=bt_node.parent)
238+
textual_node.parent.add(value, data=_assemble_data(bt_child))
228239

229240

230-
def action_delete_node(bt_tree: tree.Tree, textual_node: TreeNode) -> None:
241+
def action_delete_node(bt_tree: tree.Tree, textual_node: TreeNode | None) -> None:
231242
"""Delete node, implements for bigtree tree and textual tree in-place.
232243
233244
Args:
@@ -256,6 +267,7 @@ def action_rename_node(
256267
bt_node = _get_corresponding_bt_node(bt_tree, textual_node)
257268
bt_node.rename(value)
258269
textual_node.set_label(value)
270+
textual_node.data = _assemble_data(bt_node)
259271

260272

261273
def select_edit_attr(bt_tree: tree.Tree, textual_node: TreeNode) -> str:
@@ -273,9 +285,9 @@ def select_edit_attr(bt_tree: tree.Tree, textual_node: TreeNode) -> str:
273285

274286

275287
def action_edit_attr(
276-
bt_tree: tree.Tree, textual_node: TreeNode, value: str | None
288+
bt_tree: tree.Tree, textual_node: TreeNode | None, value: str | None
277289
) -> None:
278-
"""Delete node, implements for bigtree tree and textual tree in-place.
290+
"""Delete node, implements for bigtree tree in-place. Textual tree data only maintains path_name
279291
280292
Args:
281293
bt_tree: bigtree tree
@@ -284,18 +296,21 @@ def action_edit_attr(
284296
"""
285297
if not textual_node or not value:
286298
return
287-
kv_pairs = [kv.strip().split("=") for kv in value.split(",")]
288-
kv_pairs_eval = [(k, ast.literal_eval(v)) for k, v in kv_pairs]
289299
try:
300+
kv_pairs = [kv.strip().split("=") for kv in value.split(",")]
301+
kv_pairs_eval = [(k, ast.literal_eval(v)) for k, v in kv_pairs]
290302
new_attrs = dict(kv_pairs_eval)
291-
except ValueError as err:
303+
except (ValueError, SyntaxError) as err:
292304
raise ValueError(f"Input malformed, check `{value}`") from err
293305
bt_node = _get_corresponding_bt_node(bt_tree, textual_node)
294306
existing_attrs = _get_attr_bt_node(bt_tree, textual_node)
295307
attrs_to_remove = set(existing_attrs) - set(new_attrs)
296308
for attr_to_remove in attrs_to_remove:
297309
del bt_node.__dict__[attr_to_remove]
298310
bt_node.set_attrs(new_attrs)
311+
# Sync textual_node to bt_node
312+
textual_node.label = bt_node.name
313+
textual_node.data = _assemble_data(bt_node)
299314

300315

301316
def action_search(
@@ -319,7 +334,7 @@ def action_search(
319334
return _get_corresponding_textual_nodes(textual_node, bt_matches_path)
320335

321336

322-
def action_save_as(bt_tree: tree.Tree, value: str | None) -> None:
337+
def action_save_as(bt_tree: tree.Tree, value: str | None) -> None: # pragma: no cover
323338
"""Save tree as json file.
324339
325340
Args:

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,9 @@ omit = [
149149
"*/tests/*",
150150
"*/workflows/*",
151151
"bigtree/tree/construct/render.py",
152-
"bigtree/tree/studio/*"
152+
"bigtree/tree/studio/app.py",
153+
"bigtree/tree/studio/details.py",
154+
"bigtree/tree/studio/prompt.py",
153155
]
154156

155157
[tool.coverage.report]

tests/test_constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,10 @@ class Constants:
174174
)
175175
ERROR_NODE_NEWICK_ATTR_INVALID = "Length attribute does not exist for node "
176176

177+
# tree/studio
178+
ERROR_STUDIO_ADD_SIBLING = "Cannot add sibling for root node."
179+
ERROR_STUDIO_EDIT_ATTR = "Input malformed, check "
180+
177181
# tree/helper
178182
ERROR_NODE_PRUNE_ARGUMENT = (
179183
"Please specify either `prune_path` or `max_depth` or both."

0 commit comments

Comments
 (0)