Repository: spyder-ide/spyder

Issue:
Tuples can be "edited" in Variable Explorer but changes are discarded
## Description

In the Variable Explorer, if one opens a tuple and double clicks on one of the elements to edit, the field becomes editable and the user can enter, modify or delete text. However, upon hitting return/okay, the changes are (of course, as they should be) discarded. It would seem for consistency with tuples' immutability and to minimize new user confusion about their changes not being saved, that it would be best to make the field selectable (e.g. can select text like a text box as you can currently) but not editable. I presume there's a way to do that by selecting a different type or parameter for the Qt text field object?

**What steps will reproduce the problem?**

1. Create any tuple, e.g. 
```
tup = ("foo", "bar", 42)
```
2. Open in variable explorer and double click an element value
3. Edit field contents then click off/enter

**What is the expected output? What do you see instead?**
Expected: When double clicked, text becomes selectable as if it were a text field, but cannot be changed/edited. 
Actual: Field can be edited like any other text box, but changes of course discarded when clicked off of/"accepted"


## Version and main components

* Spyder Version: 3.2.4 
* Python Version: 3.6.3
* Qt Versions: 5.6.2, PyQt5 5.6 on Windows

## Dependencies
```
IPython >=4.0    :  6.1.0 (OK)
cython >=0.21    :  0.26.1 (OK)
jedi >=0.9.0     :  0.10.2 (OK)
nbconvert >=4.0  :  5.3.1 (OK)
numpy >=1.7      :  1.13.3 (OK)
pandas >=0.13.1  :  0.20.3 (OK)
pycodestyle >=2.3:  2.3.1 (OK)
pyflakes >=0.6.0 :  1.6.0 (OK)
pygments >=2.0   :  2.2.0 (OK)
pylint >=0.25    :  1.7.4 (OK)
qtconsole >=4.2.0:  4.3.1 (OK)
rope >=0.9.4     :  0.10.5 (OK)
sphinx >=0.6.6   :  1.6.3 (OK)
sympy >=0.7.3    :  1.1.1 (OK)

```



Discussion:
@CAM-Gerlach, do you want to take a look at this one? It should be an easy fix.
@ccordoba12 _Giggles nervously_ Sure, I can give it a look sometime over the next few days, if you think its easy enough (I'm visiting my family at the moment so it might not be right away). Should I base it against 3.x or master? Not sure if it would count as an enhancement or bug fix.

Resolving patch:
diff --git a/spyder/widgets/variableexplorer/collectionseditor.py b/spyder/widgets/variableexplorer/collectionseditor.py
--- 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)


Test patch:
diff --git a/spyder/widgets/variableexplorer/tests/test_collectioneditor.py b/spyder/widgets/variableexplorer/tests/test_collectioneditor.py
--- 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()