Repository: spyder-ide/spyder

Issue:
Spyder encounters internal error on hover (TypeError / ValueError)
## Problem Description
Internal error happens upon clicking one of functions


### What steps reproduce the problem?
1. Launch Spyder.exe
2.  Open Copilot and enable docs to follow cursor (if it's not)
3. Open **kite_tutorial.py**
3. Put cursor to **pretty_print(obj, indent=4)**

### What is the expected output? What do you see instead?
Expected to see no error

### Traceback/Error 
```python-traceback

--- Logging error ---
Traceback (most recent call last):
  File "C:\Users\mOSt\Anaconda3\lib\site-packages\spyder\plugins\editor\widgets\codeeditor.py", line 1217, in handle_hover_response
    at_point=self._last_point)
  File "C:\Users\mOSt\Anaconda3\lib\site-packages\spyder\widgets\mixins.py", line 634, in show_hint
    inspect_word=inspect_word)
  File "C:\Users\mOSt\Anaconda3\lib\site-packages\spyder\widgets\mixins.py", line 474, in _check_signature_and_format
    name_plus_char = inspect_word + open_func_char
TypeError: can only concatenate tuple (not "str") to tuple

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\mOSt\Anaconda3\lib\logging\__init__.py", line 1034, in emit
    msg = self.format(record)
  File "C:\Users\mOSt\Anaconda3\lib\logging\__init__.py", line 880, in format
    return fmt.format(record)
  File "C:\Users\mOSt\Anaconda3\lib\logging\__init__.py", line 619, in format
    record.message = record.getMessage()
  File "C:\Users\mOSt\Anaconda3\lib\logging\__init__.py", line 380, in getMessage
    msg = msg % self.args
ValueError: incomplete format
Call stack:
  File "C:\Users\mOSt\Anaconda3\Scripts\spyder-script.py", line 10, in <module>
    sys.exit(main())
  File "C:\Users\mOSt\Anaconda3\lib\site-packages\spyder\app\start.py", line 205, in main
    mainwindow.main()
  File "C:\Users\mOSt\Anaconda3\lib\site-packages\spyder\app\mainwindow.py", line 3716, in main
    mainwindow = run_spyder(app, options, args)
  File "C:\Users\mOSt\Anaconda3\lib\site-packages\spyder\app\mainwindow.py", line 3598, in run_spyder
    app.exec_()
  File "C:\Users\mOSt\Anaconda3\lib\site-packages\spyder\plugins\completion\languageserver\client.py", line 349, in on_msg_received
    handler(resp['result'], req_id)
  File "C:\Users\mOSt\Anaconda3\lib\site-packages\spyder\plugins\completion\languageserver\providers\document.py", line 169, in process_hover_result
    {'params': contents})
  File "C:\Users\mOSt\Anaconda3\lib\site-packages\spyder\plugins\completion\languageserver\plugin.py", line 284, in receive_response
    self.COMPLETION_CLIENT_NAME, req_id, response)
  File "C:\Users\mOSt\Anaconda3\lib\site-packages\spyder\plugins\completion\plugin.py", line 120, in receive_response
    self._maybe_send_locked(req_id)
  File "C:\Users\mOSt\Anaconda3\lib\site-packages\spyder\plugins\completion\plugin.py", line 161, in _maybe_send_locked
    send()
  File "C:\Users\mOSt\Anaconda3\lib\site-packages\spyder\plugins\completion\plugin.py", line 143, in send
    self.gather_and_send(request_responses)
  File "C:\Users\mOSt\Anaconda3\lib\site-packages\spyder\plugins\completion\plugin.py", line 212, in gather_and_send
    response_instance.handle_response(req_type, responses)
  File "C:\Users\mOSt\Anaconda3\lib\site-packages\spyder\plugins\editor\widgets\codeeditor.py", line 921, in handle_response
    handler(params)
  File "C:\Users\mOSt\Anaconda3\lib\site-packages\spyder\plugins\editor\widgets\codeeditor.py", line 1224, in handle_hover_response
    self.log_lsp_handle_errors("Error when processing hover")
  File "C:\Users\mOSt\Anaconda3\lib\site-packages\spyder\plugins\editor\widgets\codeeditor.py", line 952, in log_lsp_handle_errors
    logger.error('%', 1, stack_info=True)
Message: '%'
Arguments: (1,)
```

## Versions
* Spyder version: 4.0.0rc2
* Python version: 3.6.1
* Qt version: 5.9.7
* PyQt version: 5.9.2
* Operating System name/version: Windows 7, x64

### Dependencies
```
cloudpickle >=0.5.0          :  1.2.2 (OK)
pygments >=2.0               :  2.4.2 (OK)
qtconsole >=4.6.0            :  4.6.0 (OK)
nbconvert >=4.0              :  5.6.1 (OK)
sphinx >=0.6.6               :  2.2.1 (OK)
pylint >=0.25                :  2.4.4 (OK)
psutil >=0.3                 :  5.6.5 (OK)
qtawesome >=0.5.7            :  0.6.0 (OK)
qtpy >=1.5.0                 :  1.9.0 (OK)
pickleshare >=0.4            :  0.7.5 (OK)
zmq >=17                     :  18.1.0 (OK)
chardet >=2.0.0              :  3.0.4 (OK)
numpydoc >=0.6.0             :  0.9.1 (OK)
spyder_kernels >=1.8.0;<2.0.0:  1.8.0 (OK)
qdarkstyle >=2.7             :  2.7 (OK)
atomicwrites >=1.2.0         :  1.3.0 (OK)
diff_match_patch >=20181111  :  20181111 (OK)
watchdog                     :  None (OK)
keyring                      :  None (OK)
pexpect >=4.4.0              :  4.7.0 (OK)
pympler                      :  None (OK)
sympy >=0.7.3                :  1.4 (OK)
cython >=0.21                :  0.29.13 (OK)
IPython >=4.0                :  7.9.0 (OK)
matplotlib >=2.0.0           :  3.1.1 (OK)
pandas >=0.13.1              :  0.25.3 (OK)
numpy >=1.7                  :  1.17.3 (OK)
scipy >=0.17.0               :  1.3.1 (OK)
pyls >=0.31.0;<0.32.0        :  0.31.0 (OK)
rtree >=0.8.3                :  0.8.3 (OK)

```



Discussion:
@dalthviz, please take a look at this one.
Thanks for reporting. This is very serious and we'll try to fix ASAP.
FYI in my testing with Kite not installed, simply hovering over any callable on rc2 results in the error; hovering over objects and modules appears not to. In normal usage, this typically occurs once every 60 seconds or more even without intending to hover. FYI, for users reading this issue, you can temporarily disable hover hints in `Completion and Linting` ---> `Introspection` ---> `Enable hover hints` in Spyder's preferences, which prevents the error from occurring.

Resolving patch:
diff --git a/spyder/plugins/completion/kite/parsing/__init__.py b/spyder/plugins/completion/kite/parsing/__init__.py
--- a/spyder/plugins/completion/kite/parsing/__init__.py
+++ b/spyder/plugins/completion/kite/parsing/__init__.py
@@ -4,9 +4,14 @@
 # Licensed under the terms of the MIT License
 # (see spyder/__init__.py for details)
 
+# Standard library imports
 import logging
 import re
 
+# Local imports
+from spyder.py3compat import PY2
+
+
 ident_re = r'[a-zA-Z_][a-zA-Z0-9_]*'
 dotted_path_re = r'{ident}(?:\.{ident})*'.format(ident=ident_re)
 ident_full_re = ident_re+r'\Z'
@@ -41,8 +46,9 @@ def find_returning_function_path(text, cursor, line_start='\n'):
     if not re.match(ident_full_re, name):
         return None
 
-    # find the previous occurrence of name = dotted.path()
-    name_re = re.escape(name)
+    if PY2:
+        line_start = line_start.encode('utf-8')
+
     assign_re = r'{line_start}\s*{name}\s*=\s*({dotted_path})\('.format(
         line_start=re.escape(line_start),
         name=re.escape(name),
diff --git a/spyder/plugins/editor/widgets/codeeditor.py b/spyder/plugins/editor/widgets/codeeditor.py
--- a/spyder/plugins/editor/widgets/codeeditor.py
+++ b/spyder/plugins/editor/widgets/codeeditor.py
@@ -283,7 +283,7 @@ class CodeEditor(TextEditBaseWidget):
     #: Signal emitted when a response is received from an LSP server
     # For now it's only used on tests, but it could be used to track
     # and profile LSP diagnostics.
-    lsp_response_signal = Signal(str, dict)
+    lsp_response_signal = Signal(str, object)
 
     #: Signal to display object information on the Help plugin
     sig_display_object_info = Signal(str, bool)
@@ -1241,13 +1241,29 @@ def request_hover(self, line, col, offset, show_hint=True, clicked=True):
     @handles(LSPRequestTypes.DOCUMENT_HOVER)
     def handle_hover_response(self, contents):
         """Handle hover response."""
+        if running_under_pytest():
+            try:
+                from unittest.mock import Mock
+            except ImportError:
+                from mock import Mock  # Python 2
+
+            # On some tests this is returning a Mock
+            if isinstance(contents, Mock):
+                return
+
         try:
             content = contents['params']
+
+            if running_under_pytest():
+                # On some tests this is returning a list
+                if isinstance(content, list):
+                    return
+
             self.sig_display_object_info.emit(content,
                                               self._request_hover_clicked)
             if content is not None and self._show_hint and self._last_point:
                 # This is located in spyder/widgets/mixins.py
-                word = self._last_hover_word,
+                word = self._last_hover_word
                 content = content.replace(u'\xa0', ' ')
                 self.show_hint(content, inspect_word=word,
                                at_point=self._last_point)
diff --git a/spyder/plugins/editor/widgets/completion.py b/spyder/plugins/editor/widgets/completion.py
--- a/spyder/plugins/editor/widgets/completion.py
+++ b/spyder/plugins/editor/widgets/completion.py
@@ -145,8 +145,10 @@ def show_list(self, completion_list, position, automatic):
         if not self.is_internal_console:
             tooltip_point = self.rect().topRight()
             tooltip_point = self.mapToGlobal(tooltip_point)
-            for completion in self.completion_list:
-                completion['point'] = tooltip_point
+
+            if self.completion_list is not None:
+                for completion in self.completion_list:
+                    completion['point'] = tooltip_point
 
         # Show hint for first completion element
         self.setCurrentRow(0)


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
@@ -235,7 +235,8 @@ def remove_test_dir():
 # =============================================================================
 @pytest.mark.slow
 @pytest.mark.single_instance
-@pytest.mark.skipif(os.environ.get('CI', None) is None,
+@pytest.mark.skipif((os.environ.get('CI', None) is None or (PY2
+                     and sys.platform == 'darwin')),
                     reason="It's not meant to be run outside of CIs")
 def test_single_instance_and_edit_magic(main_window, qtbot, tmpdir):
     """Test single instance mode and %edit magic."""
@@ -2132,6 +2133,12 @@ def test_pylint_follows_file(qtbot, tmpdir, main_window):
         qtbot.wait(200)
         assert fname == pylint_plugin.get_filename()
 
+    # Close split panel
+    for editorstack in reversed(main_window.editor.editorstacks):
+        editorstack.close_split()
+        break
+    qtbot.wait(1000)
+
 
 @pytest.mark.slow
 @flaky(max_runs=3)
@@ -2388,8 +2395,11 @@ def test_go_to_definition(main_window, qtbot, capsys):
     with qtbot.waitSignal(code_editor.lsp_response_signal):
         code_editor.go_to_definition_from_cursor()
 
-    # Assert there's one more file open than before
-    assert len(main_window.editor.get_filenames()) == n_editors + 1
+    def _get_filenames():
+        return [osp.basename(f) for f in main_window.editor.get_filenames()]
+
+    qtbot.waitUntil(lambda: 'QtCore.py' in _get_filenames())
+    assert 'QtCore.py' in _get_filenames()
 
 
 @pytest.mark.slow
diff --git a/spyder/plugins/editor/widgets/tests/conftest.py b/spyder/plugins/editor/widgets/tests/conftest.py
--- a/spyder/plugins/editor/widgets/tests/conftest.py
+++ b/spyder/plugins/editor/widgets/tests/conftest.py
@@ -29,6 +29,7 @@
 from spyder.plugins.editor.widgets.editor import EditorStack
 from spyder.plugins.completion.plugin import CompletionManager
 from spyder.plugins.explorer.widgets.tests.conftest import create_folders_files
+from spyder.py3compat import PY2, to_text_string
 from spyder.widgets.findreplace import FindReplace
 
 
@@ -99,7 +100,14 @@ def teardown():
     return editor, fallback
 
 
-@pytest.fixture(scope="module")
+# Windows tests fail if using module scope
+if os.name == 'nt':
+    LSP_PLUGIN_SCOPE = 'module'
+else:
+    LSP_PLUGIN_SCOPE = 'function'
+
+
+@pytest.fixture(scope=LSP_PLUGIN_SCOPE)
 def lsp_plugin(qtbot_module, request):
     # Activate pycodestyle and pydocstyle
     CONF.set('lsp-server', 'pycodestyle', True)
@@ -162,12 +170,11 @@ def teardown():
 
 
 @pytest.fixture
-def lsp_codeeditor(lsp_plugin, qtbot_module, request):
+def lsp_codeeditor(lsp_plugin, qtbot_module, request, capsys):
     """CodeEditor instance with LSP services activated."""
     # Create a CodeEditor instance
     editor = codeeditor_factory()
     qtbot_module.addWidget(editor)
-    editor.show()
 
     # Redirect editor LSP requests to lsp_manager
     editor.sig_perform_completion_request.connect(lsp_plugin.send_request)
@@ -183,18 +190,36 @@ def lsp_codeeditor(lsp_plugin, qtbot_module, request):
         editor.document_did_open()
 
     def teardown():
-        editor.hide()
         editor.completion_widget.hide()
+        editor.tooltip_widget.hide()
+        editor.hide()
+
+        # Capture stderr and assert there are no errors
+        sys_stream = capsys.readouterr()
+        sys_err = sys_stream.err
+
+        if PY2:
+            sys_err = to_text_string(sys_err).encode('utf-8')
+        assert sys_err == ''
 
     request.addfinalizer(teardown)
     lsp_plugin = lsp_plugin.get_client('lsp')
+
+    editor.show()
+
     return editor, lsp_plugin
 
 
 @pytest.fixture
-def search_codeeditor(lsp_codeeditor, qtbot_module):
+def search_codeeditor(lsp_codeeditor, qtbot_module, request):
     code_editor, _ = lsp_codeeditor
     find_replace = FindReplace(None, enable_replace=True)
     find_replace.set_editor(code_editor)
     qtbot_module.addWidget(find_replace)
+
+    def teardown():
+        find_replace.hide()
+
+    request.addfinalizer(teardown)
+
     return code_editor, find_replace
diff --git a/spyder/plugins/editor/widgets/tests/test_hints_and_calltips.py b/spyder/plugins/editor/widgets/tests/test_hints_and_calltips.py
--- a/spyder/plugins/editor/widgets/tests/test_hints_and_calltips.py
+++ b/spyder/plugins/editor/widgets/tests/test_hints_and_calltips.py
@@ -15,7 +15,6 @@
 from qtpy.QtGui import QTextCursor
 import pytest
 
-
 # Constants
 PY2 = sys.version[0] == '2'
 TEST_SIG = 'some_function(foo={}, hello=None)'
@@ -98,17 +97,19 @@ def test_get_calltips(qtbot, lsp_codeeditor, params):
 
 @pytest.mark.slow
 @pytest.mark.second
-@pytest.mark.skipif(os.name == 'nt', reason="Fails on Windows")
+@pytest.mark.skipif(sys.platform != 'darwin' and bool(os.environ.get('CI')),
+                    reason="Fails on Windows and Linux on CI")
 @pytest.mark.parametrize('params', [
             # Parameter, Expected Output
             ('dict', '' if PY2 else 'dict'),
             ('type', 'type'),
+            ('range', 'range'),
             ('"".format', '-> str'),
             ('import math', 'module'),
             (TEST_TEXT, TEST_DOCSTRING)
         ]
     )
-def test_get_hints(qtbot, lsp_codeeditor, params):
+def test_get_hints(qtbot, lsp_codeeditor, params, capsys):
     """Test that the editor is returning hover hints."""
     code_editor, _ = lsp_codeeditor
     param, expected_output_text = params
@@ -116,30 +117,25 @@ def test_get_hints(qtbot, lsp_codeeditor, params):
     # Set text in editor
     code_editor.set_text(param)
 
-    # Moe cursor to end of line
-    code_editor.moveCursor(QTextCursor.End)
-
     # Get cursor coordinates
+    code_editor.moveCursor(QTextCursor.End)
+    qtbot.keyPress(code_editor, Qt.Key_Left)
     x, y = code_editor.get_coordinates('cursor')
-    # Get cursor position in characters
-    offset = code_editor.get_position('cursor')
-
-    # The `- 5` is to put the mouse on top of the word
-    point = code_editor.calculate_real_position(QPoint(x - 5, y))
+    point = code_editor.calculate_real_position(QPoint(x, y))
 
-    code_editor.tooltip_widget.hide()
     with qtbot.waitSignal(code_editor.sig_display_object_info,
                           timeout=30000) as blocker:
-        cursor = code_editor.cursorForPosition(point)
-        line, col = cursor.blockNumber(), cursor.columnNumber()
-        code_editor.request_hover(line, col, offset)
-
-        # This is needed to leave time for the tooltip to appear
-        # and make the tests succeed
-        qtbot.wait(2000)
+        qtbot.mouseMove(code_editor, point)
+        qtbot.mouseClick(code_editor, Qt.LeftButton, pos=point)
+        qtbot.waitUntil(lambda: code_editor.tooltip_widget.isVisible(),
+                        timeout=10000)
 
         args = blocker.args
         print('args:', [args])
         output_text = args[0]
         assert expected_output_text in output_text
         code_editor.tooltip_widget.hide()
+
+        # This checks that code_editor.log_lsp_handle_errors was not called
+        captured = capsys.readouterr()
+        assert captured.err == ''
diff --git a/spyder/plugins/editor/widgets/tests/test_introspection.py b/spyder/plugins/editor/widgets/tests/test_introspection.py
--- a/spyder/plugins/editor/widgets/tests/test_introspection.py
+++ b/spyder/plugins/editor/widgets/tests/test_introspection.py
@@ -87,9 +87,13 @@ def test_space_completion(lsp_codeeditor, qtbot):
     code_editor.toggle_automatic_completions(True)
     code_editor.toggle_code_snippets(True)
 
+
 @pytest.mark.slow
 @pytest.mark.first
 @flaky(max_runs=5)
+@pytest.mark.skipif(
+    os.environ.get('CI') and (PY2 or os.name != 'nt'),
+    reason='Fails consistently on CI with Linux/Mac or Python 2')
 def test_hide_widget_completion(lsp_codeeditor, qtbot):
     """Validate hiding completion widget after a delimeter or operator."""
     code_editor, _ = lsp_codeeditor
@@ -103,6 +107,8 @@ def test_hide_widget_completion(lsp_codeeditor, qtbot):
     code_editor.toggle_code_snippets(False)
 
     # Set cursor to start
+    code_editor.set_text('')
+    code_editor.completion_widget.hide()
     code_editor.go_to_line(1)
 
     # Complete from numpy import --> from numpy import ?
@@ -110,17 +116,19 @@ def test_hide_widget_completion(lsp_codeeditor, qtbot):
     with qtbot.waitSignal(code_editor.lsp_response_signal, timeout=30000):
         code_editor.document_did_change()
 
-    # press tab and get completions
+    # Press tab and get completions
     with qtbot.waitSignal(completion.sig_show_completions,
-                          timeout=10000) as sig:
+                          timeout=10000):
         qtbot.keyPress(code_editor, Qt.Key_Tab)
 
     # Check the completion widget is visible
     assert completion.isHidden() is False
 
     # Write a random delimeter on the code editor
-    delimeter = random.choice(delimiters)
-    qtbot.keyClicks(code_editor, delimeter)
+    delimiter = random.choice(delimiters)
+    print(delimiter)
+    qtbot.keyClicks(code_editor, delimiter)
+    qtbot.wait(1000)
 
     # Check the completion widget is not visible
     assert completion.isHidden() is True
@@ -132,6 +140,9 @@ def test_hide_widget_completion(lsp_codeeditor, qtbot):
 @pytest.mark.slow
 @pytest.mark.first
 @flaky(max_runs=5)
+@pytest.mark.skipif(
+    os.environ.get('CI') and (PY2 and os.name != 'nt'),
+    reason='Fails on CI with Mac/Linux and Python 2')
 def test_automatic_completions(lsp_codeeditor, qtbot):
     """Test on-the-fly completions."""
     code_editor, _ = lsp_codeeditor
diff --git a/spyder/plugins/editor/widgets/tests/test_warnings.py b/spyder/plugins/editor/widgets/tests/test_warnings.py
--- a/spyder/plugins/editor/widgets/tests/test_warnings.py
+++ b/spyder/plugins/editor/widgets/tests/test_warnings.py
@@ -43,17 +43,7 @@ def test_ignore_warnings(qtbot, lsp_codeeditor):
 
     # After this call the manager needs to be reinitialized
     manager.update_configuration()
-
-    if os.environ.get('CI', None) is None and sys.platform == 'darwin':
-        # To be able to run local tests on mac this modification is needed
-        editorstack = manager.main.editor
-        with qtbot.waitSignal(editorstack.sig_lsp_initialized, timeout=30000):
-            manager.start_client('python')
-
-        with qtbot.waitSignal(editor.lsp_response_signal, timeout=30000):
-            editor.document_did_open()
-    else:
-        qtbot.wait(2000)
+    qtbot.wait(2000)
 
     # Notify changes
     with qtbot.waitSignal(editor.lsp_response_signal, timeout=30000):