diff --git a/spyder/widgets/variableexplorer/collectionseditor.py b/spyder/widgets/variableexplorer/collectionseditor.py index 28f18bf444d..2bebab21654 100644 --- a/spyder/widgets/variableexplorer/collectionseditor.py +++ b/spyder/widgets/variableexplorer/collectionseditor.py @@ -474,38 +474,41 @@ def createEditor(self, parent, option, index): self.create_dialog(editor, dict(model=index.model(), editor=editor, key=key, readonly=readonly)) return None - #---editor = QDateTimeEdit - elif isinstance(value, datetime.datetime): - editor = QDateTimeEdit(value, parent) - editor.setCalendarPopup(True) - editor.setFont(get_font(font_size_delta=DEFAULT_SMALL_DELTA)) - return editor - #---editor = QDateEdit + #---editor = QDateEdit or QDateTimeEdit elif isinstance(value, datetime.date): - editor = QDateEdit(value, parent) - editor.setCalendarPopup(True) - editor.setFont(get_font(font_size_delta=DEFAULT_SMALL_DELTA)) - return editor + if readonly: + return None + else: + if isinstance(value, datetime.datetime): + editor = QDateTimeEdit(value, parent) + else: + editor = QDateEdit(value, parent) + editor.setCalendarPopup(True) + editor.setFont(get_font(font_size_delta=DEFAULT_SMALL_DELTA)) + return editor #---editor = TextEditor elif is_text_string(value) and len(value) > 40: te = TextEditor(None) if te.setup_and_check(value): - editor = TextEditor(value, key) + editor = TextEditor(value, key, readonly=readonly) self.create_dialog(editor, dict(model=index.model(), editor=editor, key=key, readonly=readonly)) return None #---editor = QLineEdit elif is_editable_type(value): - editor = QLineEdit(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 + if readonly: + return None + else: + editor = QLineEdit(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 #---editor = CollectionsEditor for an arbitrary object else: editor = CollectionsEditor() @@ -670,7 +673,7 @@ def setup_menu(self, minmax): triggered=self.paste) self.copy_action = create_action(self, _("Copy"), icon=ima.icon('editcopy'), - triggered=self.copy) + triggered=self.copy) self.edit_action = create_action(self, _("Edit"), icon=ima.icon('edit'), triggered=self.edit_item) diff --git a/spyder/widgets/variableexplorer/tests/test_collectioneditor.py b/spyder/widgets/variableexplorer/tests/test_collectioneditor.py index fa9e16e8242..7be4b43c0c1 100644 --- a/spyder/widgets/variableexplorer/tests/test_collectioneditor.py +++ b/spyder/widgets/variableexplorer/tests/test_collectioneditor.py @@ -9,10 +9,11 @@ # Standard library imports import copy +import datetime try: - from unittest.mock import Mock + from unittest.mock import Mock, ANY except ImportError: - from mock import Mock # Python 2 + from mock import Mock, ANY # Python 2 # Third party imports import pandas @@ -161,5 +162,108 @@ def test_rename_and_duplicate_item_in_collection_editor(): assert editor.model.get_data() == coll_copy + [coll_copy[0]] +def test_edit_mutable_and_immutable_types(qtbot, monkeypatch): + # To ensure mutable types (lists, dicts) and individual values are editable + # But not immutable ones (tuples) or anything inside of them, per #5991 + MockQLineEdit = Mock() + attr_to_patch_qlineedit = ('spyder.widgets.variableexplorer.' + + 'collectionseditor.QLineEdit') + monkeypatch.setattr(attr_to_patch_qlineedit, MockQLineEdit) + + MockTextEditor = Mock() + attr_to_patch_textedit = ('spyder.widgets.variableexplorer.' + + 'collectionseditor.TextEditor') + monkeypatch.setattr(attr_to_patch_textedit, MockTextEditor) + + MockQDateTimeEdit = Mock() + attr_to_patch_qdatetimeedit = ('spyder.widgets.variableexplorer.' + + 'collectionseditor.QDateTimeEdit') + monkeypatch.setattr(attr_to_patch_qdatetimeedit, MockQDateTimeEdit) + + MockCollectionsEditor = Mock() + mockCollectionsEditor_instance = MockCollectionsEditor() + attr_to_patch_coledit = ('spyder.widgets.variableexplorer.' + + 'collectionseditor.CollectionsEditor') + monkeypatch.setattr(attr_to_patch_coledit, MockCollectionsEditor) + + list_test = [1, "012345678901234567901234567890123456789012", + datetime.datetime(2017, 12, 24, 7, 9), [1, 2, 3], (2, "eggs")] + tup_test = tuple(list_test) + + # Tests for mutable type (list) # + editor_list = CollectionsEditorTableView(None, list_test) + + # Directly editable values inside list + editor_list_value = editor_list.delegate.createEditor( + None, None, editor_list.model.createIndex(0, 3)) + assert editor_list_value is not None + assert MockQLineEdit.call_count == 1 + + # Text Editor for long text inside list + editor_list.delegate.createEditor(None, None, + editor_list.model.createIndex(1, 3)) + assert MockTextEditor.call_count == 2 + MockTextEditor.assert_called_with(ANY, ANY, readonly=False) + + # Datetime inside list + editor_list_datetime = editor_list.delegate.createEditor( + None, None, editor_list.model.createIndex(2, 3)) + assert editor_list_datetime is not None + assert MockQDateTimeEdit.call_count == 1 + + # List inside list + editor_list.delegate.createEditor(None, None, + editor_list.model.createIndex(3, 3)) + assert mockCollectionsEditor_instance.show.call_count == 1 + mockCollectionsEditor_instance.setup.assert_called_with(ANY, ANY, + icon=ANY, + readonly=False) + + # Tuple inside list + editor_list.delegate.createEditor(None, None, + editor_list.model.createIndex(4, 3)) + assert mockCollectionsEditor_instance.show.call_count == 2 + mockCollectionsEditor_instance.setup.assert_called_with(ANY, ANY, + icon=ANY, + readonly=True) + + # Tests for immutable type (tuple) # + editor_tup = CollectionsEditorTableView(None, tup_test) + + # Directly editable values inside tuple + editor_tup_value = editor_tup.delegate.createEditor( + None, None, editor_tup.model.createIndex(0, 3)) + assert editor_tup_value is None + assert MockQLineEdit.call_count == 1 + + # Text Editor for long text inside tuple + editor_tup.delegate.createEditor(None, None, + editor_tup.model.createIndex(1, 3)) + assert MockTextEditor.call_count == 4 + MockTextEditor.assert_called_with(ANY, ANY, readonly=True) + + # Datetime inside tuple + editor_tup_datetime = editor_tup.delegate.createEditor( + None, None, editor_tup.model.createIndex(2, 3)) + assert editor_tup_datetime is None + assert MockQDateTimeEdit.call_count == 1 + + # List inside tuple + editor_tup.delegate.createEditor(None, None, + editor_tup.model.createIndex(3, 3)) + assert mockCollectionsEditor_instance.show.call_count == 3 + mockCollectionsEditor_instance.setup.assert_called_with(ANY, ANY, + icon=ANY, + readonly=True) + + # Tuple inside tuple + editor_tup.delegate.createEditor(None, None, + editor_tup.model.createIndex(4, 3)) + assert mockCollectionsEditor_instance.show.call_count == 4 + mockCollectionsEditor_instance.setup.assert_called_with(ANY, ANY, + icon=ANY, + readonly=True) + + if __name__ == "__main__": pytest.main()