Skip to content

Commit

Permalink
Implement refresh in object explorer
Browse files Browse the repository at this point in the history
* In CollectionDelegate, pass `data_function` when creating an object explorer.
* Add a refresh toolbutton to the object explorer, which calls the new
  function `.refresh_editor()`.
* In the new function, call `data_function` and use its return value as the
  new object to be displayed in the object explorer. Handle exceptions thrown
  by `data_function`.
* Add tests for the new refresh functionality.
  • Loading branch information
jitseniesen committed Oct 31, 2023
1 parent d6ed8dc commit fa85be3
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,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
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@
# Standard library imports
import logging
import traceback
from typing import Any, Callable, Optional

# Third-party imports
from qtpy.QtCore import Slot, QModelIndex, QPoint, QSize, Qt
from qtpy.QtGui import QKeySequence, QTextOption
from qtpy.QtWidgets import (QAbstractItemView, QAction, QButtonGroup,
QGroupBox, QHBoxLayout, QHeaderView,
QMenu, QPushButton, QRadioButton, QSplitter,
QToolButton, QVBoxLayout, QWidget)
from qtpy.QtWidgets import (
QAbstractItemView, QAction, QButtonGroup, QGroupBox, QHBoxLayout,
QHeaderView, QMenu, QMessageBox, QPushButton, QRadioButton, QSplitter,
QToolButton, QVBoxLayout, QWidget)

# Local imports
from spyder.api.config.fonts import SpyderFontsMixin, SpyderFontType
Expand Down Expand Up @@ -62,6 +63,7 @@ def __init__(self,
resize_to_contents=True,
parent=None,
namespacebrowser=None,
data_function: Optional[Callable[[], Any]] = None,
attribute_columns=DEFAULT_ATTR_COLS,
attribute_details=DEFAULT_ATTR_DETAILS,
readonly=None,
Expand Down Expand Up @@ -94,6 +96,7 @@ def __init__(self,
self.name = name
self.expanded = expanded
self.namespacebrowser = namespacebrowser
self.data_function = data_function
self._attr_cols = attribute_columns
self._attr_details = attribute_details
self.readonly = readonly
Expand Down Expand Up @@ -258,6 +261,14 @@ def _setup_menu(self, show_callable_attributes=False,
self.tools_layout.addSpacing(5)
self.tools_layout.addWidget(special_attributes)

self.refresh_button = create_toolbutton(
self, icon=ima.icon('refresh'),
tip=_('Refresh editor with current value of variable in console'),
triggered=self.refresh_editor)
self.refresh_button.setEnabled(self.data_function is not None)
self.tools_layout.addSpacing(5)
self.tools_layout.addWidget(self.refresh_button)

self.tools_layout.addStretch()

self.options_button = create_toolbutton(
Expand Down Expand Up @@ -406,6 +417,22 @@ def _readViewSettings(self, reset=False):
if button is not None:
button.setChecked(True)

def refresh_editor(self) -> None:
"""
Refresh data in editor.
"""
assert self.data_function is not None

try:
data = self.data_function()
except (IndexError, KeyError):
QMessageBox.critical(self, _('Collection editor'),
_('The variable no longer exists.'))
self.reject()
return

self.set_value(data)

@Slot()
def save_and_close_enable(self):
"""Handle the data change event to enable the save and close button."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@
"""

# Standard library imports
from dataclasses import dataclass
import datetime
from unittest.mock import patch

# Third party imports
from qtpy.QtCore import Qt
import numpy as np
import pytest
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QMessageBox

# Local imports
from spyder.config.manager import CONF
Expand Down Expand Up @@ -172,5 +175,60 @@ def __init__(self):
assert model.columnCount() == 11


@dataclass
class DataclassForTesting:
name: str
price: float
quantity: int


def test_objectexplorer_refreshbutton_disabled():
"""
Test that the Refresh button is disabled by default.
"""
data = DataclassForTesting('lemon', 0.15, 5)
editor = ObjectExplorer(data, name='data')
assert not editor.refresh_button.isEnabled()


def test_objectexplorer_refresh():
"""
Test that after pressing the refresh button, the value of the Array Editor
is replaced by the return value of the data_function.
"""
data_old = DataclassForTesting('lemon', 0.15, 5)
data_new = range(1, 42, 3)
editor = ObjectExplorer(data_old, name='data',
data_function=lambda: data_new)
model = editor.obj_tree.model()
root = model.index(0, 0)
assert model.data(model.index(0, 0, root), Qt.DisplayRole) == 'name'
assert model.data(model.index(0, 3, root), Qt.DisplayRole) == 'lemon'
assert editor.refresh_button.isEnabled()
editor.refresh_editor()
model = editor.obj_tree.model()
root = model.index(0, 0)
row = model.rowCount(root) - 1
assert model.data(model.index(row, 0, root), Qt.DisplayRole) == 'stop'
assert model.data(model.index(row, 3, root), Qt.DisplayRole) == '42'


def test_objectexplorer_refresh_when_variable_deleted(qtbot):
"""
Test that if the variable is deleted and then the editor is refreshed
(resulting in data_function raising a KeyError), a critical dialog box
is displayed and that the object editor is closed.
"""
def datafunc():
raise KeyError
data = DataclassForTesting('lemon', 0.15, 5)
editor = ObjectExplorer(data, name='data', data_function=datafunc)
with patch('spyder.plugins.variableexplorer.widgets.objectexplorer'
'.objectexplorer.QMessageBox.critical') as mock_critical:
with qtbot.waitSignal(editor.rejected, timeout=0):
editor.refresh_button.click()
mock_critical.assert_called_once()


if __name__ == "__main__":
pytest.main()

0 comments on commit fa85be3

Please sign in to comment.