diff --git a/spyder/plugins/variableexplorer/widgets/collectionsdelegate.py b/spyder/plugins/variableexplorer/widgets/collectionsdelegate.py index e336b5e3f47..b7658a3bac5 100644 --- a/spyder/plugins/variableexplorer/widgets/collectionsdelegate.py +++ b/spyder/plugins/variableexplorer/widgets/collectionsdelegate.py @@ -461,8 +461,11 @@ def updateEditorGeometry(self, editor, option, index): class ToggleColumnDelegate(CollectionsDelegate): """ToggleColumn Item Delegate""" - def __init__(self, parent=None, namespacebrowser=None): - CollectionsDelegate.__init__(self, parent, namespacebrowser) + + def __init__(self, parent=None, namespacebrowser=None, + data_function: Optional[Callable[[], Any]] = None): + CollectionsDelegate.__init__( + self, parent, namespacebrowser, data_function) self.current_index = None self.old_obj = None @@ -482,6 +485,49 @@ def set_value(self, index, value): if index.isValid(): index.model().set_value(index, value) + def make_data_function(self, index: QModelIndex + ) -> Optional[Callable[[], Any]]: + """ + Construct function which returns current value of data. + + This is used to refresh editors created from this piece of data. + For instance, if `self` is the delegate for an editor displays the + object `obj` and the user opens another editor for `obj.xxx.yyy`, + then to refresh the data of the second editor, the nested function + `datafun` first gets the refreshed data for `obj` and then gets the + `xxx` attribute and then the `yyy` attribute. + + Parameters + ---------- + index : QModelIndex + Index of item whose current value is to be returned by the + function constructed here. + + Returns + ------- + Optional[Callable[[], Any]] + Function which returns the current value of the data, or None if + such a function cannot be constructed. + """ + if self.data_function is None: + return None + + obj_path = index.model().get_key(index).obj_path + path_elements = obj_path.split('.') + del path_elements[0] # first entry is variable name + + def datafun(): + data = self.data_function() + try: + for attribute_name in path_elements: + data = getattr(data, attribute_name) + return data + except (NotImplementedError, AttributeError, + TypeError, ValueError): + return None + + return datafun + def createEditor(self, parent, option, index): """Overriding method createEditor""" if self.show_warning(index): @@ -518,7 +564,8 @@ def createEditor(self, parent, option, index): if isinstance(value, (list, set, tuple, dict)): from spyder.widgets.collectionseditor import CollectionsEditor editor = CollectionsEditor( - parent=parent, namespacebrowser=self.namespacebrowser) + parent=parent, namespacebrowser=self.namespacebrowser, + data_function=self.make_data_function(index)) editor.setup(value, key, icon=self.parent().windowIcon(), readonly=readonly) self.create_dialog(editor, dict(model=index.model(), editor=editor, @@ -527,7 +574,8 @@ def createEditor(self, parent, option, index): # ArrayEditor for a Numpy array elif (isinstance(value, (np.ndarray, np.ma.MaskedArray)) and np.ndarray is not FakeObject): - editor = ArrayEditor(parent=parent) + editor = ArrayEditor( + parent=parent, data_function=self.make_data_function(index)) if not editor.setup_and_check(value, title=key, readonly=readonly): return self.create_dialog(editor, dict(model=index.model(), editor=editor, @@ -548,7 +596,8 @@ def createEditor(self, parent, option, index): # DataFrameEditor for a pandas dataframe, series or index elif (isinstance(value, (pd.DataFrame, pd.Index, pd.Series)) and pd.DataFrame is not FakeObject): - editor = DataFrameEditor(parent=parent) + editor = DataFrameEditor( + parent=parent, data_function=self.make_data_function(index)) if not editor.setup_and_check(value, title=key): return self.create_dialog(editor, dict(model=index.model(), editor=editor, diff --git a/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py b/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py index 9ff03e94ccd..8f89dd49d34 100644 --- a/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py +++ b/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py @@ -150,7 +150,8 @@ def set_value(self, obj): # Tree widget old_obj_tree = self.obj_tree - self.obj_tree = ToggleColumnTreeView(self.namespacebrowser) + self.obj_tree = ToggleColumnTreeView( + self.namespacebrowser, self.data_function) self.obj_tree.setAlternatingRowColors(True) self.obj_tree.setModel(self._proxy_tree_model) self.obj_tree.setSelectionBehavior(QAbstractItemView.SelectRows) diff --git a/spyder/plugins/variableexplorer/widgets/objectexplorer/tests/test_objectexplorer.py b/spyder/plugins/variableexplorer/widgets/objectexplorer/tests/test_objectexplorer.py index f431aa0cb45..a5da4b9b7a7 100644 --- a/spyder/plugins/variableexplorer/widgets/objectexplorer/tests/test_objectexplorer.py +++ b/spyder/plugins/variableexplorer/widgets/objectexplorer/tests/test_objectexplorer.py @@ -230,5 +230,53 @@ def datafunc(): mock_critical.assert_called_once() +@dataclass +class Box: + contents: object + + +def test_objectexplorer_refresh_nested(): + """ + Open an editor for an `Box` object containing a list, and then open another + editor for the nested list. Test that refreshing the second editor works. + """ + old_data = Box([1, 2, 3]) + new_data = Box([4, 5]) + editor = ObjectExplorer( + old_data, name='data', data_function=lambda: new_data) + model = editor.obj_tree.model() + root_index = model.index(0, 0) + contents_index = model.index(0, 0, root_index) + editor.obj_tree.edit(contents_index) + delegate = editor.obj_tree.delegate + nested_editor = list(delegate._editors.values())[0]['editor'] + assert nested_editor.get_value() == [1, 2, 3] + nested_editor.widget.refresh_action.trigger() + assert nested_editor.get_value() == [4, 5] + + +def test_objectexplorer_refresh_doubly_nested(): + """ + Open an editor for an `Box` object containing another `Box` object which + in turn contains a list. Then open a second editor for the nested list. + Test that refreshing the second editor works. + """ + old_data = Box(Box([1, 2, 3])) + new_data = Box(Box([4, 5])) + editor = ObjectExplorer( + old_data, name='data', data_function=lambda: new_data) + model = editor.obj_tree.model() + root_index = model.index(0, 0) + inner_box_index = model.index(0, 0, root_index) + editor.obj_tree.expand(inner_box_index) + contents_index = model.index(0, 0, inner_box_index) + editor.obj_tree.edit(contents_index) + delegate = editor.obj_tree.delegate + nested_editor = list(delegate._editors.values())[0]['editor'] + assert nested_editor.get_value() == [1, 2, 3] + nested_editor.widget.refresh_action.trigger() + assert nested_editor.get_value() == [4, 5] + + if __name__ == "__main__": pytest.main() diff --git a/spyder/plugins/variableexplorer/widgets/objectexplorer/toggle_column_mixin.py b/spyder/plugins/variableexplorer/widgets/objectexplorer/toggle_column_mixin.py index b2071f2af3f..197e13735aa 100644 --- a/spyder/plugins/variableexplorer/widgets/objectexplorer/toggle_column_mixin.py +++ b/spyder/plugins/variableexplorer/widgets/objectexplorer/toggle_column_mixin.py @@ -10,9 +10,10 @@ # Standard library imports import logging +from typing import Any, Callable, Optional # Third-party imports -from qtpy.QtCore import Qt, Signal, Slot +from qtpy.QtCore import Qt, Slot from qtpy.QtWidgets import (QAbstractItemView, QAction, QActionGroup, QHeaderView, QTableWidget, QTreeView, QTreeWidget) @@ -155,12 +156,15 @@ class ToggleColumnTreeView(QTreeView, ToggleColumnMixIn): show/hide columns. """ - def __init__(self, namespacebrowser=None, readonly=False): + def __init__(self, namespacebrowser=None, + data_function : Optional[Callable[[], Any]] = None, + readonly=False): QTreeView.__init__(self) self.readonly = readonly from spyder.plugins.variableexplorer.widgets.collectionsdelegate \ import ToggleColumnDelegate - self.delegate = ToggleColumnDelegate(self, namespacebrowser) + self.delegate = ToggleColumnDelegate( + self, namespacebrowser, data_function) self.setItemDelegate(self.delegate) self.setEditTriggers(QAbstractItemView.DoubleClicked) self.expanded.connect(self.resize_columns_to_contents)