Skip to content

Commit

Permalink
Fix children losing track of a different parent on removal.
Browse files Browse the repository at this point in the history
If we have already t_new.add_child(node) a node to a new tree, and
we remove it from its old one (t_old.remove_child(node) or t_old.pop_node(i)),
we do want the child to still remember it belongs to a different tree!
  • Loading branch information
jordibc committed Oct 12, 2023
1 parent 7eebab4 commit bb326d1
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 4 deletions.
14 changes: 10 additions & 4 deletions ete4/coretype/tree.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -393,8 +393,11 @@ cdef class Tree(object):

def pop_child(self, child_idx=-1):
try:
child = self.children.pop(child_idx)
child.up = None
child = self.children.pop(child_idx) # parent removes child

if child.up == self: # (it may point to another already!)
child.up = None # child removes parent

return child
except ValueError as e:
raise TreeError(f'Cannot pop child: not found ({e})')
Expand All @@ -406,8 +409,11 @@ cdef class Tree(object):
but are no longer connected.
"""
try:
self.children.remove(child) # parent will not know about child
child.up = None # child will not know about parent
self.children.remove(child) # parent removes child

if child.up == self: # (it may point to another already!)
child.up = None # child removes parent

return child
except ValueError as e:
raise TreeError(f'Cannot remove child: not found ({e})')
Expand Down
58 changes: 58 additions & 0 deletions tests/test_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,64 @@ def test_tree_manipulation(self):
self.assertEqual(set([n.name for n in t1.traverse()]),
set(['A', 'B', 'C', 'I', 'root']))

def test_remove_child(self):
"""Test removing children."""
t = Tree()
t.populate(20)

# Removed node loses its parent.
node1 = t[1]
self.assertEqual(node1.up, t) # no surprise here

node1_returned = t.remove_child(t[1])
self.assertEqual(node1_returned, node1)
self.assertEqual(node1.up, None) # node1 lost its parent

t.add_child(node1) # ok, let's put it back

# A node that was already "stolen" does not lose its parent.
node0 = t[0] # first child from t, node that we will be moving around

t2 = Tree()
t2.add_child(node0) # "steals" the first child from t

self.assertEqual(t[0], t2[0]) # they should be the same node

t.remove_child(node0)
self.assertTrue(t[0] != node0) # node is no longer a child of t

self.assertEqual(node0.up, t2) # but has not lost track of its parent
self.assertEqual(t2[0], node0)

def test_pop_child(self):
"""Test popping children."""
t = Tree()
t.populate(20)

# Removed node loses its parent.
node1 = t[1]
self.assertEqual(node1.up, t) # no surprise here

node1_returned = t.pop_child()
self.assertEqual(node1_returned, node1)
self.assertEqual(node1.up, None) # node1 lost its parent

t.add_child(node1) # ok, let's put it back

# A node that was already "stolen" does not lose its parent.
node0 = t[0] # first child from t, node that we will be moving around

t2 = Tree()
t2.add_child(node0) # "steals" the first child from t

self.assertEqual(t[0], t2[0]) # they should be the same node

t.pop_child(0)
self.assertTrue(t[0] != node0) # node is no longer a child of t

self.assertEqual(node0.up, t2) # but has not lost track of its parent
self.assertEqual(t2[0], node0)

def test_pruning(self):
# test prune preserving distances
for i in range(3): # NOTE: each iteration is quite slow
Expand Down

0 comments on commit bb326d1

Please sign in to comment.