Skip to content

Commit 9e39d16

Browse files
committed
Adds dependency injection to check for root status of nodes
1 parent 13467e7 commit 9e39d16

File tree

4 files changed

+40
-12
lines changed

4 files changed

+40
-12
lines changed

README.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,10 @@ tree = ascii_tree.renderable_from_parents(
169169
)
170170
print(ascii_tree.render(tree[0]))
171171
```
172-
Root-level nodes in your hierarchy *must* return `None` from either `parent_attr` or `parent_method`.
172+
By default, nodes are considered to be "root-level" if they return `None` from either `parent_attr` or `parent_method`.
173+
174+
If your system handles root status differently, you can pass in a callback function,
175+
`is_root_callback`, to `renderable_from_parents` to override this behavior. `is_root_callback` takes a single argument, the node in question, and should return `True` if the node is a root node, and `False` otherwise.
173176

174177
Note that `renderable_from_parents` always **returns a list** -- this is to ensure that
175178
data with multiple root nodes are supported.
@@ -180,6 +183,7 @@ root
180183
└─ child
181184
└─ grandchild
182185
```
186+
183187
## Rendering Styles
184188

185189
`ascii-tree` currently provides three styles of rendering:

noxfile.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import nox
22

33

4-
@nox.session
4+
@nox.session(python=["3.7", "3.8", "3.9", "3.10", "3.11"])
55
def tests(session):
66
session.install(".")
77
session.run("pytest", "tests")

src/ascii_tree/__init__.py

+10-9
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ def renderable_from_parents(
7575
objs: t.Sequence[t.Any],
7676
parent_attr: t.Optional[str] = None,
7777
parent_method: t.Optional[t.Callable] = None,
78+
is_root_callback: t.Optional[t.Callable[[t.Any], bool]] = None,
7879
display_attr: t.Optional[str] = None,
7980
display_method: t.Optional[t.Callable] = None,
8081
) -> t.List[TextRenderNode]:
@@ -89,9 +90,9 @@ def renderable_from_parents(
8990
are multiple roots in your given sequence.
9091
"""
9192

92-
roots = []
93-
9493
def get_parent(obj):
94+
if is_root_callback and is_root_callback(ancestor):
95+
return None
9596
if parent_attr:
9697
return getattr(obj, parent_attr)
9798
elif parent_method:
@@ -107,7 +108,7 @@ def make_node(obj):
107108
children_method=lambda: list(),
108109
)
109110

110-
node_dict = dict()
111+
node_dict: t.Dict[TextRenderNode, TextRenderNode] = dict()
111112
roots = set()
112113
for obj in objs:
113114
ancestor = obj
@@ -145,18 +146,18 @@ def renderable_dir_tree(
145146
"""Create a TextRenderNode tree from a given file system path.
146147
147148
Args:
148-
path (str): The path to the directory to build the tree from.
149-
recursive (bool, optional): Whether to build the tree recursively.
149+
path: The path to the directory to build the tree from.
150+
recursive: Whether to build the tree recursively.
150151
Defaults to True.
151-
max_depth (Optional[int], optional): The maximum depth to build the tree
152+
max_depth: The maximum depth to build the tree
152153
to. If not specified, the tree will be built to its full depth. Defaults
153154
to None.
154-
slash_after_dir (bool, optional): Whether to add a forward slash after
155+
slash_after_dir: Whether to add a forward slash after
155156
directories in the tree. Defaults to True.
156-
ellipsis_after_max_depth (bool, optional): Whether to add an ellipsis
157+
ellipsis_after_max_depth: Whether to add an ellipsis
157158
after the last directory in a path that reaches the maximum depth.
158159
Defaults to True.
159-
skip_if_no_permission (bool, optional): Whether to skip adding a node to
160+
skip_if_no_permission: Whether to skip adding a node to
160161
the tree if permission is denied to access it. Defaults to True.
161162
162163

tests/test_interface.py

+24-1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,15 @@ def __init__(self, name, parent=None):
6666
self.name = name
6767
self.parent = parent
6868

69+
class DummyWithDifferentParentSentinel:
70+
# This tests the ability to use a callback to determine if a node is root
71+
def __init__(self, name, parent=None):
72+
self.name = name
73+
self.parent = parent or 5
74+
75+
def is_root(self):
76+
return self.parent == 5
77+
6978
def test_single_root(self):
7079
obj1 = self.DummyObject("Root")
7180
obj2 = self.DummyObject("Child", parent=obj1)
@@ -88,9 +97,23 @@ def test_multiple_roots(self):
8897
display_attr="name",
8998
)
9099
assert len(nodes) == 2
91-
assert sorted([node.display for node in nodes]) == ["Root1", "Root2"]
100+
nodes = sorted(nodes, key=lambda node: node.display)
101+
assert [node.display for node in nodes] == ["Root1", "Root2"]
92102
assert nodes[1].children[0].display == "Child"
93103

104+
def test_root_callback(self):
105+
obj = self.DummyWithDifferentParentSentinel("Root")
106+
obj2 = self.DummyWithDifferentParentSentinel("Child", parent=obj)
107+
nodes = renderable_from_parents(
108+
[obj2],
109+
parent_attr="parent",
110+
display_attr="name",
111+
is_root_callback=(lambda node: node.is_root()),
112+
)
113+
assert len(nodes) == 1
114+
assert nodes[0].display == "Root"
115+
assert nodes[0].children[0].display == "Child"
116+
94117

95118
if __name__ == "__main__":
96119
pytest.main()

0 commit comments

Comments
 (0)