Skip to content

Commit 7076be2

Browse files
authored
Merge pull request #164 from kayjan/enhance-dagnode
Enhance dagnode
2 parents 3b2b3d9 + cd566b1 commit 7076be2

File tree

7 files changed

+202
-23
lines changed

7 files changed

+202
-23
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Added
9+
- DAGNode: Able to access and delete node children via name with square bracket accessor with `__getitem__` and `__delitem__` magic methods.
10+
- DAGNode: Able to delete all children for a node.
11+
- DAGNode: Able to check if node contains child node with `__contains__` magic method.
12+
- DAGNode: Able to iterate the node to access children with `__iter__` magic method.
13+
### Changed
14+
- Tree Search: Modify type hints to include DAGNode for `find_children`, `find_child`, and `find_child_by_name`.
15+
- Misc: Neater handling of strings for tests.
816

917
## [0.15.5] - 2023-01-17
1018
### Changed

README.md

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -555,7 +555,7 @@ Below are the tables of attributes available to `BaseNode` and `Node` classes.
555555
| Get parent | `node_e.parent` | Node(/a/b, ) |
556556
| Get siblings | `node_e.siblings` | (Node(/a/b/d, ), Node(/a/b/f, )) |
557557
| Get left sibling | `node_e.left_sibling` | Node(/a/b/d, ) |
558-
| Get right sibling | `node_e.left_sibling` | Node(/a/b/f, ) |
558+
| Get right sibling | `node_e.right_sibling` | Node(/a/b/f, ) |
559559
| Get ancestors (lazy evaluation) | `list(node_e.ancestors)` | [Node(/a/b, ), Node(/a, )] |
560560
| Get descendants (lazy evaluation) | `list(node_b.descendants)` | [Node(/a/b/d, ), Node(/a/b/e, ), Node(/a/b/f, ), Node(/a/b/f/h, ), Node(/a/b/f/i, )] |
561561
| Get leaves (lazy evaluation) | `list(node_b.leaves)` | [Node(/a/b/d, ), Node(/a/b/e, ), Node(/a/b/f/h, ), Node(/a/b/f/i, )] |
@@ -1151,8 +1151,10 @@ Compared to nodes in tree, nodes in DAG are able to have multiple parents.
11511151

11521152
1. **From `DAGNode`**
11531153

1154-
DAGNode can be linked to each other with `parents` and `children` setter methods,
1155-
or using bitshift operator with the convention `parent_node >> child_node` or `child_node << parent_node`.
1154+
DAGNodes can be linked to each other in the following ways:
1155+
- Using `parents` and `children` setter methods
1156+
- Directly passing `parents` or `children` argument
1157+
- Using bitshift operator with the convention `parent_node >> child_node` or `child_node << parent_node`
11561158

11571159
{emphasize-lines="5-8,10"}
11581160
```python
@@ -1238,6 +1240,55 @@ print([(parent.node_name, child.node_name) for parent, child in dag_iterator(dag
12381240
# [('a', 'd'), ('c', 'd'), ('d', 'e'), ('a', 'c'), ('b', 'c')]
12391241
```
12401242

1243+
### DAG Attributes and Operations
1244+
1245+
Note that using `DAGNode` as superclass inherits the default class attributes (properties) and operations (methods).
1246+
1247+
```python
1248+
from bigtree import list_to_dag
1249+
1250+
relations_list = [
1251+
("a", "c"),
1252+
("a", "d"),
1253+
("b", "c"),
1254+
("c", "d"),
1255+
("d", "e")
1256+
]
1257+
dag = list_to_dag(relations_list)
1258+
dag
1259+
# DAGNode(d, )
1260+
1261+
# Accessing children
1262+
node_e = dag["e"]
1263+
node_a = dag.parents[0]
1264+
```
1265+
1266+
Below are the tables of attributes available to `DAGNode` class.
1267+
1268+
| Attributes wrt self | Code | Returns |
1269+
|--------------------------------------|------------------|---------|
1270+
| Check if root | `node_a.is_root` | True |
1271+
| Check if leaf node | `dag.is_leaf` | False |
1272+
| Get node name (only for `Node`) | `dag.node_name` | 'd' |
1273+
1274+
| Attributes wrt structure | Code | Returns |
1275+
|------------------------------|-----------------------|----------------------------------------------------------------------|
1276+
| Get child/children | `node_a.children` | (DAGNode(c, ), DAGNode(d, )) |
1277+
| Get parents | `dag.parents` | (DAGNode(a, ), DAGNode(c, )) |
1278+
| Get siblings | `dag.siblings` | (DAGNode(c, ),) |
1279+
| Get ancestors | `dag.ancestors` | [DAGNode(a, ), DAGNode(b, ), DAGNode(c, )] |
1280+
| Get descendants | `dag.descendants` | [DAGNode(e, )] |
1281+
1282+
Below is the table of operations available to `DAGNode` class.
1283+
1284+
| Operations | Code | Returns |
1285+
|---------------------------------------|------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------|
1286+
| Get node information | `dag.describe(exclude_prefix="_")` | [('name', 'd')] |
1287+
| Find path(s) from one node to another | `node_a.go_to(dag)` | [[DAGNode(a, ), DAGNode(c, ), DAGNode(d, description=dag-tag)], [DAGNode(a, ), DAGNode(d, description=dag-tag)]] |
1288+
| Set attribute(s) | `dag.set_attrs({"description": "dag-tag"})` | None |
1289+
| Get attribute | `dag.get_attr("description")` | 'dag-tag' |
1290+
| Copy DAG | `dag.copy()` | None |
1291+
12411292
----
12421293

