Repository: spyder-ide/spyder

Issue:
Consoles get renamed when moved in the IPython console
If I rename a console `foo` and move it next to another one, their names get swapped.


Discussion:
They don't need to be renamed for this to happen.  Just swapping two tabs causes them to switch names.

@ccordoba12   It appears this is a result of PR #5902.    In `tabs.py`, PR 5902 commented out the code in `mouseMoveEvent` that created the QDrag object.  Without the QDrag object, `dragEnterEvent` and `dropEvent` aren't called, which means the signals aren't emitted to `move_tabs` which does the renaming.  `editor.py` uses the `tabMoved()` signal (which was added in Qt 4.5).

As you said in #5902, I think `tabs.py` should be updated to match `editor.py`, which would mean using `tabMoved()`.  However there is also code in there to move tabs from one widget to another, so that would be lost without the custom drag and drop.  Although, it's not working anyway, so maybe it wouldn't be an issue to lose that functionality?

Resolving patch:
diff --git a/spyder/plugins/ipythonconsole.py b/spyder/plugins/ipythonconsole.py
--- a/spyder/plugins/ipythonconsole.py
+++ b/spyder/plugins/ipythonconsole.py
@@ -653,7 +653,7 @@ def __init__(self, parent, testing=False, test_dir=TEMPDIR,
             # Fixes Issue 561
             self.tabwidget.setDocumentMode(True)
         self.tabwidget.currentChanged.connect(self.refresh_plugin)
-        self.tabwidget.move_data.connect(self.move_tab)
+        self.tabwidget.tabBar().tabMoved.connect(self.move_tab)
         self.tabwidget.tabBar().sig_change_name.connect(
             self.rename_tabs_after_change)
 
@@ -1554,16 +1554,25 @@ def rename_client_tab(self, client, given_name):
         self.tabwidget.setTabText(index, client.get_name())
 
     def rename_tabs_after_change(self, given_name):
+        """Rename tabs after a change in name."""
         client = self.get_current_client()
 
+        # Prevent renames that want to assign the same name of
+        # a previous tab
+        repeated = False
+        for cl in self.get_clients():
+            if id(client) != id(cl) and given_name == cl.given_name:
+                repeated = True
+                break
+
         # Rename current client tab to add str_id
-        if client.allow_rename and not u'/' in given_name:
+        if client.allow_rename and not u'/' in given_name and not repeated:
             self.rename_client_tab(client, given_name)
         else:
             self.rename_client_tab(client, None)
 
         # Rename related clients
-        if client.allow_rename and not u'/' in given_name:
+        if client.allow_rename and not u'/' in given_name and not repeated:
             for cl in self.get_related_clients(client):
                 self.rename_client_tab(cl, given_name)
 
diff --git a/spyder/widgets/editor.py b/spyder/widgets/editor.py
--- a/spyder/widgets/editor.py
+++ b/spyder/widgets/editor.py
@@ -29,7 +29,7 @@
                             QVBoxLayout, QWidget, QListWidget, QListWidgetItem)
 
 # Local imports
-from spyder.config.base import _, DEBUG, STDERR, STDOUT
+from spyder.config.base import _, DEBUG, PYTEST, STDERR, STDOUT
 from spyder.config.gui import config_shortcut, get_shortcut
 from spyder.config.utils import (get_edit_filetypes, get_edit_filters,
                                  get_filter, is_kde_desktop, is_anaconda)
@@ -596,6 +596,9 @@ def __init__(self, parent, actions):
         self.edit_filetypes = None
         self.edit_filters = None
 
+        # For testing
+        self.save_dialog_on_tests = not PYTEST
+
     @Slot()
     def show_in_external_file_explorer(self, fnames=None):
         """Show file in external file explorer"""
@@ -1532,7 +1535,8 @@ def save_if_changed(self, cancelable=False, index=None):
             if finfo.filename == self.tempfile_path or yes_all:
                 if not self.save(index):
                     return False
