Skip to content

Commit 2e7bd4e

Browse files
authored
Merge pull request #244 from kayjan/modify-search
Modify search
2 parents 4072d96 + c082751 commit 2e7bd4e

File tree

7 files changed

+166
-77
lines changed

7 files changed

+166
-77
lines changed

CHANGELOG.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
## [0.18.2] - 2024-06-01
10+
### Changed:
11+
- Tree Search: Standardize handling of singular and plural search.
12+
- Tree Search: Added `find_relative_path` that return a single node from search and
13+
rename existing `find_relative_path` to `find_relative_paths`.
14+
**This might not be backwards-compatible!**
15+
916
## [0.18.1] - 2024-05-30
1017
### Changed:
1118
- Misc: Remove support of Python 3.7 due to incompatibility with polars.
@@ -572,7 +579,8 @@ ignore null attribute columns.
572579
- Utility Iterator: Tree traversal methods.
573580
- Workflow To Do App: Tree use case with to-do list implementation.
574581

575-
[Unreleased]: https://github.com/kayjan/bigtree/compare/0.18.1...HEAD
582+
[Unreleased]: https://github.com/kayjan/bigtree/compare/0.18.2...HEAD
583+
[0.18.2]: https://github.com/kayjan/bigtree/compare/0.18.1...0.18.2
576584
[0.18.1]: https://github.com/kayjan/bigtree/compare/0.18.0...0.18.1
577585
[0.18.0]: https://github.com/kayjan/bigtree/compare/0.17.2...0.18.0
578586
[0.17.2]: https://github.com/kayjan/bigtree/compare/0.17.1...0.17.2

bigtree/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "0.18.1"
1+
__version__ = "0.18.2"
22

33
from bigtree.binarytree.construct import list_to_binarytree
44
from bigtree.dag.construct import dataframe_to_dag, dict_to_dag, list_to_dag
@@ -63,6 +63,7 @@
6363
find_path,
6464
find_paths,
6565
find_relative_path,
66+
find_relative_paths,
6667
findall,
6768
)
6869
from bigtree.utils.groot import speak_like_groot, whoami

bigtree/tree/search.py

+97-41
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"find_name",
1313
"find_names",
1414
"find_relative_path",
15+
"find_relative_paths",
1516
"find_full_path",
1617
"find_path",
1718
"find_paths",
@@ -28,6 +29,28 @@
2829
DAGNodeT = TypeVar("DAGNodeT", bound=DAGNode)
2930

3031