12431294
## Demo Usage

bigtree/node/dagnode.py

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
import copy
4-
from typing import Any, Dict, Iterable, List, Optional, Tuple, TypeVar
4+
from typing import Any, Dict, Generator, Iterable, List, Optional, Tuple, TypeVar
55

66
from bigtree.utils.exceptions import LoopError, TreeError
77
from bigtree.utils.iterators import preorder_iter
@@ -319,6 +319,13 @@ def children(self: T, new_children: Iterable[T]) -> None:
319319
self.__children.remove(new_child)
320320
raise TreeError(exc_info)
321321

322+
@children.deleter
323+
def children(self) -> None:
324+
"""Delete child node(s)"""
325+
for child in self.children:
326+
self.__children.remove(child) # type: ignore
327+
child.__parents.remove(self) # type: ignore
328+
322329
def __pre_assign_children(self: T, new_children: Iterable[T]) -> None:
323330
"""Custom method to check before attaching children
324331
Can be overridden with `_DAGNode__pre_assign_children()`
@@ -553,6 +560,32 @@ def __copy__(self: T) -> T:
553560
obj.__dict__.update(self.__dict__)
554561
return obj
555562

563+
def __getitem__(self, child_name: str) -> T:
564+
"""Get child by name identifier
565+
566+
Args:
567+
child_name (str): name of child node
568+
569+
Returns:
570+
(Self): child node
571+
"""
572+
from bigtree.tree.search import find_child_by_name
573+
574+
return find_child_by_name(self, child_name) # type: ignore
575+
576+
def __delitem__(self, child_name: str) -> None:
577+
"""Delete child by name identifier, will not throw error if child does not exist
578+
579+
Args:
580+
child_name (str): name of child node
581+
"""
582+
from bigtree.tree.search import find_child_by_name
583+
584+
child = find_child_by_name(self, child_name)
585+
if child:
586+
self.__children.remove(child) # type: ignore
587+
child.__parents.remove(self) # type: ignore
588+
556589
def __repr__(self) -> str:
557590
"""Print format of DAGNode
558591
@@ -567,20 +600,39 @@ def __repr__(self) -> str:
567600
return f"{class_name}({self.node_name}, {node_description})"
568601

569602
def __rshift__(self: T, other: T) -> None:
570-
"""Set children using >> bitshift operator for self >> other
603+
"""Set children using >> bitshift operator for self >> children (other)
571604
572605
Args:
573606
other (Self): other node, children
574607
"""
575608
other.parents = [self]
576609

577610
def __lshift__(self: T, other: T) -> None:
578-
"""Set parent using << bitshift operator for self << other
611+
"""Set parent using << bitshift operator for self << parent (other)
579612
580613
Args:
581614
other (Self): other node, parent
582615
"""
583616
self.parents = [other]
584617

618+
def __iter__(self) -> Generator[T, None, None]:
619+
"""Iterate through child nodes
620+
621+
Returns:
622+
(Self): child node
623+
"""
624+
yield from self.children # type: ignore
625+
626+
def __contains__(self, other_node: T) -> bool:
627+
"""Check if child node exists
628+
629+
Args:
630+
other_node (T): child node
631+
632+
Returns:
633+
(bool)
634+
"""
635+
return other_node in self.children
636+
585637