-            elif finfo.editor.document().isModified():
+            elif (finfo.editor.document().isModified() and
+                  self.save_dialog_on_tests):
 
                 self.msgbox = QMessageBox(
                         QMessageBox.Question,


Test patch:
diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py
--- a/spyder/app/tests/test_mainwindow.py
+++ b/spyder/app/tests/test_mainwindow.py
@@ -32,7 +32,6 @@
 from spyder.py3compat import PY2, to_text_string
 from spyder.utils.ipython.kernelspec import SpyderKernelSpec
 from spyder.utils.programs import is_module_installed
-from spyder.utils.test import close_save_message_box
 
 #==============================================================================
 # Constants
@@ -167,7 +166,6 @@ def test_calltip(main_window, qtbot):
     qtbot.keyPress(code_editor, Qt.Key_ParenRight, delay=1000)
     qtbot.keyPress(code_editor, Qt.Key_Enter, delay=1000)
 
-    QTimer.singleShot(1000, lambda: close_save_message_box(qtbot))
     main_window.editor.close_file()
 
 
@@ -532,8 +530,7 @@ def test_run_cython_code(main_window, qtbot):
 
 
 @flaky(max_runs=3)
-@pytest.mark.skipif(os.environ.get('CI', None) is not None or os.name == 'nt',
-                    reason="It times out in our CIs and fails on Windows.")
+@pytest.mark.skipif(os.name == 'nt', reason="It fails on Windows.")
 def test_open_notebooks_from_project_explorer(main_window, qtbot):
     """Test that notebooks are open from the Project explorer."""
     projects = main_window.projects
@@ -568,10 +565,7 @@ def test_open_notebooks_from_project_explorer(main_window, qtbot):
 
     # Assert its contents are the expected ones
     file_text = editorstack.get_current_editor().toPlainText()
-    assert file_text == '\n# coding: utf-8\n\n# In[1]:\n\n1 + 1\n\n\n# In[ ]:\n\n\n\n\n'
-
-    # Close last file (else tests hang here)
-    editorstack.close_file(force=True)
+    assert file_text == '\n# coding: utf-8\n\n# In[1]:\n\n\n1 + 1\n\n\n'
 
     # Close project
     projects.close_project()
@@ -751,7 +745,6 @@ def test_open_files_in_new_editor_window(main_window, qtbot):
 
 
 @flaky(max_runs=3)
-@pytest.mark.skipif(PYQT_WHEEL, reason="It times out sometimes on PyQt wheels")
 def test_close_when_file_is_changed(main_window, qtbot):
     """Test closing spyder when there is a file with modifications open."""
     # Wait until the window is fully up
@@ -765,15 +758,10 @@ def test_close_when_file_is_changed(main_window, qtbot):
     editor = editorstack.get_current_editor()
     editor.document().setModified(True)
 
-    # Close.main-window
-    QTimer.singleShot(1000, lambda: close_save_message_box(qtbot))
-    main_window.close()
-
     # Wait for the segfault
     qtbot.wait(3000)
 
 
-
 @flaky(max_runs=3)
 def test_maximize_minimize_plugins(main_window, qtbot):
     """Test that the maximize button is working correctly."""
diff --git a/spyder/plugins/tests/test_ipythonconsole.py b/spyder/plugins/tests/test_ipythonconsole.py
--- a/spyder/plugins/tests/test_ipythonconsole.py
+++ b/spyder/plugins/tests/test_ipythonconsole.py
@@ -101,6 +101,58 @@ def close_console():
 #==============================================================================
 # Tests
 #==============================================================================
+@flaky(max_runs=3)
+@pytest.mark.skipif(os.name == 'nt', reason="It times out sometimes on Windows")
+def test_tab_rename_for_slaves(ipyconsole, qtbot):
+    """Test slave clients are renamed correctly."""
+    # Wait until the window is fully up
+    shell = ipyconsole.get_current_shellwidget()
+    qtbot.waitUntil(lambda: shell._prompt_html is not None,
+                    timeout=SHELL_TIMEOUT)
+
+    cf = ipyconsole.get_current_client().connection_file
+    ipyconsole._create_client_for_kernel(cf, None, None, None)
+    qtbot.wait(1000)
+
+    # Rename slave
+    ipyconsole.rename_tabs_after_change('foo')
+
+    # Assert both clients have the same name
+    assert 'foo' in ipyconsole.get_clients()[0].get_name()
+    assert 'foo' in ipyconsole.get_clients()[1].get_name()
+
+
+@flaky(max_runs=3)
+@pytest.mark.skipif(os.name == 'nt', reason="It times out sometimes on Windows")
+def test_no_repeated_tabs_name(ipyconsole, qtbot):
+    """Test that tabs can't have repeated given names."""
+    # Rename first client
+    ipyconsole.rename_tabs_after_change('foo')
+
+    # Create a new client and try to rename it
+    ipyconsole.create_new_client()
+    ipyconsole.rename_tabs_after_change('foo')
+
+    # Assert the rename didn't take place
+    client_name = ipyconsole.get_current_client().get_name()
+    assert '2' in client_name
+
+
+@flaky(max_runs=3)
+@pytest.mark.skipif(os.name == 'nt', reason="It times out sometimes on Windows")
+def test_tabs_preserve_name_after_move(ipyconsole, qtbot):
+    """Test that tabs preserve their names after they are moved."""
+    # Create a new client
+    ipyconsole.create_new_client()
+
+    # Move tabs
+    ipyconsole.tabwidget.tabBar().moveTab(0, 1)
+
+    # Assert the second client is in the first position
+    client_name = ipyconsole.get_clients()[0].get_name()
+    assert '2' in client_name
+
+
 @flaky(max_runs=3)
 @pytest.mark.skipif(os.name == 'nt', reason="It times out sometimes on Windows")
 def test_conf_env_vars(ipyconsole, qtbot):
@@ -554,7 +606,7 @@ def test_clear_and_reset_magics_dbg(ipyconsole, qtbot):
 
     # Test clear magic
     shell.clear_console()
-    qtbot.wait(500)
+    qtbot.wait(1000)
     assert '\nipdb> ' == control.toPlainText()
 
     # Test reset magic
diff --git a/spyder/utils/test.py b/spyder/utils/test.py
--- a/spyder/utils/test.py
+++ b/spyder/utils/test.py
@@ -25,19 +25,3 @@ def close_message_box(qtbot):
     for w in top_level_widgets:
         if isinstance(w, QMessageBox):
             qtbot.keyClick(w, Qt.Key_Enter)
-
-def close_save_message_box(qtbot):
-    """
-    Closes QMessageBox's for save that can appear when testing.
-
-    You can use this with QTimer to close a QMessageBox for save functions.
-    Before calling anything that may show a QMessageBox for save call:
-    QTimer.singleShot(1000, lambda: close_save_message_box(qtbot))
-    """
-    top_level_widgets = QApplication.topLevelWidgets()
-    for w in top_level_widgets:
-        if isinstance(w, QMessageBox):
-            if os.name == 'nt':
-               qtbot.keyClick(w, Qt.Key_Enter)
-            else:
-               qtbot.keyClick(w, Qt.Key_N, modifier=Qt.ShiftModifier)
diff --git a/spyder/widgets/tests/test_save.py b/spyder/widgets/tests/test_save.py
--- a/spyder/widgets/tests/test_save.py
+++ b/spyder/widgets/tests/test_save.py
@@ -50,12 +50,17 @@ def base_editor_bot(qtbot):
 
 
 @pytest.fixture
-def editor_bot(base_editor_bot):
+def editor_bot(base_editor_bot, request):
     """
     Set up EditorStack with CodeEditors containing some Python code.
     The cursor is at the empty line below the code.
     """
     editor_stack, qtbot = base_editor_bot
+
+    show_save_dialog = request.node.get_marker('show_save_dialog')
+    if show_save_dialog:
+        editor_stack.save_dialog_on_tests = True
+
     qtbot.addWidget(editor_stack)
     add_files(editor_stack)
     return editor_stack, qtbot
@@ -84,6 +89,7 @@ def editor_splitter_layout_bot(editor_splitter_bot):
 
 # Tests
 # -------------------------------
+@pytest.mark.show_save_dialog
 def test_save_if_changed(editor_bot, mocker):
     """Test EditorStack.save_if_changed()."""
     editor_stack, qtbot = editor_bot