Repository: spyder-ide/spyder

Issue:
Variable Explorer windows don't close when Spyder exits; prevents it from reopening until manually closed
## Description

Recently, I've experienced issues on my Windows 8.1 Pro x64 and Windows 10 Ent x64 machines where Spyder will mysteriously not launch (from the start menu icon) with no indication as to why, only on close inspection to find a variable explorer window hidden somewhere that hasn't closed when Spyder exits (as it presumably should). As far as I'm aware, _any_ open variable explorer window, regardless of type, on Spyder exit will stay open and trigger this behavior; Spyder will only launch after every such window is manually tracked down and closed. 

As the windows have little to no function when Spyder is not open, it seems they should be terminated along with their parent application. Perhaps something to do with them have a dialog-type UI, rather than a normal window? No clue; not a GUI programmer.

**What steps will reproduce the problem?**

1. Double click on a suitable variable name in the main variable explorer to open a variable explorer window,
2. Close Spyder.
3.  Attempt to relaunch via start menu shortcut without closing variable explorer sub windows

**What is the expected output? What do you see instead?**

Expected: Variable explorer windows close along with Spyder, and the latter is able to then launch normally after doing so.

Actual: Variable explorer windows do not close with spyder and stay in the background, often hidden, and prevent Spyder from relaunching. 


**Please provide any additional information below**

Reproduced on multiple OS versions/Spyder installs/physical machines.


## Version and main components

* Spyder Version: 3.2.4 
* Python Version: 3.6.3
* Qt Versions: 5.6.2, PyQt5 5.6 on Windows 8.1 Pro x64 and Windows 10 Ent x64

## Dependencies
```
pyflakes >=0.6.0 :  1.6.0 (OK)
pycodestyle >=2.3:  2.3.1 (OK)
pygments >=2.0   :  2.2.0 (OK)
pandas >=0.13.1  :  0.20.3 (OK)
numpy >=1.7      :  1.13.3 (OK)
sphinx >=0.6.6   :  1.6.3 (OK)
rope >=0.9.4     :  0.10.5 (OK)
jedi >=0.9.0     :  0.10.2 (OK)
nbconvert >=4.0  :  5.3.1 (OK)
sympy >=0.7.3    :  1.1.1 (OK)
cython >=0.21    :  0.26.1 (OK)
qtconsole >=4.2.0:  4.3.1 (OK)
IPython >=4.0    :  6.1.0 (OK)
pylint >=0.25    :  1.7.4 (OK)

```

Variable Explorer windows don't close when Spyder exits; prevents it from reopening until manually closed
## Description

Recently, I've experienced issues on my Windows 8.1 Pro x64 and Windows 10 Ent x64 machines where Spyder will mysteriously not launch (from the start menu icon) with no indication as to why, only on close inspection to find a variable explorer window hidden somewhere that hasn't closed when Spyder exits (as it presumably should). As far as I'm aware, _any_ open variable explorer window, regardless of type, on Spyder exit will stay open and trigger this behavior; Spyder will only launch after every such window is manually tracked down and closed. 

As the windows have little to no function when Spyder is not open, it seems they should be terminated along with their parent application. Perhaps something to do with them have a dialog-type UI, rather than a normal window? No clue; not a GUI programmer.

**What steps will reproduce the problem?**

1. Double click on a suitable variable name in the main variable explorer to open a variable explorer window,
2. Close Spyder.
3.  Attempt to relaunch via start menu shortcut without closing variable explorer sub windows

**What is the expected output? What do you see instead?**

Expected: Variable explorer windows close along with Spyder, and the latter is able to then launch normally after doing so.

Actual: Variable explorer windows do not close with spyder and stay in the background, often hidden, and prevent Spyder from relaunching. 


**Please provide any additional information below**

Reproduced on multiple OS versions/Spyder installs/physical machines.


## Version and main components

* Spyder Version: 3.2.4 
* Python Version: 3.6.3
* Qt Versions: 5.6.2, PyQt5 5.6 on Windows 8.1 Pro x64 and Windows 10 Ent x64