586638
T = TypeVar("T", bound=DAGNode)

bigtree/tree/search.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from typing import Any, Callable, Iterable, List, Tuple, TypeVar
1+
from typing import Any, Callable, Iterable, List, Tuple, TypeVar, Union
22

33
from bigtree.node.basenode import BaseNode
4+
from bigtree.node.dagnode import DAGNode
45
from bigtree.node.node import Node
56
from bigtree.utils.exceptions import SearchError
67
from bigtree.utils.iterators import preorder_iter
@@ -24,6 +25,7 @@
2425

2526
T = TypeVar("T", bound=BaseNode)
2627
NodeT = TypeVar("NodeT", bound=Node)
28+
DAGNodeT = TypeVar("DAGNodeT", bound=DAGNode)
2729

2830

2931
def findall(
@@ -368,11 +370,11 @@ def find_attrs(
368370

369371

370372
def find_children(
371-
tree: T,
372-
condition: Callable[[T], bool],
373+
tree: Union[T, DAGNodeT],
374+
condition: Callable[[Union[T, DAGNodeT]], bool],
373375
min_count: int = 0,
374376
max_count: int = 0,
375-
) -> Tuple[T, ...]:
377+
) -> Tuple[Union[T, DAGNodeT], ...]:
376378
"""
377379
Search children for nodes matching condition (callable function).
378380
@@ -385,15 +387,15 @@ def find_children(
385387
(Node(/a/b, age=65), Node(/a/c, age=60))
386388
387389
Args:
388-
tree (BaseNode): tree to search for its children
390+
tree (BaseNode/DAGNode): tree to search for its children
389391
condition (Callable): function that takes in node as argument, returns node if condition evaluates to `True`
390392
min_count (int): checks for minimum number of occurrences,
391393
raise SearchError if the number of results do not meet min_count, defaults to None
392394
max_count (int): checks for maximum number of occurrences,
393395
raise SearchError if the number of results do not meet min_count, defaults to None
394396
395397
Returns:
396-
(BaseNode)
398+
(BaseNode/DAGNode)
397399
"""
398400
result = tuple([node for node in tree.children if node and condition(node)])
399401
if min_count and len(result) < min_count:
@@ -408,9 +410,9 @@ def find_children(
408410

409411

410412
def find_child(
411-
tree: T,
412-
condition: Callable[[T], bool],
413-
) -> T:
413+
tree: Union[T, DAGNodeT],
414+
condition: Callable[[Union[T, DAGNodeT]], bool],
415+
) -> Union[T, DAGNodeT]:
414416
"""
415417
Search children for *single node* matching condition (callable function).
416418
@@ -423,18 +425,20 @@ def find_child(
423425
Node(/a/b, age=65)
424426
425427
Args:
426-
tree (BaseNode): tree to search for its child
428+
tree (BaseNode/DAGNode): tree to search for its child
427429
condition (Callable): function that takes in node as argument, returns node if condition evaluates to `True`
428430
429431
Returns:
430-
(BaseNode)
432+
(BaseNode/DAGNode)
431433
"""
432434
result = find_children(tree, condition, max_count=1)
433435
if result:
434436
return result[0]
435437

436438

437-
def find_child_by_name(tree: NodeT, name: str) -> NodeT:
439+
def find_child_by_name(
440+
tree: Union[NodeT, DAGNodeT], name: str
441+
) -> Union[NodeT, DAGNodeT]:
438442
"""
439443
Search tree for single node matching name attribute.
440444
@@ -449,10 +453,10 @@ def find_child_by_name(tree: NodeT, name: str) -> NodeT:
449453
Node(/a/c/d, age=40)
450454
451455
Args:
452-
tree (Node): tree to search, parent node
456+
tree (Node/DAGNode): tree to search, parent node
453457
name (str): value to match for name attribute, child node
454458
455459
Returns:
456-
(Node)
460+
(Node/DAGNode)
457461
"""
458462
return find_child(tree, lambda node: node.node_name == name)

tests/binarytree/test_helper.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,16 @@ class TestCloneTree:
99
def test_clone_tree(binarytree_node):
1010
root_clone = clone_tree(binarytree_node, node_type=Node)
1111
assert isinstance(root_clone, Node), "Wrong type returned"
12-
expected_str = """1\n├── 2\n│ ├── 4\n│ │ └── 8\n│ └── 5\n└── 3\n ├── 6\n └── 7\n"""
12+
expected_str = (
13+
"1\n"
14+
"├── 2\n"
15+
"│ ├── 4\n"
16+
"│ │ └── 8\n"
17+
"│ └── 5\n"
18+
"└── 3\n"
19+
" ├── 6\n"
20+
" └── 7\n"
21+
)
1322
assert_print_statement(print_tree, expected_str, tree=binarytree_node)
1423

1524

@@ -87,6 +96,10 @@ def test_tree_diff(binarytree_node):
8796
other_tree_node = prune_tree(binarytree_node, "1/3")
8897
tree_only_diff = get_tree_diff(binarytree_node, other_tree_node, only_diff=True)
8998
expected_str = (
90-
"""1\n└── 2 (-)\n ├── 4 (-)\n │ └── 8 (-)\n └── 5 (-)\n"""
99+
"1\n"
100+
"└── 2 (-)\n"
101+
" ├── 4 (-)\n"
102+
" │ └── 8 (-)\n"
103+
" └── 5 (-)\n"
91104
)
92105
assert_print_statement(print_tree, expected_str, tree=tree_only_diff)

tests/node/test_dagnode.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -951,6 +951,56 @@ def assert_dag_structure_self(self):
951951
actual == expected
952952
), f"Node attribute should be {expected}, but it is {actual}"
953953

