Skip to content

Commit

Permalink
Implement refresh of editors opened from object explorer
Browse files Browse the repository at this point in the history
* Pass the `data_function` from the object explorer, which can retrieve
  new data for the object being edited, via `ToggleColumnTreeView`
  to `ToggleColumnDelegate`.
* Add `ToggleColumnDelegate.make_data_function()` for retrieving new data
  for editors opened from the object explorer.
* Pass the result of the above function to editors opened from the object
  explorer.
* Add tests for new functionality.
  • Loading branch information
jitseniesen committed Oct 31, 2023
1 parent 028e7af commit 00b8dab
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 9 deletions.
59 changes: 54 additions & 5 deletions spyder/plugins/variableexplorer/widgets/collectionsdelegate.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,8 +462,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

Expand All @@ -483,6 +486,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):
Expand Down Expand Up @@ -519,7 +565,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,
Expand All @@ -528,7 +575,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,
Expand All @@ -549,7 +597,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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 00b8dab

Please sign in to comment.