# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright (c) 2019- Spyder Project Contributors # # Components of objectbrowser originally distributed under # the MIT (Expat) license. Licensed under the terms of the MIT License; # see NOTICE.txt in the Spyder root directory for details # ----------------------------------------------------------------------------- # Standard library imports import copy import datetime import functools import operator # Third party imports from qtpy.compat import to_qvariant from qtpy.QtCore import QDateTime, Qt, Signal, Slot 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, is_known_type) # Local imports from spyder.config.base import _, is_pynsist, running_in_mac_app from spyder.config.fonts import DEFAULT_SMALL_DELTA from spyder.config.gui import get_font from spyder.py3compat import is_binary_string, is_text_string, to_text_string from spyder.plugins.variableexplorer.widgets.arrayeditor import ArrayEditor from spyder.plugins.variableexplorer.widgets.dataframeeditor import ( DataFrameEditor) from spyder.plugins.variableexplorer.widgets.texteditor import TextEditor from spyder.plugins.variableexplorer.widgets.objectexplorer.attribute_model \ import safe_tio_call LARGE_COLLECTION = 1e5 LARGE_ARRAY = 5e6 class CollectionsDelegate(QItemDelegate): """CollectionsEditor Item Delegate""" sig_free_memory_requested = Signal() sig_editor_creation_started = Signal() sig_editor_shown = Signal() def __init__(self, parent=None): QItemDelegate.__init__(self, parent) self._editors = {} # keep references on opened editors def get_value(self, index): if index.isValid(): return index.model().get_value(index) def set_value(self, index, value): if index.isValid(): index.model().set_value(index, value) def show_warning(self, index): """ Decide if showing a warning when the user is trying to view a big variable associated to a Tablemodel index. This avoids getting the variables' value to know its size and type, using instead those already computed by the TableModel. The problem is when a variable is too big, it can take a lot of time just to get its value. """ val_type = index.sibling(index.row(), 1).data() val_size = index.sibling(index.row(), 2).data() if val_type in ['list', 'set', 'tuple', 'dict']: if int(val_size) > LARGE_COLLECTION: return True elif (val_type in ['DataFrame', 'Series'] or 'Array' in val_type or 'Index' in val_type): # Avoid errors for user declared types that contain words like # the ones we're looking for above try: # From https://blender.stackexchange.com/a/131849 shape = [int(s) for s in val_size.strip("()").split(",") if s] size = functools.reduce(operator.mul, shape) if size > LARGE_ARRAY: return True except Exception: pass return False def createEditor(self, parent, option, index, object_explorer=False): """Overriding method createEditor""" val_type = index.sibling(index.row(), 1).data() self.sig_editor_creation_started.emit() if index.column() < 3: return None if self.show_warning(index): answer = QMessageBox.warning( self.parent(), _("Warning"), _("Opening this variable can be slow\n\n" "Do you want to continue anyway?"), QMessageBox.Yes | QMessageBox.No) if answer == QMessageBox.No: self.sig_editor_shown.emit() return None try: value = self.get_value(index) if value is None: return None except ImportError as msg: self.sig_editor_shown.emit() module = str(msg).split("'")[1] if module in ['pandas', 'numpy']: if module == 'numpy': val_type = 'array' else: val_type = 'dataframe, series' message = _("Spyder is unable to show the {val_type} or object" " you're trying to view because {module}" " is not installed. ") if running_in_mac_app(): message += _("Please consider using the full version of " "the Spyder MacOS application.
") else: message += _("Please install this package in your Spyder " "environment.
") QMessageBox.critical( self.parent(), _("Error"), message.format(val_type=val_type, module=module)) return else: if running_in_mac_app() or is_pynsist(): message = _("Spyder is unable to show the variable you're" " trying to view because the module " "{module} is not supported in the " "Spyder Lite application.
") else: message = _("Spyder is unable to show the variable you're" " trying to view because the module " "{module} is not found in your " "Spyder environment. Please install this " "package in this environment.
") QMessageBox.critical(self.parent(), _("Error"), message.format(module=module)) return except Exception as msg: QMessageBox.critical( self.parent(), _("Error"), _("Spyder was unable to retrieve the value of " "this variable from the console.

