Skip to content

Commit 75a54fc

Browse files
committed
Add node.up()
1 parent f68d747 commit 75a54fc

File tree

6 files changed

+198
-54
lines changed

6 files changed

+198
-54
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# Changelog
22

3-
## 0.9.1 (unreleased)
3+
## 0.10.0 (unreleased)
44

55
- Passes pyright 'basic' checks.
66
- tree.to_rdf() is now available for Tree (not only TypedTree)
7+
- New method `node.up()` allows method chaining when adding nodes.
78

89
## 0.9.0 (2024-09-12)
910

docs/jupyter/tutorial.ipynb renamed to docs/jupyter/walkthrough.ipynb

Lines changed: 110 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@
55
"cell_type": "markdown",
66
"metadata": {},
77
"source": [
8-
"# Nutree Tutorial"
8+
"# Nutree Overview"
99
]
1010
},
1111
{
1212
"cell_type": "markdown",
1313
"metadata": {},
1414
"source": [
1515
"Nutree organizes arbitrary object instances in an unobtrusive way. <br>\n",
16-
"That means, we can add existing objects without having to derrive from a common \n",
17-
"base class or having them implement a specific protocol."
16+
"That means, we can add existing objects without having to derive from a common \n",
17+
"base class or implement a specific protocol."
1818
]
1919
},
2020
{
@@ -30,7 +30,7 @@
3030
},
3131
{
3232
"cell_type": "code",
33-
"execution_count": 10,
33+
"execution_count": 2,
3434
"metadata": {},
3535
"outputs": [],
3636
"source": [
@@ -65,7 +65,7 @@
6565
},
6666
{
6767
"cell_type": "code",
68-
"execution_count": 11,
68+
"execution_count": 3,
6969
"metadata": {},
7070
"outputs": [],
7171
"source": [
@@ -93,21 +93,21 @@
9393
},
9494
{
9595
"cell_type": "code",
96-
"execution_count": 12,
96+
"execution_count": 4,
9797
"metadata": {},
9898
"outputs": [
9999
{
100100
"name": "stdout",
101101
"output_type": "stream",
102102
"text": [
103103
"Tree<'Organization'>\n",
104-
"├── <__main__.Department object at 0x106e6e690>\n",
105-
"│ ├── <__main__.Department object at 0x106e6d640>\n",
106-
"│ │ ╰── <__main__.Person object at 0x106e6e990>\n",
107-
"│ ╰── <__main__.Person object at 0x10683b200>\n",
108-
"├── <__main__.Department object at 0x106e6d7f0>\n",
109-
"│ ╰── <__main__.Person object at 0x106e6c6b0>\n",
110-
"╰── <__main__.Person object at 0x106e6e660>\n"
104+
"├── <__main__.Department object at 0x1072ef050>\n",
105+
"│ ├── <__main__.Department object at 0x1072bd9a0>\n",
106+
"│ │ ╰── <__main__.Person object at 0x1074dd790>\n",
107+
"│ ╰── <__main__.Person object at 0x1072ef770>\n",
108+
"├── <__main__.Department object at 0x1072eed50>\n",
109+
"│ ╰── <__main__.Person object at 0x1074dc320>\n",
110+
"╰── <__main__.Person object at 0x1072ef500>\n"
111111
]
112112
}
113113
],
@@ -135,12 +135,12 @@
135135
"Tree nodes store a reference to the object in the `node.data` attribute.\n",
136136
"\n",
137137
"The nodes are formatted by the object's `__repr__` implementation by default. <br>\n",
138-
"We can overide ths by passing an f-string as `repr` argument:"
138+
"We can overide this by passing an f-string as `repr` argument:"
139139
]
140140
},
141141
{
142142
"cell_type": "code",
143-
"execution_count": 13,
143+
"execution_count": 5,
144144
"metadata": {},
145145
"outputs": [
146146
{
@@ -162,6 +162,90 @@
162162
"tree.print(repr=\"{node.data}\")"
163163
]
164164
},
165+
{
166+
"cell_type": "markdown",
167+
"metadata": {},
168+
"source": []
169+
},
170+
{
171+
"cell_type": "markdown",
172+
"metadata": {},
173+
"source": [
174+
"## Special Data Types\n",
175+
"\n",
176+
"### Plain Strings\n",
177+
"\n",
178+
"We can add simple string objects the same way as any other object\n",
179+
"(Note also, how we make use of code chaining in this example):"
180+
]
181+
},
182+
{
183+
"cell_type": "code",
184+
"execution_count": 13,
185+
"metadata": {},
186+
"outputs": [
187+
{
188+
"name": "stdout",
189+
"output_type": "stream",
190+
"text": [
191+
"Tree<'4578652576'>\n",
192+
"╰── 'A'\n",
193+
" ├── 'a1'\n",
194+
" ╰── 'a2'\n",
195+
" ╰── 'B'\n"
196+
]
197+
}
198+
],
199+
"source": [
200+
"tree = Tree()\n",
201+
"tree.add(\"A\").add(\"a1\").up().add(\"a2\").up(-1).add(\"B\")\n",
202+
"tree.print()"
203+
]
204+
},
205+
{
206+
"cell_type": "code",
207+
"execution_count": 11,
208+
"metadata": {},
209+
"outputs": [
210+
{
211+
"name": "stdout",
212+
"output_type": "stream",
213+
"text": [
214+
"Tree<'4572026816'>\n",
215+
"├── 'A'\n",
216+
"│ ├── 'a1'\n",
217+
"│ ╰── 'a2'\n",
218+
"╰── 'B'\n"
219+
]
220+
}
221+
],
222+
"source": [
223+
"Tree().add(\"A\").add(\"a1\").up().add(\"a2\").up(2).add(\"B\").tree.print()"
224+
]
225+
},
226+
{
227+
"cell_type": "code",
228+
"execution_count": 7,
229+
"metadata": {},
230+
"outputs": [
231+
{
232+
"name": "stdout",
233+
"output_type": "stream",
234+
"text": [
235+
"Tree<'4571953776'>\n",
236+
"├── 'A'\n",
237+
"│ ╰── 'C'\n",
238+
"│ ╰── 'E'\n",
239+
"├── 'B'\n",
240+
"╰── 'D'\n"
241+
]
242+
}
243+
],
244+
"source": [
245+
"t = Tree._from_list([(0, \"A\"), (0, \"B\"), (1, \"C\"), (0, \"D\"), (3, \"E\")])\n",
246+
"print(t.format())"
247+
]
248+
},
165249
{
166250
"cell_type": "markdown",
167251
"metadata": {},
@@ -172,18 +256,20 @@
172256
},
173257
{
174258
"cell_type": "code",
175-
"execution_count": 25,
259+
"execution_count": 8,
176260
"metadata": {},
177261
"outputs": [
178262
{
179-
"data": {
180-
"text/plain": [
181-
"Node<'Person<Alice (25)>', data_id=275672678>"
182-
]
183-
},
184-
"execution_count": 25,
185-
"metadata": {},
186-
"output_type": "execute_result"
263+
"ename": "KeyError",
264+
"evalue": "'<__main__.Person object at 0x1072ef500>'",
265+
"output_type": "error",
266+
"traceback": [
267+
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
268+
"\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)",
269+
"Cell \u001b[0;32mIn[8], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mtree\u001b[49m\u001b[43m[\u001b[49m\u001b[43malice\u001b[49m\u001b[43m]\u001b[49m\n",
270+
"File \u001b[0;32m~/prj/git/nutree/nutree/tree.py:167\u001b[0m, in \u001b[0;36mTree.__getitem__\u001b[0;34m(self, data)\u001b[0m\n\u001b[1;32m 164\u001b[0m res \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mfind_all(data)\n\u001b[1;32m 166\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m res:\n\u001b[0;32m--> 167\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mdata\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 168\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(res) \u001b[38;5;241m>\u001b[39m \u001b[38;5;241m1\u001b[39m:\n\u001b[1;32m 169\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m AmbiguousMatchError(\n\u001b[1;32m 170\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mdata\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[38;5;124m has \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mlen\u001b[39m(res)\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m occurrences. \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 171\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mUse tree.find_all() or tree.find_first() to resolve this.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 172\u001b[0m )\n",
271+
"\u001b[0;31mKeyError\u001b[0m: '<__main__.Person object at 0x1072ef500>'"
272+
]
187273
}
188274
],
189275
"source": [
@@ -333,29 +419,6 @@
333419
"outputs": [],
334420
"source": []
335421
},
336-
{
337-
"cell_type": "code",
338-
"execution_count": 20,
339-
"metadata": {},
340-
"outputs": [
341-
{
342-
"name": "stdout",
343-
"output_type": "stream",
344-
"text": [
345-
"Tree<'4410903680'>\n",
346-
"├── 'A'\n",
347-
"│ ╰── 'C'\n",
348-
"│ ╰── 'E'\n",
349-
"├── 'B'\n",
350-
"╰── 'D'\n"
351-
]
352-
}
353-
],
354-
"source": [
355-
"t = Tree._from_list([(0, \"A\"), (0, \"B\"), (1, \"C\"), (0, \"D\"), (3, \"E\")])\n",
356-
"print(t.format())"
357-
]
358-
},
359422
{
360423
"cell_type": "code",
361424
"execution_count": null,

docs/sphinx/ug_basics.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,29 @@ Nodes are usually created by adding a new data instance to a parent::
3939
╰── 'Books'
4040
╰── 'The Little Prince'
4141

42+
Chaining
43+
~~~~~~~~
44+
45+
Since `node.add()` return a Node object we can chain calls.
46+
The `node.up()` method allows to select an ancestor node and the `node.tree`
47+
return the Tree instance::
48+
49+
tree = Tree()
50+
tree.add("A").add("a1").up().add("a2").up(2).add("B")
51+
tree.print()
52+
53+
::
54+
55+
Tree<>
56+
├── 'A'
57+
│ ├── 'a1'
58+
│ ╰── 'a2'
59+
╰── 'B'
60+
61+
or for friends of code golf::
62+
63+
Tree().add("A").add("a1").up().add("a2").up(2).add("B").tree.print()
64+
4265
.. seealso::
4366

4467
See :doc:`ug_objects` for details on how to manage arbitrary objects, dicts,

nutree/node.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,10 +189,33 @@ def tree(self) -> Tree:
189189

190190
@property
191191
def parent(self) -> Node | None:
192-
"""Return parent node or None for toplevel nodes."""
192+
"""Return parent node or None for toplevel nodes.
193+
194+
See also :meth:`~nutree.node.Node.up`.
195+
"""
193196
p = self._parent
194197
return p if p._parent else None
195198

199+
def up(self, level: int = 1) -> Node:
200+
"""Return ancestor node.
201+
202+
Unlike :meth:`~nutree.node.Node.parent`, this method returns the
203+
system root node for toplevel nodes.
204+
205+
One use case is method chaining when creating trees::
206+
207+
tree = Tree().add("n1").add("child1").up().add("child2").up(2).add("n2")
208+
"""
209+
if level < 1:
210+
raise ValueError("Level must be positive")
211+
p = self
212+
while level > 0:
213+
p = p._parent
214+
if p is None:
215+
raise ValueError("Cannot go up beyond system root node")
216+
level -= 1
217+
return p
218+
196219
@property
197220
def children(self) -> list[Node]:
198221
"""Return list of direct child nodes (list may be empty)."""
@@ -827,7 +850,7 @@ def copy_to(
827850
return target.add_child(self, before=before, deep=deep)
828851
assert before is None
829852
if not self._children:
830-
raise ValueError("need child nodes when `add_self=False`")
853+
raise ValueError("Need child nodes when `add_self=False`")
831854
res = None
832855
for child in self.children:
833856
n = target.add_child(child, before=None, deep=deep)
@@ -917,7 +940,7 @@ def filtered(self, predicate: PredicateCallbackType) -> Tree:
917940
See also :ref:`iteration-callbacks`.
918941
"""
919942
if not predicate:
920-
raise ValueError("predicate is required (use copy() instead)")
943+
raise ValueError("Predicate is required (use copy() instead)")
921944
return self.copy(add_self=True, predicate=predicate)
922945

923946
def filter(self, predicate: PredicateCallbackType) -> None:
@@ -926,7 +949,7 @@ def filter(self, predicate: PredicateCallbackType) -> None:
926949
See also :ref:`iteration-callbacks`.
927950
"""
928951
if not predicate:
929-
raise ValueError("predicate is required (use copy() instead)")
952+
raise ValueError("Predicate is required (use copy() instead)")
930953

931954
def _visit(parent: Node) -> bool:
932955
"""Return True if any descendant returned True."""

nutree/tree.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ def filtered(self, predicate: PredicateCallbackType) -> Tree:
430430
See also :ref:`iteration-callbacks`.
431431
"""
432432
if not predicate:
433-
raise ValueError("predicate is required (use copy() instead)")
433+
raise ValueError("Predicate is required (use copy() instead)")
434434
return self.copy(predicate=predicate)
435435

436436
def clear(self) -> None:

tests/test_core.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,40 @@ def test_add_child(self):
6868
""",
6969
)
7070

71+
def test_chain(self):
72+
tree = Tree("fixture")
73+
74+
tree.add("A").add("a1").add("a11").up().add("a12").up(2).add("a2").up(2).add(
75+
"B"
76+
)
77+
assert fixture.check_content(
78+
tree,
79+
"""
80+
Tree<'fixture'>
81+
+- A
82+
| +- a1
83+
| | +- a11
84+
| | `- a12
85+
| `- a2
86+
`- B
87+
""",
88+
)
89+
90+
a11 = tree.find("a11")
91+
assert a11.up().name == "a1"
92+
assert a11.up(1).name == "a1"
93+
assert a11.up(2).name == "A"
94+
assert a11.up(3).is_system_root()
95+
96+
with pytest.raises(ValueError, match="Cannot go up beyond system root node"):
97+
a11.up(4)
98+
with pytest.raises(ValueError, match="Cannot go up beyond system root node"):
99+
a11.up(5)
100+
with pytest.raises(ValueError, match="Level must be positive"):
101+
a11.up(0)
102+
with pytest.raises(ValueError, match="Level must be positive"):
103+
a11.up(-1)
104+
71105
def test_meta(self):
72106
tree = fixture.create_tree()
73107
node = tree.first_child()
@@ -1209,7 +1243,7 @@ def pred(node):
12091243
)
12101244

12111245
# Should use tree.copy() instead:
1212-
with pytest.raises(ValueError, match="predicate is required"):
1246+
with pytest.raises(ValueError, match="Predicate is required"):
12131247
tree_2 = tree.filtered(predicate=None) # type: ignore
12141248

12151249
tree_2 = tree.copy()

0 commit comments

Comments
 (0)