Repository: spyder-ide/spyder

Issue:
Data Type of customized class is changed to base class when viewed in the Variable Explorer
### Problem Description
Data Type of customized class is changed to base class when viewed in the Variable Explorer


### What steps will reproduce the problem?

1. Define custom class/instance by the following code

```python
class DateScheme(dict):
    """
    Container object for Date Schemes
    Dictionary-like object that exposes its keys as attributes.
    """

    def __init__(self, name):
        self.name = name
        # other keys omitted

    def __dir__(self):
        return self.keys()

    def __setattr__(self, key, value):
        self[key] = value

    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError(key)

DateSchemeInstance = DateScheme("NewName")
```

2. double click on 'DateSchemeInstance' in variable explorer

### What is the expected output? What do you see instead?

on opening the Window the data type of 'DateSchemeInstance' is changed to 'Dictionary' (which is unexpected and unwanted)

### Paste Traceback/Error Below (if applicable)
not applicable

### Package Versions

```

* Spyder: 3.2.7
* Python: 3.6.0 64 bits
* Qt: 5.6.2
* PyQt: 5.6
* Operating System: Windows

```

### Dependencies

```
IPython >=4.0    :  5.1.0 (OK)
cython >=0.21    :  0.25.2 (OK)
jedi >=0.9.0     :  0.9.0 (OK)
nbconvert >=4.0  :  4.2.0 (OK)
numpy >=1.7      :  1.11.3 (OK)
pandas >=0.13.1  :  0.21.0 (OK)
pycodestyle >=2.3:  2.3.1 (OK)
pyflakes >=0.6.0 :  1.5.0 (OK)
pygments >=2.0   :  2.1.3 (OK)
pylint >=0.25    :  1.6.4 (OK)
qtconsole >=4.2.0:  4.2.1 (OK)
rope >=0.9.4     :  0.9.4-1 (OK)
sphinx >=0.6.6   :  1.5.1 (OK)
sympy >=0.7.3    :  1.0 (OK)
```

Remark: I was redirected to this page after posting this question https://stackoverflow.com/questions/49690523/variable-explorer-changes-data-type on StackOverflow.


Discussion:
Thanks for reporting. We'll take a look at this in our next release.
I think the situation has improved somewhat due to PR #7202 . Now, the type only changes when the user edits and saves the values of the derived type.

Updated steps to reproduce:
1. Define a new type in the console: `class MyDict(dict): pass`
2. Define a variable of this type in the console: `md = MyDict({1: 2})` . The Variable Explorer shows this variable with type `MyDict`. It does not display the value.
3. Double click the variable `md` in the Variable Explorer. You get a window showing the values in the dict.
4. If you press `Close` in the dictionary editor, the type of `md` in the Variable Explorer remains `MyDict`.
5. However, if you change a value in the dictionary editor, and press `Save and Close`, the type of `md` in the Variable Explorer and in the IPython console changes to `dict`.

I guess a simple solution would be to make the dictionary editor read-only if it is used on a type derived from `dict`.
Looking at the code, https://github.com/spyder-ide/spyder/blob/master/spyder/widgets/variableexplorer/collectionseditor.py#L1356 calls `.copy()` on the object. For an object `md` of the derived type `MyDict` in the example I gave in the previous comment, `md.copy()` creates a `dict`, not a `MyDict`. So when you click `Save and Close`, it is this `dict` that gets assigned to `md`.

Arguably, this is a bug in `MyDict`. On the other hand, it does seem quite common and rather surprising.
Thanks for tracking this one down @jitseniesen! Do you have some ideas about how to solve this one?

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
@@ -1351,6 +1351,12 @@ def setup(self, data, title='', readonly=False, width=650, remote=False,
                 readonly = True
                 self.data_copy = data
             datalen = len(get_object_attrs(data))
+
+        # If the copy has a different type, then do not allow editing, because
+        # this would change the type after saving; cf. issue #6936
+        if type(self.data_copy) != type(data):
+            readonly = True
+
         self.widget = CollectionsEditorWidget(self, self.data_copy,
                                               title=title, readonly=readonly,
                                               remote=remote)


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
@@ -474,5 +474,34 @@ def test_edit_nonsettable_objects(qtbot, nonsettable_objects_data):
                     == getattr(expected_obj, key) for key in keys])
 
 
+def test_collectionseditor_with_class_having_buggy_copy(qtbot):
+    """
+    Test that editor for object whose .copy() returns a different type is
+    readonly; cf. issue #6936.
+    """
+    class MyDictWithBuggyCopy(dict):
+        pass
+
+    md = MyDictWithBuggyCopy({1: 2})
+    editor = CollectionsEditor()
+    editor.setup(md)
+    assert editor.widget.editor.readonly
+
+
+def test_collectionseditor_with_class_having_correct_copy(qtbot):
+    """
+    Test that editor for object whose .copy() returns the same type is not
+    readonly; cf. issue #6936.
+    """
+    class MyDictWithCorrectCopy(dict):
+        def copy(self):
+            return MyDictWithCorrectCopy(self)
+
+    md = MyDictWithCorrectCopy({1: 2})
+    editor = CollectionsEditor()
+    editor.setup(md)
+    assert not editor.widget.editor.readonly
+
+
 if __name__ == "__main__":
     pytest.main()