" "The error message was:
" "%s") % to_text_string(msg)) return key = index.model().get_key(index) readonly = (isinstance(value, (tuple, set)) or self.parent().readonly or not is_known_type(value)) # CollectionsEditor for a list, tuple, dict, etc. if isinstance(value, (list, set, tuple, dict)) and not object_explorer: from spyder.widgets.collectionseditor import CollectionsEditor editor = CollectionsEditor(parent=parent) editor.setup(value, key, icon=self.parent().windowIcon(), readonly=readonly) self.create_dialog(editor, dict(model=index.model(), editor=editor, key=key, readonly=readonly)) return None # ArrayEditor for a Numpy array elif (isinstance(value, (np.ndarray, np.ma.MaskedArray)) and 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) if not editor.setup_and_check(value, title=key, readonly=readonly): return self.create_dialog(editor, dict(model=index.model(), editor=editor, key=key, readonly=readonly)) return None # ArrayEditor for an images elif (isinstance(value, PIL.Image.Image) and np.ndarray is not FakeObject and PIL.Image is not FakeObject and not object_explorer): arr = np.array(value) editor = ArrayEditor(parent=parent) if not editor.setup_and_check(arr, title=key, readonly=readonly): return conv_func = lambda arr: PIL.Image.fromarray(arr, mode=value.mode) self.create_dialog(editor, dict(model=index.model(), editor=editor, key=key, readonly=readonly, conv=conv_func)) return None # DataFrameEditor for a pandas dataframe, series or index elif (isinstance(value, (pd.DataFrame, pd.Index, pd.Series)) 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) if not editor.setup_and_check(value, title=key): return self.create_dialog(editor, dict(model=index.model(), editor=editor, key=key, readonly=readonly)) return None # QDateEdit and QDateTimeEdit for a dates or datetime respectively elif isinstance(value, datetime.date) and not object_explorer: if readonly: self.sig_editor_shown.emit() return None else: if isinstance(value, datetime.datetime): editor = QDateTimeEdit(value, parent=parent) # Needed to handle NaT values # See spyder-ide/spyder#8329 try: value.time() except ValueError: self.sig_editor_shown.emit() return None else: editor = QDateEdit(value, parent=parent) editor.setCalendarPopup(True) editor.setFont(get_font(font_size_delta=DEFAULT_SMALL_DELTA)) self.sig_editor_shown.emit() return editor # TextEditor for a long string elif is_text_string(value) and len(value) > 40 and not object_explorer: te = TextEditor(None, parent=parent) if te.setup_and_check(value): editor = TextEditor(value, key, readonly=readonly, parent=parent) self.create_dialog(editor, dict(model=index.model(), editor=editor, key=key, readonly=readonly)) return None # QLineEdit for an individual value (int, float, short string, etc) elif is_editable_type(value) and not object_explorer: if readonly: self.sig_editor_shown.emit() return None else: editor = QLineEdit(parent=parent) editor.setFont(get_font(font_size_delta=DEFAULT_SMALL_DELTA)) editor.setAlignment(Qt.AlignLeft) # This is making Spyder crash because the QLineEdit that it's # been modified is removed and a new one is created after # evaluation. So the object on which this method is trying to # act doesn't exist anymore. # editor.returnPressed.connect(self.commitAndCloseEditor) self.sig_editor_shown.emit() return editor # ObjectExplorer for an arbitrary Python object else: from spyder.plugins.variableexplorer.widgets.objectexplorer \ import ObjectExplorer editor = ObjectExplorer( value, name=key, parent=parent, readonly=readonly) self.create_dialog(editor, dict(model=index.model(), editor=editor, key=key, readonly=readonly)) return None def create_dialog(self, editor, data): self._editors[id(editor)] = data editor.accepted.connect( lambda eid=id(editor): self.editor_accepted(eid)) editor.rejected.connect( lambda eid=id(editor): self.editor_rejected(eid)) self.sig_editor_shown.emit() editor.show() def editor_accepted(self, editor_id): data = self._editors[editor_id] if not data['readonly']: index = data['model'].get_index_from_key(data['key']) value = data['editor'].get_value() conv_func = data.get('conv', lambda v: v) self.set_value(index, conv_func(value)) # This is needed to avoid the problem reported on # spyder-ide/spyder#8557. try: self._editors.pop(editor_id) except KeyError: pass self.free_memory() def editor_rejected(self, editor_id): # This is needed to avoid the problem reported on # spyder-ide/spyder#8557. try: self._editors.pop(editor_id) except KeyError: pass self.free_memory() def free_memory(self): """Free memory after closing an editor.""" try: self.sig_free_memory_requested.emit() except RuntimeError: pass def commitAndCloseEditor(self): """Overriding method commitAndCloseEditor""" editor = self.sender() # Avoid a segfault with PyQt5. Variable value won't be changed # but at least Spyder won't crash. It seems generated by a bug in sip. try: self.commitData.emit(editor) except AttributeError: pass self.closeEditor.emit(editor, QAbstractItemDelegate.NoHint) def setEditorData(self, editor, index): """ Overriding method setEditorData Model --> Editor """ value = self.get_value(index) if isinstance(editor, QLineEdit): if is_binary_string(value): try: value = to_text_string(value, 'utf8') except Exception: pass if not is_text_string(value): value = repr(value) editor.setText(value) elif isinstance(editor, QDateEdit): editor.setDate(value) elif isinstance(editor, QDateTimeEdit): editor.setDateTime(QDateTime(value.date(), value.time())) def setModelData(self, editor, model, index): """ Overriding method setModelData Editor --> Model """ if ((hasattr(model, "sourceModel") and not hasattr(model.sourceModel(), "set_value")) or not hasattr(model, "set_value")): # Read-only mode return if isinstance(editor, QLineEdit): value = editor.text() try: value = display_to_value(to_qvariant(value), self.get_value(index), ignore_errors=False) except Exception as msg: QMessageBox.critical(editor, _("Edit item"), _("Unable to assign data to item." "

Error message:
%s" ) % str(msg)) return elif isinstance(editor, QDateEdit): qdate = editor.date() value = datetime.date(qdate.year(), qdate.month(), qdate.day()) elif isinstance(editor, QDateTimeEdit): qdatetime = editor.dateTime() qdate = qdatetime.date() qtime = qdatetime.time() # datetime uses microseconds, QDateTime returns milliseconds value = datetime.datetime(qdate.year(), qdate.month(), qdate.day(), qtime.hour(), qtime.minute(), qtime.second(), qtime.msec()*1000) else: # Should not happen... raise RuntimeError("Unsupported editor widget") self.set_value(index, value) def updateEditorGeometry(self, editor, option, index): """ Overriding method updateEditorGeometry. This is necessary to set the correct position of the QLineEdit editor since option.rect doesn't have values -> QRect() and makes the editor to be invisible (i.e. it has 0 as x, y, width and height) when doing double click over a cell. See spyder-ide/spyder#9945 """ table_view = editor.parent().parent() if isinstance(table_view, QTableView): row = index.row() column = index.column() y0 = table_view.rowViewportPosition(row) x0 = table_view.columnViewportPosition(column) width = table_view.columnWidth(column) height = table_view.rowHeight(row) editor.setGeometry(x0, y0, width, height) else: super(CollectionsDelegate, self).updateEditorGeometry( editor, option, index) class ToggleColumnDelegate(CollectionsDelegate): """ToggleColumn Item Delegate""" def __init__(self, parent=None): CollectionsDelegate.__init__(self, parent) self.current_index = None self.old_obj = None def restore_object(self): """Discart changes made to the current object in edition.""" if self.current_index and self.old_obj is not None: index = self.current_index index.model().treeItem(index).obj = self.old_obj def get_value(self, index): """Get object value in index.""" if index.isValid(): value = index.model().treeItem(index).obj return value def set_value(self, index, value): if index.isValid(): index.model().set_value(index, value) def createEditor(self, parent, option, index): """Overriding method createEditor""" if self.show_warning(index): answer = QMessageBox.warning( self.parent(), _("Warning"), _("Opening this variable can be slow\n\n" "Do you want to continue anyway?"), QMessageBox.Yes | QMessageBox.No) if answer == QMessageBox.No: return None try: value = self.get_value(index) try: self.old_obj = value.copy() except AttributeError: self.old_obj = copy.deepcopy(value) if value is None: return None except Exception as msg: QMessageBox.critical( self.parent(), _("Error"), _("Spyder was unable to retrieve the value of " "this variable from the console.