## Dependencies
```
pyflakes >=0.6.0 :  1.6.0 (OK)
pycodestyle >=2.3:  2.3.1 (OK)
pygments >=2.0   :  2.2.0 (OK)
pandas >=0.13.1  :  0.20.3 (OK)
numpy >=1.7      :  1.13.3 (OK)
sphinx >=0.6.6   :  1.6.3 (OK)
rope >=0.9.4     :  0.10.5 (OK)
jedi >=0.9.0     :  0.10.2 (OK)
nbconvert >=4.0  :  5.3.1 (OK)
sympy >=0.7.3    :  1.1.1 (OK)
cython >=0.21    :  0.26.1 (OK)
qtconsole >=4.2.0:  4.3.1 (OK)
IPython >=4.0    :  6.1.0 (OK)
pylint >=0.25    :  1.7.4 (OK)

```



Discussion:
Thanks for reporting. According to

https://stackoverflow.com/q/2216711/438386

the fix seems simple. We'll take a look at it in a future release.
Thanks for reporting. According to

https://stackoverflow.com/q/2216711/438386

the fix seems simple. We'll take a look at it in a future release.

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
@@ -1,11 +1,13 @@
 # -*- coding: utf-8 -*-
-#
+# -----------------------------------------------------------------------------
 # Copyright © Spyder Project Contributors
+#
 # Licensed under the terms of the MIT License
 # (see spyder/__init__.py for details)
+# ----------------------------------------------------------------------------
 
 """
-Collections (i.e. dictionary, list and tuple) editor widget and dialog
+Collections (i.e. dictionary, list and tuple) editor widget and dialog.
 """
 
 #TODO: Multiple selection: open as many editors (array/dict/...) as necessary,
@@ -33,7 +35,7 @@
                             QInputDialog, QItemDelegate, QLineEdit, QMenu,
                             QMessageBox, QTableView, QVBoxLayout, QWidget)
 
-# Local import
+# Local imports
 from spyder.config.base import _
 from spyder.config.fonts import DEFAULT_SMALL_DELTA
 from spyder.config.gui import get_font
@@ -62,6 +64,7 @@
 LARGE_NROWS = 100
 ROWS_TO_LOAD = 50
 
+
 class ProxyObject(object):
     """Dictionary proxy to an unknown object."""
 
@@ -438,30 +441,30 @@ def createEditor(self, parent, option, index):
                                    ) % to_text_string(msg))
             return
         key = index.model().get_key(index)
-        readonly = isinstance(value, tuple) or self.parent().readonly \
-                   or not is_known_type(value)
-        #---editor = CollectionsEditor
+        readonly = (isinstance(value, tuple) or self.parent().readonly
+                    or not is_known_type(value))
+        # CollectionsEditor for a list, tuple, dict, etc.
         if isinstance(value, (list, tuple, dict)):
-            editor = 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
-        #---editor = ArrayEditor
+        # ArrayEditor for a Numpy array
         elif isinstance(value, (ndarray, MaskedArray)) \
           and ndarray is not FakeObject:
-            editor = ArrayEditor(parent)
+            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
-        #---showing image
+        # ArrayEditor for an images
         elif isinstance(value, Image) and ndarray is not FakeObject \
           and Image is not FakeObject:
             arr = array(value)
-            editor = ArrayEditor(parent)
+            editor = ArrayEditor(parent=parent)
             if not editor.setup_and_check(arr, title=key, readonly=readonly):
                 return
             conv_func = lambda arr: Image.fromarray(arr, mode=value.mode)
@@ -469,10 +472,10 @@ def createEditor(self, parent, option, index):
                                             key=key, readonly=readonly,
                                             conv=conv_func))
             return None
-        #--editor = DataFrameEditor
+        # DataFrameEditor for a pandas dataframe, series or index
         elif isinstance(value, (DataFrame, DatetimeIndex, Series)) \
           and DataFrame is not FakeObject:
-            editor = DataFrameEditor()
+            editor = DataFrameEditor(parent=parent)
             if not editor.setup_and_check(value, title=key):
                 return
             editor.dataModel.set_format(index.model().dataframe_format)
@@ -480,33 +483,34 @@ def createEditor(self, parent, option, index):
             self.create_dialog(editor, dict(model=index.model(), editor=editor,
                                             key=key, readonly=readonly))
             return None
-        #---editor = QDateEdit or QDateTimeEdit
+        # 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)
+                    editor = QDateTimeEdit(value, parent=parent)
                 else:
