Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR: Add Refresh button to editors from Variable Explorer #21312

Merged
merged 16 commits into from
Nov 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
517 changes: 341 additions & 176 deletions spyder/plugins/variableexplorer/widgets/arrayeditor.py

Large diffs are not rendered by default.

143 changes: 131 additions & 12 deletions spyder/plugins/variableexplorer/widgets/collectionsdelegate.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
import datetime
import functools
import operator
from typing import Any, Callable, Optional

# Third party imports
from qtpy.compat import to_qvariant
from qtpy.QtCore import QDateTime, Qt, Signal
from qtpy.QtWidgets import (QAbstractItemDelegate, QDateEdit, QDateTimeEdit,
QItemDelegate, QLineEdit, QMessageBox, QTableView)
from qtpy.QtCore import QDateTime, QModelIndex, Qt, Signal
from qtpy.QtWidgets import (
QAbstractItemDelegate, QDateEdit, QDateTimeEdit, QItemDelegate, QLineEdit,
QMessageBox, QTableView)
from spyder_kernels.utils.lazymodules import (
FakeObject, numpy as np, pandas as pd, PIL)
from spyder_kernels.utils.nsview import (display_to_value, is_editable_type,
Expand All @@ -43,9 +45,15 @@ class CollectionsDelegate(QItemDelegate, SpyderFontsMixin):
sig_editor_creation_started = Signal()
sig_editor_shown = Signal()

def __init__(self, parent=None, namespacebrowser=None):
def __init__(
self,
parent=None,
namespacebrowser=None,
data_function: Optional[Callable[[], Any]] = None
):
QItemDelegate.__init__(self, parent)
self.namespacebrowser = namespacebrowser
self.data_function = data_function
self._editors = {} # keep references on opened editors

def get_value(self, index):
Expand All @@ -56,6 +64,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 that displays
the dict `xxx` and the user opens another editor for `xxx["aaa"]`,
then to refresh the data of the second editor, the nested function
`datafun` first gets the refreshed data for `xxx` and then gets the
item with key "aaa".

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
key = index.model().keys[index.row()]

def datafun():
data = self.data_function()
if isinstance(data, (tuple, list, dict, set)):
return data[key]

try:
jitseniesen marked this conversation as resolved.
Show resolved Hide resolved
return getattr(data, key)
except (NotImplementedError, AttributeError,
TypeError, ValueError):
return None

return datafun

def show_warning(self, index):
"""
Decide if showing a warning when the user is trying to view
Expand Down Expand Up @@ -163,7 +214,10 @@ def createEditor(self, parent, option, index, object_explorer=False):
elif isinstance(value, (list, set, tuple, dict)) and not object_explorer:
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 @@ -174,7 +228,10 @@ def createEditor(self, parent, option, index, object_explorer=False):
np.ndarray is not FakeObject and not object_explorer):
# We need to leave this import here for tests to pass.
from .arrayeditor import ArrayEditor
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):
self.sig_editor_shown.emit()
return
Expand Down Expand Up @@ -205,7 +262,10 @@ def createEditor(self, parent, option, index, object_explorer=False):
and pd.DataFrame is not FakeObject and not object_explorer):
# We need to leave this import here for tests to pass.
from .dataframeeditor import DataFrameEditor
editor = DataFrameEditor(parent=parent)
editor = DataFrameEditor(
parent=parent,
data_function=self.make_data_function(index)
)
if not editor.setup_and_check(value, title=key):
self.sig_editor_shown.emit()
return
Expand Down Expand Up @@ -272,6 +332,7 @@ def createEditor(self, parent, option, index, object_explorer=False):
name=key,
parent=parent,
namespacebrowser=self.namespacebrowser,
data_function=self.make_data_function(index),
readonly=readonly)
self.create_dialog(editor, dict(model=index.model(),
editor=editor,
Expand Down Expand Up @@ -414,8 +475,12 @@ 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 @@ -435,6 +500,51 @@ 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 @@ -471,7 +581,10 @@ 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 @@ -480,7 +593,10 @@ 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 @@ -501,7 +617,10 @@ 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
Loading