Repository: spyder-ide/spyder

Issue:
TypeError: tabText(self, int) when moving plugin tabs
## Description

**What steps will reproduce the problem?**

1. 
2. 
3. 

**What is the expected output? What do you see instead?**


**Please provide any additional information below**

```
  File "C:\ProgramData\Anaconda3\lib\site-packages\spyder\plugins\__init__.py", line 110, in eventFilter
    self.tab_moved(event)
  File "C:\ProgramData\Anaconda3\lib\site-packages\spyder\plugins\__init__.py", line 146, in tab_moved
    self.move_tab(from_index, to_index)
  File "C:\ProgramData\Anaconda3\lib\site-packages\spyder\plugins\__init__.py", line 158, in move_tab
    from_plugin = self._get_plugin(from_index)
  File "C:\ProgramData\Anaconda3\lib\site-packages\spyder\plugins\__init__.py", line 62, in _get_plugin
    tab_text = self.dock_tabbar.tabText(index).replace('&', '')
TypeError: tabText(self, int): argument 1 has unexpected type 'NoneType'
```


## 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
```
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
TypeError: tabText(self, int) when moving plugin tabs
## Description

**What steps will reproduce the problem?**

1. 
2. 
3. 

**What is the expected output? What do you see instead?**


**Please provide any additional information below**

```
  File "C:\ProgramData\Anaconda3\lib\site-packages\spyder\plugins\__init__.py", line 110, in eventFilter
    self.tab_moved(event)
  File "C:\ProgramData\Anaconda3\lib\site-packages\spyder\plugins\__init__.py", line 146, in tab_moved
    self.move_tab(from_index, to_index)
  File "C:\ProgramData\Anaconda3\lib\site-packages\spyder\plugins\__init__.py", line 158, in move_tab
    from_plugin = self._get_plugin(from_index)
  File "C:\ProgramData\Anaconda3\lib\site-packages\spyder\plugins\__init__.py", line 62, in _get_plugin
    tab_text = self.dock_tabbar.tabText(index).replace('&', '')
TypeError: tabText(self, int): argument 1 has unexpected type 'NoneType'
```


## 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
```
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


Discussion:
Something related to naming and moving around tabs somewhere in the UI, it seems. Others may know more specifically, but you describe what you were doing around the time the problem occurred?
This happens when moving plugin tabs to reorganize them (e.g. moving the IPython console to be next to the History Log).

I really don't understand why this error happens, but we can catch it and simply pass if it shows up.
@ccordoba12  Is the help wanted tag for just catching the error, or tracing it more thoroughly? 
Just catching the error.
I can take a look...
Great, thanks!
I can't actually reproduce it, but I can at least catch the error and just ``pass`` it if that'll do for now...not sure how much more I can do without actually seeing what's going on...
Yep, that's fine for now. I don't understand how this error appears 
either, but the best thing we can is to catch it and pass on it.

El 06/01/18 a las 17:15, C.A.M. Gerlach escribió:
>
> I can't actually reproduce it, but I can at least catch the error and 
> just |pass| it if that'll do for now...not sure how much more I can do 
> without actually seeing what's going on...
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub 
> <https://github.com/spyder-ide/spyder/issues/5813#issuecomment-355781996>, 
> or mute the thread 
> <https://github.com/notifications/unsubscribe-auth/AAWS7SAchoKMeQJfKM37Lz1KB5FxIjAAks5tH_B-gaJpZM4QmavY>.
>


Should I close this Issue? It didn't happen to me again; I just thought posting the error would help in development of Spyder, but you seem to need the discussion thread.
No, please leave it open. We'll make a simple fix for this error because it happens for other people as well.
Alright, good luck with the fix!
> I just thought posting the error would help in development of Spyder

It does, thank you! It should be first in my (long) queue as soon as I get my current PRs straightened out, and it should definitely make ``3.2.7``.
Something related to naming and moving around tabs somewhere in the UI, it seems. Others may know more specifically, but you describe what you were doing around the time the problem occurred?
This happens when moving plugin tabs to reorganize them (e.g. moving the IPython console to be next to the History Log).

I really don't understand why this error happens, but we can catch it and simply pass if it shows up.
@ccordoba12  Is the help wanted tag for just catching the error, or tracing it more thoroughly? 
Just catching the error.
I can take a look...
Great, thanks!
I can't actually reproduce it, but I can at least catch the error and just ``pass`` it if that'll do for now...not sure how much more I can do without actually seeing what's going on...
Yep, that's fine for now. I don't understand how this error appears 
either, but the best thing we can is to catch it and pass on it.