-                    editor = QDateEdit(value, parent)
+                    editor = QDateEdit(value, parent=parent)
                 editor.setCalendarPopup(True)
                 editor.setFont(get_font(font_size_delta=DEFAULT_SMALL_DELTA))
                 return editor
-        #---editor = TextEditor
+        # TextEditor for a long string
         elif is_text_string(value) and len(value) > 40:
-            te = TextEditor(None)
+            te = TextEditor(None, parent=parent)
             if te.setup_and_check(value):
-                editor = TextEditor(value, key, readonly=readonly)
+                editor = TextEditor(value, key,
+                                    readonly=readonly, parent=parent)
                 self.create_dialog(editor, dict(model=index.model(),
                                                 editor=editor, key=key,
                                                 readonly=readonly))
             return None
-        #---editor = QLineEdit
+        # QLineEdit for an individual value (int, float, short string, etc)
         elif is_editable_type(value):
             if readonly:
                 return None
             else:
-                editor = QLineEdit(parent)
+                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
@@ -515,15 +519,15 @@ def createEditor(self, parent, option, index):
                 # act doesn't exist anymore.
                 # editor.returnPressed.connect(self.commitAndCloseEditor)
                 return editor
-        #---editor = CollectionsEditor for an arbitrary object
+        # CollectionsEditor for an arbitrary Python object
         else:
-            editor = 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
-            
+
     def create_dialog(self, editor, data):
         self._editors[id(editor)] = data
         editor.accepted.connect(


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
@@ -1,10 +1,13 @@
 # -*- coding: utf-8 -*-
-#
+# -----------------------------------------------------------------------------
 # Copyright © Spyder Project Contributors
+#
 # Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+# ----------------------------------------------------------------------------
 
 """
-Tests for collectionseditor.py
+Tests for collectionseditor.py .
 """
 
 # Standard library imports
@@ -12,14 +15,16 @@
 import copy
 import datetime
 try:
-    from unittest.mock import Mock, ANY
+    from unittest.mock import Mock
 except ImportError:
-    from mock import Mock, ANY  # Python 2
+    from mock import Mock  # Python 2
 
 # Third party imports
+import numpy
 import pandas
 import pytest
 from flaky import flaky
+from qtpy.QtWidgets import QWidget
 
 # Local imports
 from spyder.widgets.variableexplorer.collectionseditor import (
@@ -27,16 +32,20 @@
     LARGE_NROWS, ROWS_TO_LOAD)
 
 
-# Helper functions
+# =============================================================================
+# Utility functions
+# =============================================================================
 def data(cm, i, j):
     return cm.data(cm.createIndex(i, j))
 
+
 def data_table(cm, n_rows, n_cols):
     return [[data(cm, i, j) for i in range(n_rows)] for j in range(n_cols)]
 
-# --- Tests
-# -----------------------------------------------------------------------------
 
+# =============================================================================
+# Tests
+# ============================================================================
 def test_create_dataframeeditor_with_correct_format(qtbot, monkeypatch):
     MockDataFrameEditor = Mock()
     mockDataFrameEditor_instance = MockDataFrameEditor()
@@ -167,9 +176,11 @@ def test_rename_and_duplicate_item_in_collection_editor():
 
 
 def test_edit_mutable_and_immutable_types(monkeypatch):
-    """Check to ensure mutable types (lists, dicts) and individual values are
-    editable, but not immutable ones (tuples) or anything inside of them,
-    per #5991"""
+    """
+    Test that mutable objs/vals are editable in VarExp; immutable ones aren't.
+
+    Regression test for #5991 .
+    """
     MockQLineEdit = Mock()
     attr_to_patch_qlineedit = ('spyder.widgets.variableexplorer.' +
                                'collectionseditor.QLineEdit')
@@ -208,7 +219,7 @@ def test_edit_mutable_and_immutable_types(monkeypatch):
     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)
+    assert not MockTextEditor.call_args[1]["readonly"]
 
     # Datetime inside list
     editor_list_datetime = editor_list.delegate.createEditor(
@@ -220,17 +231,13 @@ def test_edit_mutable_and_immutable_types(monkeypatch):
     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)
+    assert not mockCollectionsEditor_instance.setup.call_args[1]["readonly"]
 
     # 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)