32+
def __check_result_count(
33+
result: Tuple[Any, ...], min_count: int, max_count: int
34+
) -> None:
35+
"""Check result fulfil min_count and max_count requirements
36+
37+
Args:
38+
result (Tuple[Any]): result of search
39+
min_count (int): checks for minimum number of occurrences,
40+
raise SearchError if the number of results do not meet min_count, defaults to None
41+
max_count (int): checks for maximum number of occurrences,
42+
raise SearchError if the number of results do not meet min_count, defaults to None
43+
"""
44+
if min_count and len(result) < min_count:
45+
raise SearchError(
46+
f"Expected more than or equal to {min_count} element(s), found {len(result)} elements\n{result}"
47+
)
48+
if max_count and len(result) > max_count:
49+
raise SearchError(
50+
f"Expected less than or equal to {max_count} element(s), found {len(result)} elements\n{result}"
51+
)
52+
53+
3154
def findall(
3255
tree: T,
3356
condition: Callable[[T], bool],
@@ -36,7 +59,7 @@ def findall(
3659
max_count: int = 0,
3760
) -> Tuple[T, ...]:
3861
"""
39-
Search tree for nodes matching condition (callable function).
62+
Search tree for one or more nodes matching condition (callable function).
4063
4164
Examples:
4265
>>> from bigtree import Node, findall
@@ -60,20 +83,13 @@ def findall(
6083
(Tuple[BaseNode, ...])
6184
"""
6285
result = tuple(preorder_iter(tree, filter_condition=condition, max_depth=max_depth))
63-
if min_count and len(result) < min_count:
64-
raise SearchError(
65-
f"Expected more than {min_count} element(s), found {len(result)} elements\n{result}"
66-
)
67-
if max_count and len(result) > max_count:
68-
raise SearchError(
69-
f"Expected less than {max_count} element(s), found {len(result)} elements\n{result}"
70-
)
86+
__check_result_count(result, min_count, max_count)
7187
return result
7288

7389

7490
def find(tree: T, condition: Callable[[T], bool], max_depth: int = 0) -> T:
7591
"""
76-
Search tree for *single node* matching condition (callable function).
92+
Search tree for a single node matching condition (callable function).
7793
7894
Examples:
7995
>>> from bigtree import Node, find
@@ -86,7 +102,7 @@ def find(tree: T, condition: Callable[[T], bool], max_depth: int = 0) -> T:
86102
>>> find(root, lambda node: node.age > 5)
87103
Traceback (most recent call last):
88104
...
89-
bigtree.utils.exceptions.SearchError: Expected less than 1 element(s), found 4 elements
105+
bigtree.utils.exceptions.SearchError: Expected less than or equal to 1 element(s), found 4 elements
90106
(Node(/a, age=90), Node(/a/b, age=65), Node(/a/c, age=60), Node(/a/c/d, age=40))
91107
92108
Args:
@@ -104,7 +120,7 @@ def find(tree: T, condition: Callable[[T], bool], max_depth: int = 0) -> T:
104120

105121
def find_name(tree: NodeT, name: str, max_depth: int = 0) -> NodeT:
106122
"""
107-
Search tree for single node matching name attribute.
123+
Search tree for a single node matching name attribute.
108124
109125
Examples:
110126
>>> from bigtree import Node, find_name
@@ -128,7 +144,7 @@ def find_name(tree: NodeT, name: str, max_depth: int = 0) -> NodeT:
128144

129145
def find_names(tree: NodeT, name: str, max_depth: int = 0) -> Iterable[NodeT]:
130146
"""
131-
Search tree for multiple node(s) matching name attribute.
147+
Search tree for one or more nodes matching name attribute.
132148
133149
Examples:
134150
>>> from bigtree import Node, find_names
@@ -152,9 +168,9 @@ def find_names(tree: NodeT, name: str, max_depth: int = 0) -> Iterable[NodeT]:
152168
return findall(tree, lambda node: node.node_name == name, max_depth)
153169

154170

155-
def find_relative_path(tree: NodeT, path_name: str) -> Iterable[NodeT]:
171+
def find_relative_path(tree: NodeT, path_name: str) -> NodeT:
156172
r"""
157-
Search tree for single node matching relative path attribute.
173+
Search tree for a single node matching relative path attribute.
158174
159175
- Supports unix folder expression for relative path, i.e., '../../node_name'
160176
- Supports wildcards, i.e., '\*/node_name'
@@ -167,18 +183,64 @@ def find_relative_path(tree: NodeT, path_name: str) -> Iterable[NodeT]:
167183
>>> c = Node("c", age=60, parent=root)
168184
>>> d = Node("d", age=40, parent=c)
169185
>>> find_relative_path(d, "..")
170-
(Node(/a/c, age=60),)
186+
Node(/a/c, age=60)
171187
>>> find_relative_path(d, "../../b")
172-
(Node(/a/b, age=65),)
188+
Node(/a/b, age=65)
173189
>>> find_relative_path(d, "../../*")
190+
Traceback (most recent call last):
191+
...
192+
bigtree.utils.exceptions.SearchError: Expected less than or equal to 1 element(s), found 2 elements
174193
(Node(/a/b, age=65), Node(/a/c, age=60))
175194
176195
Args:
177196
tree (Node): tree to search
178197
path_name (str): value to match (relative path) of path_name attribute
179198
180199
Returns:
181-
(Iterable[Node])
200+
(Node)
201+
"""
202+
result = find_relative_paths(tree, path_name, max_count=1)
203+
204+
if result:
205+
return result[0]
206+
207+
208+
def find_relative_paths(
209+
tree: NodeT,
210+
path_name: str,
211+
min_count: int = 0,
212+
max_count: int = 0,
213+
) -> Tuple[NodeT, ...]:
214+
r"""
215+
Search tree for one or more nodes matching relative path attribute.
216+
217+
- Supports unix folder expression for relative path, i.e., '../../node_name'
218+
- Supports wildcards, i.e., '\*/node_name'
219+
- If path name starts with leading separator symbol, it will start at root node.
220+
221+
Examples:
222+
>>> from bigtree import Node, find_relative_paths
223+
>>> root = Node("a", age=90)
224+
>>> b = Node("b", age=65, parent=root)
225+
>>> c = Node("c", age=60, parent=root)
226+
>>> d = Node("d", age=40, parent=c)
227+
>>> find_relative_paths(d, "..")
228+
(Node(/a/c, age=60),)
229+
>>> find_relative_paths(d, "../../b")
230+
(Node(/a/b, age=65),)
231+
>>> find_relative_paths(d, "../../*")
232+
(Node(/a/b, age=65), Node(/a/c, age=60))
233+
234+
Args:
235+
tree (Node): tree to search
236+
path_name (str): value to match (relative path) of path_name attribute
237+
min_count (int): checks for minimum number of occurrences,
238+
raise SearchError if the number of results do not meet min_count, defaults to None
239+
max_count (int): checks for maximum number of occurrences,
240+
raise SearchError if the number of results do not meet min_count, defaults to None
241+
242+
Returns:
243+
(Tuple[Node, ...])
182244
"""
183245
sep = tree.sep
184246
if path_name.startswith(sep):
@@ -220,13 +282,14 @@ def resolve(node: NodeT, path_idx: int) -> None:
220282
resolve(node, path_idx + 1)
221283

222284
resolve(tree, 0)
223-
224-
return tuple(resolved_nodes)
285+
result = tuple(resolved_nodes)
286+
__check_result_count(result, min_count, max_count)
287+
return result
225288

226289

227290
def find_full_path(tree: NodeT, path_name: str) -> NodeT:
228291
"""
229-
Search tree for single node matching path attribute.
292+
Search tree for a single node matching path attribute.
230293
231294
- Path name can be with or without leading tree path separator symbol.
232295
- Path name must be full path, works similar to `find_path` but faster.
@@ -265,7 +328,7 @@ def find_full_path(tree: NodeT, path_name: str) -> NodeT:
265328

266329
def find_path(tree: NodeT, path_name: str) -> NodeT:
267330
"""
268-
Search tree for single node matching path attribute.
331+
Search tree for a single node matching path attribute.
269332
270333
- Path name can be with or without leading tree path separator symbol.
271334
- Path name can be full path or partial path (trailing part of path) or node name.
@@ -292,9 +355,9 @@ def find_path(tree: NodeT, path_name: str) -> NodeT:
292355
return find(tree, lambda node: node.path_name.endswith(path_name))
293356

294357

295-
def find_paths(tree: NodeT, path_name: str) -> Tuple[NodeT, ...]:
358+
def find_paths(tree: NodeT, path_name: str) -> Iterable[NodeT]:
296359
"""
297-
Search tree for multiple nodes matching path attribute.
360+
Search tree for one or more nodes matching path attribute.
298361
299362
- Path name can be with or without leading tree path separator symbol.
300363
- Path name can be partial path (trailing part of path) or node name.
@@ -315,7 +378,7 @@ def find_paths(tree: NodeT, path_name: str) -> Tuple[NodeT, ...]:
315378
path_name (str): value to match (full path) or trailing part (partial path) of path_name attribute
316379
317380
Returns:
318-
(Tuple[Node, ...])
381+
(Iterable[Node])
319382
"""
320383
path_name = path_name.rstrip(tree.sep)
321384
return findall(tree, lambda node: node.path_name.endswith(path_name))
@@ -325,7 +388,7 @@ def find_attr(
325388
tree: BaseNode, attr_name: str, attr_value: Any, max_depth: int = 0
326389
) -> BaseNode:
327390
"""
328-
Search tree for single node matching custom attribute.
391+
Search tree for a single node matching custom attribute.
329392
330393
Examples:
331394
>>> from bigtree import Node, find_attr
@@ -354,9 +417,9 @@ def find_attr(
354417

355418
def find_attrs(
356419
tree: BaseNode, attr_name: str, attr_value: Any, max_depth: int = 0
357-
) -> Tuple[BaseNode, ...]:
420+
) -> Iterable[BaseNode]:
358421
"""
359-
Search tree for node(s) matching custom attribute.
422+
Search tree for one or more nodes matching custom attribute.
360423
361424
Examples:
362425
>>> from bigtree import Node, find_attrs
@@ -374,7 +437,7 @@ def find_attrs(
374437
max_depth (int): maximum depth to search for, based on the `depth` attribute, defaults to None
375438
376439
Returns:
377-
(Tuple[BaseNode, ...])
440+
(Iterable[BaseNode])
378441
"""
379442
return findall(
380443
tree,
@@ -390,7 +453,7 @@ def find_children(
390453
max_count: int = 0,
391454
) -> Tuple[Union[T, DAGNodeT], ...]:
392455
"""
393-
Search children for nodes matching condition (callable function).
456+
Search children for one or more nodes matching condition (callable function).
394457
395458
Examples:
396459
>>> from bigtree import Node, find_children
@@ -410,17 +473,10 @@ def find_children(
410473
raise SearchError if the number of results do not meet min_count, defaults to None
411474
412475
Returns:
413-
(BaseNode/DAGNode)
476+
(Tuple[Union[BaseNode, DAGNode], ...])
414477
"""
415478
result = tuple([node for node in tree.children if node and condition(node)])
416-
if min_count and len(result) < min_count:
417-
raise SearchError(
418-
f"Expected more than {min_count} element(s), found {len(result)} elements\n{result}"
419-
)
420-
if max_count and len(result) > max_count:
421-
raise SearchError(
422-
f"Expected less than {max_count} element(s), found {len(result)} elements\n{result}"
423-
)
479+
__check_result_count(result, min_count, max_count)
424480
return result
425481

426482

@@ -429,7 +485,7 @@ def find_child(
429485
condition: Callable[[Union[T, DAGNodeT]], bool],
430486
) -> Union[T, DAGNodeT]:
431487
"""
432-
Search children for *single node* matching condition (callable function).
488+
Search children for a single node matching condition (callable function).
433489
434490
Examples:
435491
>>> from bigtree import Node, find_child
@@ -456,7 +512,7 @@ def find_child_by_name(
456512
tree: Union[NodeT, DAGNodeT], name: str
457513
) -> Union[NodeT, DAGNodeT]:
458514
"""
459-
Search tree for single node matching name attribute.
515+
Search tree for a single node matching name attribute.
460516
461517
Examples:
462518
>>> from bigtree import Node, find_child_by_name

docs/bigtree/tree/search.md

+6-6
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ title: Tree Search
66

77
Search methods for Trees.
88

9-
| Search by | One node | One or more nodes |
10-
|---------------------|-----------------------------------------------------|------------------------------------|
11-
| General method | `find`, `find_child` | `findall`, `find_children` |
12-
| Node name | `find_name`, `find_child_by_name` | `find_names` |
13-
| Node path | `find_path`, `find_full_path`, `find_relative_path` | `find_paths`, `find_relative_path` |
14-
| Node attributes | `find_attr` | `find_attrs` |
9+
| Search by | One node | One or more nodes |
10+
|---------------------|-----------------------------------------------------|-------------------------------------|
11+
| General method | `find`, `find_child` | `findall`, `find_children` |
12+
| Node name | `find_name`, `find_child_by_name` | `find_names` |
13+
| Node path | `find_path`, `find_full_path`, `find_relative_path` | `find_paths`, `find_relative_paths` |
14+
| Node attributes | `find_attr` | `find_attrs` |
1515

1616
-----
1717

docs/demo/tree.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -729,7 +729,7 @@ it does not require traversing the whole tree to find the node(s).
729729
# Node(/a/c/d, age=40)
730730

731731
find_relative_path(c, "../b") # relative path
732-
# (Node(/a/b, age=65),)
732+
# Node(/a/b, age=65)
733733

734734
find_path(root, "/c/d") # partial path
735735
# Node(/a/c/d, age=40)
@@ -743,7 +743,7 @@ it does not require traversing the whole tree to find the node(s).
743743

744744
=== "Find multiple nodes"
745745
```python hl_lines="12 15 18 21 24"
746-
from bigtree import Node, findall, find_names, find_relative_path, find_paths, find_attrs
746+
from bigtree import Node, findall, find_names, find_relative_paths, find_paths, find_attrs
747747
root = Node("a", age=90)
748748
b = Node("b", age=65, parent=root)
749749
c = Node("c", age=60, parent=root)
@@ -760,7 +760,7 @@ it does not require traversing the whole tree to find the node(s).
760760
find_names(root, "c")
761761
# (Node(/a/c, age=60), Node(/a/c/c, age=40))
762762

763-
find_relative_path(c, "../*") # relative path
763+
find_relative_paths(c, "../*") # relative path
764764
# (Node(/a/b, age=65), Node(/a/c, age=60))
765765

766766
find_paths(root, "/c") # partial path

tests/test_constants.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,12 @@ class Constants:
212212
"Path {path_name} does not match the root node name {root_name}"
213213
)
214214

215-
ERROR_SEARCH_LESS_THAN_N_ELEMENT = "Expected less than {count} element(s), found "
216-
ERROR_SEARCH_MORE_THAN_N_ELEMENT = "Expected more than {count} element(s), found "
215+
ERROR_SEARCH_LESS_THAN_N_ELEMENT = (
216+
"Expected less than or equal to {count} element(s), found "
217+
)
218+
ERROR_SEARCH_MORE_THAN_N_ELEMENT = (
219+
"Expected more than or equal to {count} element(s), found "
220+
)
217221

218222
# workflow/todo
219223
ERROR_WORKFLOW_TODO_TYPE = "Invalid data type for item"

0 commit comments

Comments
 (0)