El 06/01/18 a las 17:15, C.A.M. Gerlach escribió:
>
> I can't actually reproduce it, but I can at least catch the error and 
> just |pass| it if that'll do for now...not sure how much more I can do 
> without actually seeing what's going on...
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub 
> <https://github.com/spyder-ide/spyder/issues/5813#issuecomment-355781996>, 
> or mute the thread 
> <https://github.com/notifications/unsubscribe-auth/AAWS7SAchoKMeQJfKM37Lz1KB5FxIjAAks5tH_B-gaJpZM4QmavY>.
>


Should I close this Issue? It didn't happen to me again; I just thought posting the error would help in development of Spyder, but you seem to need the discussion thread.
No, please leave it open. We'll make a simple fix for this error because it happens for other people as well.
Alright, good luck with the fix!
> I just thought posting the error would help in development of Spyder

It does, thank you! It should be first in my (long) queue as soon as I get my current PRs straightened out, and it should definitely make ``3.2.7``.

Resolving patch:
diff --git a/spyder/plugins/__init__.py b/spyder/plugins/__init__.py
--- a/spyder/plugins/__init__.py
+++ b/spyder/plugins/__init__.py
@@ -96,7 +96,7 @@ def _fix_cursor(self, from_index, to_index):
             new_pos = self.dock_tabbar.mapToGlobal(QPoint(x + delta, y))
             cursor.setPos(new_pos)
 
-    def eventFilter(self,  obj,  event):
+    def eventFilter(self, obj, event):
         """Filter mouse press events.
 
         Events that are captured and not propagated return True. Events that
@@ -107,7 +107,10 @@ def eventFilter(self,  obj,  event):
             self.tab_pressed(event)
             return False
         if event_type == QEvent.MouseMove:
-            self.tab_moved(event)
+            try:
+                self.tab_moved(event)
+            except TypeError:
+                pass
             return True
         if event_type == QEvent.MouseButtonRelease:
             self.tab_released(event)
@@ -138,11 +141,11 @@ def tab_moved(self, event):
             QApplication.setOverrideCursor(Qt.ClosedHandCursor)
             self.moving = True
 
-        if self.to_index == -1:
+        if self.to_index in (-1, None):
             self.to_index = self.from_index
 
         from_index, to_index = self.from_index, self.to_index
-        if from_index != to_index and from_index != -1 and to_index != -1:
+        if from_index not in (to_index, -1, None):
             self.move_tab(from_index, to_index)
             self._fix_cursor(from_index, to_index)
             self.from_index = to_index


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
@@ -1,8 +1,10 @@
 # -*- coding: utf-8 -*-
-#
+# -----------------------------------------------------------------------------
 # Copyright © Spyder Project Contributors
-# Licensed under the terms of the MIT License
 #
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+# -----------------------------------------------------------------------------
 
 """
 Tests for the main window
@@ -23,15 +25,16 @@
 from numpy.testing import assert_array_equal
 import pytest
 from qtpy import PYQT4, PYQT5, PYQT_VERSION
-from qtpy.QtCore import Qt, QTimer
+from qtpy.QtCore import Qt, QTimer, QEvent
 from qtpy.QtTest import QTest
-from qtpy.QtWidgets import QApplication, QFileDialog, QLineEdit
+from qtpy.QtWidgets import QApplication, QFileDialog, QLineEdit, QTabBar
 
 from spyder import __trouble_url__
 from spyder.app.mainwindow import MainWindow  # Tests fail without this import
 from spyder.app import start
 from spyder.config.base import get_home_dir
 from spyder.config.main import CONF
+from spyder.plugins import TabFilter
 from spyder.plugins.runconfig import RunConfiguration
 from spyder.py3compat import PY2, to_text_string
 from spyder.utils.ipython.kernelspec import SpyderKernelSpec
@@ -163,7 +166,7 @@ def close_window():
 @pytest.mark.use_introspection
 @flaky(max_runs=3)
 @pytest.mark.skipif(os.name == 'nt' or (not PY2 and PYQT_VERSION < "5.9.0"),
-                    reason="It times out on AppVeyor and fails on PY3 and PyQt 5.6")
+                    reason="Times out on AppVeyor and fails on PY3/PyQt 5.6")
 @pytest.mark.timeout(timeout=45, method='thread')
 def test_calltip(main_window, qtbot):
     """Test that the calltip in editor is hidden when matching ')' is found."""