954+
# Test accessing with square bracket (__getitem__)
955+
assert (
956+
self.a["c"] == self.c
957+
), f"Accessor method not returning correct for node {self.a} for {self.c}"
958+
assert (
959+
self.a["d"] == self.d
960+
), f"Accessor method not returning correct for node {self.a} for {self.d}"
961+
assert (
962+
self.a["c"]["d"] == self.d
963+
), f"Accessor method not returning correct for node {self.a} for {self.d}"
964+
965+
# Test deletion of children with square bracket (__delitem__)
966+
assert len(self.a.children) == 2, f"Before deletion: error in {self.a.children}"
967+
del self.a["d"]
968+
assert len(self.a.children) == 1, f"After deletion: error in {self.a.children}"
969+
self.a >> self.d
970+
assert (
971+
len(self.a.children) == 2
972+
), f"Revert after deletion: error in {self.a.children}"
973+
974+
# Test deletion of non-existent children with square bracket (__delitem__)
975+
assert len(self.a.children) == 2, f"Before deletion: error in {self.a.children}"
976+
del self.a["b"]
977+
assert len(self.a.children) == 2, f"After deletion: error in {self.a.children}"
978+
979+
# Test deletion of all children
980+
assert len(self.a.children) == 2, f"Before deletion: error in {self.a.children}"
981+
assert self.a in self.c.parents, f"Before deletion: error in {self.c.parents}"
982+
assert self.a in self.d.parents, f"Before deletion: error in {self.d.parents}"
983+
del self.a.children
984+
assert len(self.a.children) == 0, f"After deletion: error in {self.a.children}"
985+
assert self.a not in self.c.parents, f"Before deletion: error in {self.c.parents}"
986+
assert self.a not in self.d.parents, f"Before deletion: error in {self.d.parents}"
987+
self.a >> self.c
988+
self.a >> self.d
989+
assert (
990+
len(self.a.children) == 2
991+
), f"Revert after deletion: error in {self.a.children}"
992+
993+
# Test iteration (__iter__)
994+
expected = [self.c, self.d]
995+
actual = [child for child in self.a]
996+
assert (
997+
actual == expected
998+
), f"Node {self.a} should have {expected} children when iterated, but it has {actual}"
999+
1000+
# Test contains (__contains__)
1001+
assert self.c in self.a, f"Check if {self.a} contains {self.c}, expected True"
1002+
assert self.b not in self.a, f"Check if {self.a} contains {self.b}, expected False"
1003+
9541004

9551005
def assert_dag_child_attr(dag, parent_name, child_name, child_attr, child_value):
9561006
"""Test tree attributes"""

tests/node/test_node.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ def assert_tree_structure_node_self(self):
422422
actual == expected
423423
), f"Node should have path {expected}, but path is {actual}"
424424

425-
# Test show
425+
# Test show()
426426
expected_str = (
427427
"a\n"
428428
"├── b\n"
@@ -454,6 +454,7 @@ def assert_tree_structure_node_self(self):
454454
style="ansi",
455455
)
456456

457+
# Test hshow()
457458
expected_str = (
458459
" ┌─ d\n"
459460
" ┌─ b ─┤ ┌─ g\n"

0 commit comments

Comments
 (0)