From 7415aed8c39e772c0b54dbcc28aecd6efd45bbec Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Fri, 10 May 2019 10:36:11 +0100 Subject: [PATCH 1/5] Fix missing _listener_cache trait. --- traitsui/tree_node.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/traitsui/tree_node.py b/traitsui/tree_node.py index 08f16a925..5ca797841 100644 --- a/traitsui/tree_node.py +++ b/traitsui/tree_node.py @@ -1742,6 +1742,9 @@ class TreeNodeObject(HasPrivateTraits): """ Represents the object that corresponds to a tree node. """ + #: A cache for listeners that need to keep state. + _listener_cache = Dict + #------------------------------------------------------------------------- # Returns whether chidren of this object are allowed or not: #------------------------------------------------------------------------- From b6971380240659d77eeea7781456080d2fbcb09c Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Fri, 10 May 2019 11:03:06 +0100 Subject: [PATCH 2/5] Correct use of node rather than object. --- traitsui/tree_node.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/traitsui/tree_node.py b/traitsui/tree_node.py index 5ca797841..f29138a58 100644 --- a/traitsui/tree_node.py +++ b/traitsui/tree_node.py @@ -1892,11 +1892,11 @@ def tno_when_label_changed(self, node, listener, remove): """ label = node.label if label[:1] != '=': - memo = ('label', label, object, listener) + memo = ('label', label, node, listener) if not remove: def wrapped_listener(target, name, new): """ Ensure listener gets called with correct object. """ - return listener(object, name, new) + return listener(node, name, new) self._listener_cache[memo] = wrapped_listener else: From b79f3c14686d585157f0978729b29b9daeff8440 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Fri, 10 May 2019 11:44:16 +0100 Subject: [PATCH 3/5] Add regression test for #557. --- traitsui/tests/editors/test_tree_editor.py | 79 +++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/traitsui/tests/editors/test_tree_editor.py b/traitsui/tests/editors/test_tree_editor.py index d3777a41d..d3c19bcbe 100644 --- a/traitsui/tests/editors/test_tree_editor.py +++ b/traitsui/tests/editors/test_tree_editor.py @@ -17,7 +17,9 @@ import unittest from traits.api import Bool, HasTraits, Instance, List, Str -from traitsui.api import Item, TreeEditor, TreeNode, View +from traitsui.api import ( + Item, ObjectTreeNode, TreeEditor, TreeNode, TreeNodeObject, View +) from traitsui.tests._tools import ( press_ok_button, skip_if_null, skip_if_not_qt4, @@ -80,6 +82,49 @@ def default_traits_view(self): return traits_view +class BogusTreeNodeObject(TreeNodeObject): + """ A bogus tree node. """ + + name = Str("Bogus") + + bogus_list = List + + wrapped_bogus = Instance(BogusWrap) + + def _wrapped_bogus_default(self): + return BogusTreeNodeObject(bogus_list=[BogusTreeNodeObject()]) + + +class BogusTreeNodeObjectView(HasTraits): + """ A traitsui view visualizing Bogus objects as trees. """ + + bogus = Instance(BogusTreeNodeObject) + + hide_root = Bool + + word_wrap = Bool + + def default_traits_view(self): + tree_editor = TreeEditor( + nodes=[ + ObjectTreeNode( + node_for=[BogusTreeNodeObject], + label='name', + ) + ], + hide_root=self.hide_root, + editable=False, + word_wrap=self.word_wrap, + ) + + traits_view = View( + Item(name='bogus', id='engine', editor=tree_editor), + buttons=['OK'], + ) + + return traits_view + + class TestTreeView(unittest.TestCase): def _test_tree_editor_releases_listeners(self, hide_root, nodes=None, @@ -110,6 +155,32 @@ def _test_tree_editor_releases_listeners(self, hide_root, nodes=None, notifiers_list = bogus.trait(trait)._notifiers(False) self.assertEqual(0, len(notifiers_list)) + def _test_tree_node_object_releases_listeners(self, hide_root, trait='bogus_list', + expected_listeners=1): + """ The TreeEditor should release the listener to the root node's children + when it's disposed of. + """ + + with store_exceptions_on_all_threads(): + bogus = BogusTreeNodeObject(bogus_list=[BogusTreeNodeObject()]) + tree_editor_view = BogusTreeNodeObjectView( + bogus=bogus, + hide_root=hide_root, + ) + ui = tree_editor_view.edit_traits() + + # The TreeEditor sets a listener on the bogus object's + # children list + notifiers_list = bogus.trait(trait)._notifiers(False) + self.assertEqual(expected_listeners, len(notifiers_list)) + + # Manually close the UI + press_ok_button(ui) + + # The listener should be removed after the UI has been closed + notifiers_list = bogus.trait(trait)._notifiers(False) + self.assertEqual(0, len(notifiers_list)) + @skip_if_null def test_tree_editor_listeners_with_shown_root(self): nodes = [ @@ -160,6 +231,12 @@ def test_tree_editor_xgetattr_label_listener(self): expected_listeners=2, ) + @skip_if_null + def test_tree_node_object_label_listener(self): + self._test_tree_node_object_releases_listeners( + hide_root=False, trait='name') + + @skip_if_null def test_smoke_save_restore_prefs(self): bogus = Bogus(bogus_list=[Bogus()]) From 1e1f2252f4bfde061cb28c0c2d04c79dcbca020a Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Fri, 10 May 2019 11:54:53 +0100 Subject: [PATCH 4/5] Clean up and a couple of additional tests. --- traitsui/tests/editors/test_tree_editor.py | 62 ++++++++++++++++------ 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/traitsui/tests/editors/test_tree_editor.py b/traitsui/tests/editors/test_tree_editor.py index d3c19bcbe..96e71db84 100644 --- a/traitsui/tests/editors/test_tree_editor.py +++ b/traitsui/tests/editors/test_tree_editor.py @@ -89,11 +89,6 @@ class BogusTreeNodeObject(TreeNodeObject): bogus_list = List - wrapped_bogus = Instance(BogusWrap) - - def _wrapped_bogus_default(self): - return BogusTreeNodeObject(bogus_list=[BogusTreeNodeObject()]) - class BogusTreeNodeObjectView(HasTraits): """ A traitsui view visualizing Bogus objects as trees. """ @@ -102,19 +97,22 @@ class BogusTreeNodeObjectView(HasTraits): hide_root = Bool - word_wrap = Bool + nodes = List(TreeNode) + + def _nodes_default(self): + return [ + TreeNode( + node_for=[BogusTreeNodeObject], + children='bogus_list', + label='=Bogus' + ) + ] def default_traits_view(self): tree_editor = TreeEditor( - nodes=[ - ObjectTreeNode( - node_for=[BogusTreeNodeObject], - label='name', - ) - ], + nodes=self.nodes, hide_root=self.hide_root, editable=False, - word_wrap=self.word_wrap, ) traits_view = View( @@ -155,7 +153,8 @@ def _test_tree_editor_releases_listeners(self, hide_root, nodes=None, notifiers_list = bogus.trait(trait)._notifiers(False) self.assertEqual(0, len(notifiers_list)) - def _test_tree_node_object_releases_listeners(self, hide_root, trait='bogus_list', + def _test_tree_node_object_releases_listeners(self, hide_root, nodes=None, + trait='bogus_list', expected_listeners=1): """ The TreeEditor should release the listener to the root node's children when it's disposed of. @@ -166,6 +165,7 @@ def _test_tree_node_object_releases_listeners(self, hide_root, trait='bogus_list tree_editor_view = BogusTreeNodeObjectView( bogus=bogus, hide_root=hide_root, + nodes=nodes, ) ui = tree_editor_view.edit_traits() @@ -232,10 +232,40 @@ def test_tree_editor_xgetattr_label_listener(self): ) @skip_if_null - def test_tree_node_object_label_listener(self): + def test_tree_node_object_listeners_with_shown_root(self): + nodes = [ + ObjectTreeNode( + node_for=[BogusTreeNodeObject], + children='bogus_list', + label='=Bogus' + ) + ] self._test_tree_node_object_releases_listeners( - hide_root=False, trait='name') + nodes=nodes, hide_root=False) + @skip_if_null + def test_tree_node_object_listeners_with_hidden_root(self): + nodes = [ + ObjectTreeNode( + node_for=[BogusTreeNodeObject], + children='bogus_list', + label='=Bogus' + ) + ] + self._test_tree_node_object_releases_listeners( + nodes=nodes, hide_root=True) + + @skip_if_null + def test_tree_node_object_label_listener(self): + nodes = [ + ObjectTreeNode( + node_for=[BogusTreeNodeObject], + children='bogus_list', + label='name' + ) + ] + self._test_tree_node_object_releases_listeners( + nodes=nodes, hide_root=False, trait='name') @skip_if_null def test_smoke_save_restore_prefs(self): From eadc94547fc149f93845d03647d2badd93f5d8ad Mon Sep 17 00:00:00 2001 From: Matt Reay Date: Fri, 24 May 2019 10:00:11 -0500 Subject: [PATCH 5/5] Pass correct arguement --- traitsui/tree_node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/traitsui/tree_node.py b/traitsui/tree_node.py index 08f16a925..d2353794e 100644 --- a/traitsui/tree_node.py +++ b/traitsui/tree_node.py @@ -246,7 +246,7 @@ def append_child(self, object, child): def insert_child(self, object, index, child): """ Inserts a child into the object's children. """ - children = self.get_children() + children = self.get_children(object) children[index:index] = [child] #-------------------------------------------------------------------------