+    assert mockCollectionsEditor_instance.setup.call_args[1]["readonly"]
 
     # Tests for immutable type (tuple) #
     editor_tup = CollectionsEditorTableView(None, tup_test)
@@ -245,7 +252,7 @@ def test_edit_mutable_and_immutable_types(monkeypatch):
     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)
+    assert MockTextEditor.call_args[1]["readonly"]
 
     # Datetime inside tuple
     editor_tup_datetime = editor_tup.delegate.createEditor(
@@ -257,23 +264,22 @@ def test_edit_mutable_and_immutable_types(monkeypatch):
     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)
+    assert mockCollectionsEditor_instance.setup.call_args[1]["readonly"]
 
     # 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)
+    assert mockCollectionsEditor_instance.setup.call_args[1]["readonly"]
 
 
 @flaky(max_runs=3)
 def test_view_module_in_coledit():
-    """Check that modules don't produce an error when trying to open them in
-    Variable Explorer, and are set as readonly. Regression test for #6080"""
+    """
+    Test that modules don't produce an error when opening in Variable Explorer.
+
+    Also check that they are set as readonly. Regression test for #6080 .
+    """
     editor = CollectionsEditor()
     editor.setup(os, "module_test", readonly=False)
     assert editor.widget.editor.readonly
@@ -289,7 +295,6 @@ def test_notimplementederror_multiindex():
                    for minute in range(5, 35, 5)]
     time_delta_multiindex = pandas.MultiIndex.from_product([[0, 1, 2, 3, 4],
                                                             time_deltas])
-#    coll = {'rng': rng}
     col_model = CollectionsModel(None, time_delta_multiindex)
     assert col_model.rowCount() == col_model.rows_loaded == ROWS_TO_LOAD
     assert col_model.columnCount() == 4
@@ -300,5 +305,53 @@ def test_notimplementederror_multiindex():
     assert col_model.rowCount() == 5 * ROWS_TO_LOAD
 
 
+def test_editor_parent_set(monkeypatch):
+    """
+    Test that editors have their parent set so they close with Spyder.
+
+    Regression test for #5696 .
+    """
+    # Mocking and setup
+    test_parent = QWidget()
+
+    MockCollectionsEditor = Mock()
+    attr_to_patch_coledit = ('spyder.widgets.variableexplorer.' +
+                             'collectionseditor.CollectionsEditor')
+    monkeypatch.setattr(attr_to_patch_coledit, MockCollectionsEditor)
+
+    MockArrayEditor = Mock()
+    attr_to_patch_arredit = ('spyder.widgets.variableexplorer.' +
+                             'collectionseditor.ArrayEditor')
+    monkeypatch.setattr(attr_to_patch_arredit, MockArrayEditor)
+
+    MockDataFrameEditor = Mock()
+    attr_to_patch_dfedit = ('spyder.widgets.variableexplorer.' +
+                            'collectionseditor.DataFrameEditor')
+    monkeypatch.setattr(attr_to_patch_dfedit, MockDataFrameEditor)
+
+    MockTextEditor = Mock()
+    attr_to_patch_textedit = ('spyder.widgets.variableexplorer.' +
+                              'collectionseditor.TextEditor')
+    monkeypatch.setattr(attr_to_patch_textedit, MockTextEditor)
+
+    editor_data = [[0, 1, 2, 3, 4],
+                   numpy.array([1.0, 42.0, 1337.0]),
+                   pandas.DataFrame([[1, 2, 3], [20, 30, 40]]),
+                   "012345678901234567890123456789012345678901234567890123456",
+                   os]
+    col_editor = CollectionsEditorTableView(test_parent, editor_data)
+    assert col_editor.parent() is test_parent
+
+    for idx, mock_class in enumerate([MockCollectionsEditor,
+                                      MockArrayEditor,
+                                      MockDataFrameEditor,
+                                      MockTextEditor,
+                                      MockCollectionsEditor]):
+        col_editor.delegate.createEditor(col_editor.parent(), None,
+                                         col_editor.model.createIndex(idx, 3))
+        assert mock_class.call_count == 1 + (idx // 3)
+        assert mock_class.call_args[1]["parent"] is test_parent
+
+
 if __name__ == "__main__":
     pytest.main()