" "The error message was:
" "%s") % to_text_string(msg)) return self.current_index = index key = index.model().get_key(index).obj_name readonly = (isinstance(value, (tuple, set)) or self.parent().readonly or not is_known_type(value)) # CollectionsEditor for a list, tuple, dict, etc. if isinstance(value, (list, set, tuple, dict)): from spyder.widgets.collectionseditor import CollectionsEditor editor = CollectionsEditor(parent=parent) editor.setup(value, key, icon=self.parent().windowIcon(), readonly=readonly) self.create_dialog(editor, dict(model=index.model(), editor=editor, key=key, readonly=readonly)) return None # ArrayEditor for a Numpy array elif (isinstance(value, (np.ndarray, np.ma.MaskedArray)) and np.ndarray is not FakeObject): editor = ArrayEditor(parent=parent) if not editor.setup_and_check(value, title=key, readonly=readonly): return self.create_dialog(editor, dict(model=index.model(), editor=editor, key=key, readonly=readonly)) return None # ArrayEditor for an images elif (isinstance(value, PIL.Image.Image) and np.ndarray is not FakeObject and PIL.Image is not FakeObject): arr = np.array(value) editor = ArrayEditor(parent=parent) if not editor.setup_and_check(arr, title=key, readonly=readonly): return conv_func = lambda arr: PIL.Image.fromarray(arr, mode=value.mode) self.create_dialog(editor, dict(model=index.model(), editor=editor, key=key, readonly=readonly, conv=conv_func)) return None # 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) if not editor.setup_and_check(value, title=key): return self.create_dialog(editor, dict(model=index.model(), editor=editor, key=key, readonly=readonly)) return None # QDateEdit and QDateTimeEdit for a dates or datetime respectively elif isinstance(value, datetime.date): if readonly: return None else: if isinstance(value, datetime.datetime): editor = QDateTimeEdit(value, parent=parent) else: editor = QDateEdit(value, parent=parent) editor.setCalendarPopup(True) editor.setFont(get_font(font_size_delta=DEFAULT_SMALL_DELTA)) return editor # TextEditor for a long string elif is_text_string(value) and len(value) > 40: te = TextEditor(None, parent=parent) if te.setup_and_check(value): editor = TextEditor(value, key, readonly=readonly, parent=parent) self.create_dialog(editor, dict(model=index.model(), editor=editor, key=key, readonly=readonly)) return None # QLineEdit for an individual value (int, float, short string, etc) elif is_editable_type(value): if readonly: return None else: editor = QLineEdit(parent=parent) editor.setFont(get_font(font_size_delta=DEFAULT_SMALL_DELTA)) editor.setAlignment(Qt.AlignLeft) # This is making Spyder crash because the QLineEdit that it's # been modified is removed and a new one is created after # evaluation. So the object on which this method is trying to # act doesn't exist anymore. # editor.returnPressed.connect(self.commitAndCloseEditor) return editor # An arbitrary Python object. # Since we are already in the Object Explorer no editor is needed else: return None def editor_accepted(self, editor_id): """Actions to execute when the editor has been closed.""" data = self._editors[editor_id] if not data['readonly'] and self.current_index: index = self.current_index value = data['editor'].get_value() conv_func = data.get('conv', lambda v: v) self.set_value(index, conv_func(value)) # This is needed to avoid the problem reported on # spyder-ide/spyder#8557. try: self._editors.pop(editor_id) except KeyError: pass self.free_memory() def editor_rejected(self, editor_id): """Actions to do when the editor was rejected.""" self.restore_object() super(ToggleColumnDelegate, self).editor_rejected(editor_id)