@@ -181,7 +184,7 @@ def test_calltip(main_window, qtbot):
 
     qtbot.keyPress(code_editor, Qt.Key_ParenLeft, delay=3000)
     qtbot.keyPress(code_editor, Qt.Key_A, delay=1000)
-    qtbot.waitUntil(lambda: calltip.isVisible(), timeout=1000)
+    qtbot.waitUntil(lambda: calltip.isVisible(), timeout=3000)
 
     qtbot.keyPress(code_editor, Qt.Key_ParenRight, delay=1000)
     qtbot.keyPress(code_editor, Qt.Key_Space)
@@ -1224,5 +1227,23 @@ def test_troubleshooting_menu_item_and_url(monkeypatch):
             raise
 
 
+@flaky(max_runs=3)
+@pytest.mark.slow
+def test_tabfilter_typeerror_full(main_window):
+    """Test for #5813 ; event filter handles None indicies when moving tabs."""
+    MockEvent = MagicMock()
+    MockEvent.return_value.type.return_value = QEvent.MouseMove
+    MockEvent.return_value.pos.return_value = 0
+    mockEvent_instance = MockEvent()
+
+    test_tabbar = main_window.findChildren(QTabBar)[0]
+    test_tabfilter = TabFilter(test_tabbar, main_window)
+    test_tabfilter.from_index = None
+    test_tabfilter.moving = True
+
+    assert test_tabfilter.eventFilter(None, mockEvent_instance)
+    assert mockEvent_instance.pos.call_count == 1
+
+
 if __name__ == "__main__":
     pytest.main()
diff --git a/spyder/plugins/tests/test_init.py b/spyder/plugins/tests/test_init.py
new file mode 100644
--- /dev/null
+++ b/spyder/plugins/tests/test_init.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright © Spyder Project Contributors
+#
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+# -----------------------------------------------------------------------------
+
+"""Tests for the base plugin classes ('__init__.py')."""
+
+# Standard library imports
+try:
+    from unittest.mock import Mock, MagicMock
+except ImportError:
+    from mock import Mock, MagicMock  # Python 2
+
+# 3rd party imports
+import pytest
+from qtpy.QtCore import QEvent
+
+# Local imports
+from spyder.plugins import TabFilter
+
+
+# =============================================================================
+# Tests
+# =============================================================================
+def test_tabfilter_typeerror_simple():
+    """Test for #5813 ; event filter handles None indicies when moving tabs."""
+    MockEvent = MagicMock()
+    MockEvent.return_value.type.return_value = QEvent.MouseMove
+    MockEvent.return_value.pos.return_value = 0
+    mockEvent_instance = MockEvent()
+
+    MockTabBar = MagicMock()
+    MockTabBar.return_value.tabAt.return_value = 0
+    mockTabBar_instance = MockTabBar()
+
+    MockMainWindow = Mock()
+    mockMainWindow_instance = MockMainWindow()
+
+    test_tabfilter = TabFilter(mockTabBar_instance, mockMainWindow_instance)
+    test_tabfilter.from_index = None
+    test_tabfilter.moving = True
+
+    assert test_tabfilter.eventFilter(None, mockEvent_instance)
+    assert mockEvent_instance.pos.call_count == 1
+    assert mockTabBar_instance.tabAt.call_count == 1
+
+
+if __name__ == "__main__":
+    pytest.main()
diff --git a/spyder/widgets/tests/test_editor.py b/spyder/widgets/tests/test_editor.py
--- a/spyder/widgets/tests/test_editor.py
+++ b/spyder/widgets/tests/test_editor.py
@@ -509,6 +509,7 @@ def test_tab_moves_focus_from_search_to_replace(editor_find_replace_bot):
     assert finder.replace_text.hasFocus()
 
 
+@flaky(max_runs=3)
 @pytest.mark.skipif(platform.startswith('linux'), reason="Fails on Linux.")
 def test_tab_copies_find_to_replace(editor_find_replace_bot):
     """Check that text in the find box is copied to the replace box on tab
@@ -518,8 +519,8 @@ def test_tab_copies_find_to_replace(editor_find_replace_bot):
     finder.show_replace()
     finder.search_text.setFocus()
     finder.search_text.set_current_text('This is some test text!')
-    qtbot.keyPress(finder.search_text, Qt.Key_Tab)
-    qtbot.wait(100)
+    qtbot.keyClick(finder.search_text, Qt.Key_Tab)
+    qtbot.wait(500)
     assert finder.replace_text.currentText() == 'This is some test text!'