Repository: spyder-ide/spyder
Pull request: PR: Fix hover/calltip for Python objects without signature
<!--- Make sure to read the Contributing Guidelines:                   --->
<!--- https://github.com/spyder-ide/spyder/blob/master/CONTRIBUTING.md --->
<!--- and follow PEP 8, PEP 257 and Spyder's code style:               --->
<!--- https://github.com/spyder-ide/spyder/wiki/Dev:-Coding-Style      --->

## Description of Changes


* [x] Wrote at least one-line docstrings (for any new functions)
* [x] Added unit test(s) covering the changes (if testable)
<!--- Remember that an image/animation is worth a thousand words! --->
* [x] Included a screenshot or animation (if affecting the UI, see [Licecap](https://www.cockos.com/licecap/))

![hints](https://user-images.githubusercontent.com/3627835/57564411-b9aa7c00-7370-11e9-8a91-49a53bc81d35.gif)


<!--- Explain what you've done and why --->




### Issue(s) Resolved

<!--- List the issue(s) below, in the form "Fixes #1234"; one per line --->

Fixes #9294

### Affirmation

By submitting this Pull Request or typing my (user)name below,
I affirm the [Developer Certificate of Origin](https://developercertificate.org)
with respect to all commits and content included in this PR,
and understand I am releasing the same under Spyder's MIT (Expat) license.

<!--- TYPE YOUR USER/NAME AFTER THE FOLLOWING: --->
I certify the above statement is true and correct: @goanpeca 

<!--- Thanks for your help making Spyder better for everyone! --->


Problem statement:
Error when getting hover of "dict"
## Description

### What steps will reproduce the problem?

<!--- You can use Markdown here --->

I got the following error after writing

```python
dict()
```

in the editor and placing the mouse on top of it.

### Traceback
```python-traceback
    at_point=self._last_point)
    signature = self._format_signature(signature)
    parameter=parameter,
IndexError: tuple index out of range

Traceback (most recent call last):
    msg = self.format(record)
    return fmt.format(record)
    record.message = record.getMessage()
    msg = msg % self.args
ValueError: incomplete format
Call stack:
    start.main()
    mainwindow.main()
    mainwindow = run_spyder(app, options, args)
    app.exec_()
    handler(resp['result'], req_id)
    {'params': contents})
    handler(params)
    self.log_lsp_handle_errors("Error when processing hover")
    logger.error('%', 1, stack_info=True)
Arguments: (1,)
```

## Versions

* Spyder version: 4.0.0.dev0 061f7c3d0
* Python version: 3.6.6
* Qt version: 5.9.6
* PyQt5 version: 5.9.2
* Operating System: Linux 4.13.16-041316-generic

### Dependencies

```
pygments >=2.0    :  2.3.1 (OK)
qdarkstyle >=2.6.4:  2.6.5 (OK)
sphinx >=0.6.6    :  1.8.2 (OK)
psutil >=0.3      :  5.4.8 (OK)
pyls >=0.19.0     :  0.90.0.dev0 (OK)
nbconvert >=4.0   :  5.4.1 (OK)
pandas >=0.13.1   :  0.23.4 (OK)
numpy >=1.7       :  1.15.4 (OK)
sympy >=0.7.3     :  1.3 (OK)
cython >=0.21     :  0.29.2 (OK)
qtconsole >=4.2.0 :  4.4.2 (OK)
IPython >=4.0     :  7.3.0 (OK)
matplotlib >=2.0.0:  3.0.2 (OK)
pylint >=0.25     :  2.1.1 (OK)
```

Hints from issue discussion:
@goanpeca, please take a look at this one.
I'm also getting the following error when getting the signature of `dict`:

```python-traceback
    parameter=parameter,
    parameter=parameter,
    parameter=parameter,
IndexError: tuple index out of range

Traceback (most recent call last):
    msg = self.format(record)
    return fmt.format(record)
    record.message = record.getMessage()
    msg = msg % self.args
ValueError: incomplete format
Call stack:
    start.main()
    mainwindow.main()
    mainwindow = run_spyder(app, options, args)
    app.exec_()
    handler(resp['result'], req_id)
    {'params': response})
    handler(params)
    self.log_lsp_handle_errors("Error when processing signature")
    logger.error('%', 1, stack_info=True)
Arguments: (1,)
```

It seems to be the originated by the same cause.
@ccordoba12 I am not sure how to handle this since the original logic was expecting something like from the LSP:

```python
'Popen(self, args, bufsize=-1, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=True, shell=False, cwd=None, env=None, universal_newlines=None, startupinfo=None, creationflags=0, restore_signals=True, start_new_session=False, pass_fds=(), *, encoding=None, errors=None, text=None)'
```

But for some builtins (that allow different ways of calling)
we get 

```python
'dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list.  For example:  dict(one=1, two=2)'
```

Which is not really a signature, but a dosctring, so how should we handle these cases, since the highlight of parameter logic expects 1 signature, not more than that.
I could check for now for `->` which seems a specific character of these methods and simply not color  anything.
> I could check for now for -> which seems a specific character of these methods and simply not color anything.

I think that's not robust enough. The simplest solution should be: if you can't find a signature, simply return the docstring.

Commit message: Split and handle multiple signatures

Patch to review:
diff --git a/spyder/plugins/editor/widgets/codeeditor.py b/spyder/plugins/editor/widgets/codeeditor.py
index 04a0620d6a3..a3884b9f746 100644
--- a/spyder/plugins/editor/widgets/codeeditor.py
+++ b/spyder/plugins/editor/widgets/codeeditor.py
@@ -968,6 +968,7 @@ def handle_hover_response(self, contents):
                 if self._show_hint and self._last_point:
                     # This is located in spyder/widgets/mixins.py
                     word = self._last_hover_word,
+                    content = content.replace(u'\xa0', ' ')
                     self.show_hint(content, inspect_word=word,
                                    at_point=self._last_point)
                     self._last_point = None
diff --git a/spyder/widgets/mixins.py b/spyder/widgets/mixins.py
index 3f1d744578d..9b1b6680666 100644
--- a/spyder/widgets/mixins.py
+++ b/spyder/widgets/mixins.py
@@ -247,7 +247,7 @@ def _format_text(self, title=None, signature=None, text=None,
 
         return template
 
-    def _format_signature(self, signature, parameter=None,
+    def _format_signature(self, signatures, parameter=None,
                           parameter_color=_PARAMETER_HIGHLIGHT_COLOR,
                           char_color=_CHAR_HIGHLIGHT_COLOR):
         """
@@ -281,51 +281,121 @@ def handle_sub(matchobj):
             new = match.replace(parameter, active_parameter_template)
             return new
 
-        # Remove duplicate spaces
-        signature = ' '.join(signature.split())
+        if not isinstance(signatures, list):
+            signatures = [signatures]
 
-        # Replace ay initial spaces
-        signature = signature.replace('( ', '(')
+        new_signatures = []
+        for signature in signatures:
+            # Remove duplicate spaces
+            signature = ' '.join(signature.split())
 
-        # Process signature template
-        if parameter:
-            pattern = r'[\*|(|\s](' + parameter + r')[,|)|\s|=]'
+            # Replace initial spaces
+            signature = signature.replace('( ', '(')
 
-        formatted_lines = []
-        name = signature.split('(')[0]
-        indent = ' ' * (len(name) + 1)
-        rows = textwrap.wrap(signature, width=60, subsequent_indent=indent)
-        for row in rows:
+            # Process signature template
             if parameter:
-                # Add template to highlight the active parameter
-                row = re.sub(pattern, handle_sub, row)
+                pattern = r'[\*|(|\s](' + parameter + r')[,|)|\s|=]'
+
+            formatted_lines = []
+            name = signature.split('(')[0]
+            indent = ' ' * (len(name) + 1)
+            rows = textwrap.wrap(signature, width=60, subsequent_indent=indent)
+            for row in rows:
+                if parameter:
+                    # Add template to highlight the active parameter
+                    row = re.sub(pattern, handle_sub, row)
+
+                row = row.replace(' ', '&nbsp;')
+                row = row.replace('span&nbsp;', 'span ')
+
+                language = getattr(self, 'language', None)
+                if language and 'python' == language.lower():
+                    for char in ['(', ')', ',', '*', '**']:
+                        new_char = chars_template.format(char=char)
+                        row = row.replace(char, new_char)
+
+                formatted_lines.append(row)
+            title_template = '<br>'.join(formatted_lines)
+
+            # Get current font properties
+            font = self.font()
+            font_size = font.pointSize()
+            font_family = font.family()
+
+            # Format title to display active parameter
+            if parameter:
+                title = title_template.format(
+                    font_size=font_size,
+                    font_family=font_family,
+                    color=parameter_color,
+                    parameter=parameter,
+                )
+            else:
+                title = title_template
+            new_signatures.append(title)
 
-            row = row.replace(' ', '&nbsp;')
-            row = row.replace('span&nbsp;', 'span ')
+        return '<br><br>'.join(new_signatures)
 
-            language = getattr(self, 'language', None)
-            if language and 'python' == language.lower():
-                for char in ['(', ')', ',', '*', '**']:
-                    new_char = chars_template.format(char=char)
-                    row = row.replace(char, new_char)
+    def _check_signature_and_format(self, signature_or_text, parameter=None):
+        """
+        LSP hints might provide docstrings instead of signatures.
 
-            formatted_lines.append(row)
-        title_template = '<br>'.join(formatted_lines)
+        This method will check for multiple signatures (dict, type etc...) and
+        format the text accordingly.
+        """
+        open_func_char = ''
+        has_signature = False
+        has_multisignature = False
+        language = getattr(self, 'language', '').lower()
+        lines = signature_or_text.split('\n')
+
+        if language == 'python':
+            open_func_char = '('
+            idx = signature_or_text.find(open_func_char)
+            inspect_word = signature_or_text[:idx]
+            name_plus_char = inspect_word + open_func_char
+
+            # Signature type
+            count = signature_or_text.count(name_plus_char)
+            has_signature = open_func_char in lines[0]
+            has_multisignature = count > 1
+
+        if has_signature and not has_multisignature:
+            for i, line in enumerate(lines):
+                if line.strip() == '':
+                    break
 
-        # Get current font properties
-        font = self.font()
-        font_size = font.pointSize()
-        font_family = font.family()
+            if i == 0:
+                signature = lines[0]
+                extra_text = None
+            else:
+                signature = '\n'.join(lines[:i])
+                extra_text = '\n'.join(lines[i:])
 
-        # Format title to display active parameter
-        title = title_template.format(
-            font_size=font_size,
-            font_family=font_family,
-            color=parameter_color,
-            parameter=parameter,
-        )
+            if signature:
+                new_signature = self._format_signature(
+                    signatures=signature,
+                    parameter=parameter,
+                )
+        elif has_multisignature:
+            signature = signature_or_text.replace(name_plus_char,
+                                                  '<br>' + name_plus_char)
+            signature = signature[4:]  # Remove the first line break
+            signature = signature.replace('\n', ' ')
+            signature = signature.replace('<br>', '\n')
+            signatures = signature.split('\n')
+            signatures = [sig for sig in signatures if sig]  # Remove empty
+
+            new_signature = self._format_signature(
+                signatures=signatures,
+                parameter=parameter,
+            )
+            extra_text = None
+        else:
+            new_signature = None
+            extra_text = signature_or_text
 
-        return title
+        return new_signature, extra_text, inspect_word
 
     def show_calltip(self, signature, parameter=None):
         """
@@ -338,19 +408,15 @@ def show_calltip(self, signature, parameter=None):
         # Find position of calltip
         point = self._calculate_position()
 
-        # Format signature
-        html_signature = self._format_signature(
-            signature=signature,
-            parameter=parameter,
-        )
-
-        inspect_word = signature.split('(')[0]
+        # Format
+        res = self._check_signature_and_format(signature, parameter)
+        new_signature, text, inspect_word = res
         text = self._format_text(
-            signature=html_signature,
+            signature=new_signature,
+            text=text,
             inspect_word=inspect_word,
             display_link=False,
         )
-
         self._update_stylesheet(self.calltip_widget)
 
         # Show calltip
@@ -387,29 +453,9 @@ def show_tooltip(self, title=None, signature=None, text=None,
 
     def show_hint(self, text, inspect_word, at_point):
         """Show code hint and crop text as needed."""
-        # Check language
-        lines = text.split('\n')
-        is_signature = False
-        if 'python' in self.language.lower():
-            is_signature = '(' in lines[0]
-
-        # Split signature and the rest
-        signature, additional_text = None, text
-        if is_signature:
-            for i, line in enumerate(lines):
-                if line.strip() == '':
-                    break
-            if i == 0:
-                signature = lines[0]
-                additional_text = None
-            else:
-                signature = '\n'.join(lines[:i])
-                additional_text = '\n'.join(lines[i:])
-
-            if signature:
-                signature = self._format_signature(signature)
-
         # Check if signature and format
+        res = self._check_signature_and_format(text)
+        html_signature, extra_text, _ = res
         point = self.get_word_start_pos(at_point)
 
         # This is needed to get hover hints
@@ -417,7 +463,7 @@ def show_hint(self, text, inspect_word, at_point):
         cursor.movePosition(QTextCursor.StartOfWord, QTextCursor.MoveAnchor)
         self._last_hover_cursor = cursor
 
-        self.show_tooltip(signature=signature, text=additional_text,
+        self.show_tooltip(signature=html_signature, text=extra_text,
                           at_point=point, inspect_word=inspect_word,
                           display_link=True, max_lines=10)
 


Code review comments:
diff_hunk: @@ -275,11 +275,11 @@ class CodeEditor(TextEditBaseWidget):
     lsp_response_signal = Signal(str, dict)
 
     #: Signal to display object information on the Help plugin
-    sig_display_object_info = Signal(str)
+    sig_display_object_info = Signal(object)
 
     #: Signal only used for tests
     # TODO: Remove it!
-    sig_signature_invoked = Signal()
+    sig_signature_invoked = Signal(object)
path: spyder/plugins/editor/widgets/codeeditor.py
text: @user1:
Also this one?

@author:
Same as above, but in this case I need to be able to check on the tests what the reply was 

https://github.com/spyder-ide/spyder/pull/9301/files#diff-d6fbacae4782021d26229f59f1a4a082R101

diff_hunk: @@ -0,0 +1,106 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+#
+"""Tests for editor calltips and hover hints tooltips."""
+
+# Standard library imports
+import sys
+
+# Third party imports
+from qtpy.QtCore import Qt, QPoint
+from qtpy.QtGui import QTextCursor
+import pytest
+
+# Constants
+PY2 = sys.version[0] == '2'
+TEST_SIG = 'some_function(hello=None)'
+TEST_DOCSTRING = "This is the test docstring."
+TEST_TEXT = """'''Testing something'''
+def {SIG}:
+    '''{DOC}'''
+
+
+some_function""".format(SIG=TEST_SIG, DOC=TEST_DOCSTRING)
+
+
+@pytest.mark.slow
+@pytest.mark.parametrize('params', [
+            # Parameter, Expected Output
+            ('dict', 'dict'),
+            ('type', 'type'),
+            (TEST_TEXT, TEST_SIG)
+        ]
+    )
+def test_get_calltips(qtbot, lsp_codeeditor, params):
+    """Test that the editor is returning hints."""
+    code_editor, _ = lsp_codeeditor
+
+    param, expected_output_text = params
+
+    # Set text in editor
+    code_editor.set_text(param)
+
+    code_editor.moveCursor(QTextCursor.End)
+
+    with qtbot.waitSignal(code_editor.sig_signature_invoked,
+                          timeout=30000) as blocker:
+        code_editor.document_did_change('')
+        qtbot.keyPress(code_editor, Qt.Key_ParenLeft, delay=1000)
+        qtbot.wait(3000)
+
+        # Not working on some CI
+        # assert code_editor.calltip_widget.isVisible()
+
+        signatures = blocker.args[0]['params']['signatures']
+        assert expected_output_text in signatures['label']
+
+
+# @pytest.mark.slow
+@pytest.mark.parametrize('params', [
+            # Parameter, Expected Output
+            ('dict', '' if PY2 else 'dict'),
+            ('type', 'type'),
+            ('import math', 'module'),
+            (TEST_TEXT, TEST_DOCSTRING)
+        ]
+    )
+def test_get_hints(qtbot, lsp_codeeditor, params):
+    """Test that the editor is returning hover hints."""
+    code_editor, _ = lsp_codeeditor
+
+    param, expected_output_text = params
+
+    # Set text in editor
+    code_editor.set_text(param)
+
+    code_editor.moveCursor(QTextCursor.End)
+    x, y = code_editor.get_coordinates('cursor')
+
+    # The `- 5` is to put the mouse on top of the word
+    point = code_editor.calculate_real_position(QPoint(x - 5, y))
+
+    with qtbot.waitSignal(code_editor.sig_display_object_info,
+                          timeout=30000) as blocker:
+        code_editor.hide_tooltip()
+        cursor = code_editor.cursorForPosition(point)
+        line, col = cursor.blockNumber(), cursor.columnNumber()
+        code_editor.request_hover(line, col)
+        qtbot.wait(3000)
+
+        # Mouse move/hover events are flaky on CI
+        # code_editor.request_hover(self, line, col, show_hint=True)
+        # code_editor.document_did_change('')
+        # qtbot.mouseMove(code_editor, pos=point, delay=2000)
+        # code_editor.document_did_change('')
+        # qtbot.mouseClick(code_editor, Qt.LeftButton, pos=point, delay=2000)
+        # assert code_editor.tooltip_widget.isVisible()
path: spyder/plugins/editor/widgets/tests/test_hints_and_calltips.py
text: @user1:
Please remove these commented lines if they are not necessary.

@author:
Will do

diff_hunk: @@ -0,0 +1,106 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+#
+"""Tests for editor calltips and hover hints tooltips."""
+
+# Standard library imports
+import sys
+
+# Third party imports
+from qtpy.QtCore import Qt, QPoint
+from qtpy.QtGui import QTextCursor
+import pytest
+
+# Constants
+PY2 = sys.version[0] == '2'
+TEST_SIG = 'some_function(hello=None)'
+TEST_DOCSTRING = "This is the test docstring."
+TEST_TEXT = """'''Testing something'''
+def {SIG}:
+    '''{DOC}'''
+
+
+some_function""".format(SIG=TEST_SIG, DOC=TEST_DOCSTRING)
+
+
+@pytest.mark.slow
+@pytest.mark.parametrize('params', [
+            # Parameter, Expected Output
+            ('dict', 'dict'),
+            ('type', 'type'),
+            (TEST_TEXT, TEST_SIG)
+        ]
+    )
+def test_get_calltips(qtbot, lsp_codeeditor, params):
+    """Test that the editor is returning hints."""
+    code_editor, _ = lsp_codeeditor
+
+    param, expected_output_text = params
+
+    # Set text in editor
+    code_editor.set_text(param)
+
+    code_editor.moveCursor(QTextCursor.End)
+
+    with qtbot.waitSignal(code_editor.sig_signature_invoked,
+                          timeout=30000) as blocker:
+        code_editor.document_did_change('')
+        qtbot.keyPress(code_editor, Qt.Key_ParenLeft, delay=1000)
+        qtbot.wait(3000)
+
+        # Not working on some CI
+        # assert code_editor.calltip_widget.isVisible()
+
+        signatures = blocker.args[0]['params']['signatures']
+        assert expected_output_text in signatures['label']
+
+
+# @pytest.mark.slow
+@pytest.mark.parametrize('params', [
+            # Parameter, Expected Output
+            ('dict', '' if PY2 else 'dict'),
+            ('type', 'type'),
+            ('import math', 'module'),
+            (TEST_TEXT, TEST_DOCSTRING)
+        ]
+    )
+def test_get_hints(qtbot, lsp_codeeditor, params):
+    """Test that the editor is returning hover hints."""
+    code_editor, _ = lsp_codeeditor
+
+    param, expected_output_text = params
+
+    # Set text in editor
+    code_editor.set_text(param)
+
+    code_editor.moveCursor(QTextCursor.End)
+    x, y = code_editor.get_coordinates('cursor')
+
+    # The `- 5` is to put the mouse on top of the word
+    point = code_editor.calculate_real_position(QPoint(x - 5, y))
+
+    with qtbot.waitSignal(code_editor.sig_display_object_info,
+                          timeout=30000) as blocker:
+        code_editor.hide_tooltip()
path: spyder/plugins/editor/widgets/tests/test_hints_and_calltips.py
text: @user1:
This should be put before the `with`, right?

@author:
This might not be needed anymore now that I am checking the output of the signal and not the visibility of the hover hints

diff_hunk: @@ -0,0 +1,106 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+#
+"""Tests for editor calltips and hover hints tooltips."""
+
+# Standard library imports
+import sys
+
+# Third party imports
+from qtpy.QtCore import Qt, QPoint
+from qtpy.QtGui import QTextCursor
+import pytest
+
+# Constants
+PY2 = sys.version[0] == '2'
+TEST_SIG = 'some_function(hello=None)'
+TEST_DOCSTRING = "This is the test docstring."
+TEST_TEXT = """'''Testing something'''
+def {SIG}:
+    '''{DOC}'''
+
+
+some_function""".format(SIG=TEST_SIG, DOC=TEST_DOCSTRING)
+
+
+@pytest.mark.slow
+@pytest.mark.parametrize('params', [
+            # Parameter, Expected Output
+            ('dict', 'dict'),
+            ('type', 'type'),
+            (TEST_TEXT, TEST_SIG)
+        ]
+    )
+def test_get_calltips(qtbot, lsp_codeeditor, params):
+    """Test that the editor is returning hints."""
+    code_editor, _ = lsp_codeeditor
+
+    param, expected_output_text = params
+
+    # Set text in editor
+    code_editor.set_text(param)
+
+    code_editor.moveCursor(QTextCursor.End)
+
+    with qtbot.waitSignal(code_editor.sig_signature_invoked,
+                          timeout=30000) as blocker:
+        code_editor.document_did_change('')
+        qtbot.keyPress(code_editor, Qt.Key_ParenLeft, delay=1000)
+        qtbot.wait(3000)
+
+        # Not working on some CI
+        # assert code_editor.calltip_widget.isVisible()
+
+        signatures = blocker.args[0]['params']['signatures']
+        assert expected_output_text in signatures['label']
+
+
+# @pytest.mark.slow
path: spyder/plugins/editor/widgets/tests/test_hints_and_calltips.py
text: @user1:
Please revert this and mark as `second`.

@author:
Will do

diff_hunk: @@ -0,0 +1,106 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+#
+"""Tests for editor calltips and hover hints tooltips."""
+
+# Standard library imports
+import sys
+
+# Third party imports
+from qtpy.QtCore import Qt, QPoint
+from qtpy.QtGui import QTextCursor
+import pytest
+
+# Constants
+PY2 = sys.version[0] == '2'
+TEST_SIG = 'some_function(hello=None)'
+TEST_DOCSTRING = "This is the test docstring."
+TEST_TEXT = """'''Testing something'''
+def {SIG}:
+    '''{DOC}'''
+
+
+some_function""".format(SIG=TEST_SIG, DOC=TEST_DOCSTRING)
+
+
+@pytest.mark.slow
+@pytest.mark.parametrize('params', [
+            # Parameter, Expected Output
+            ('dict', 'dict'),
+            ('type', 'type'),
+            (TEST_TEXT, TEST_SIG)
+        ]
+    )
+def test_get_calltips(qtbot, lsp_codeeditor, params):
+    """Test that the editor is returning hints."""
+    code_editor, _ = lsp_codeeditor
+
+    param, expected_output_text = params
+
+    # Set text in editor
+    code_editor.set_text(param)
+
+    code_editor.moveCursor(QTextCursor.End)
+
+    with qtbot.waitSignal(code_editor.sig_signature_invoked,
+                          timeout=30000) as blocker:
+        code_editor.document_did_change('')
+        qtbot.keyPress(code_editor, Qt.Key_ParenLeft, delay=1000)
+        qtbot.wait(3000)
+
+        # Not working on some CI
+        # assert code_editor.calltip_widget.isVisible()
+
+        signatures = blocker.args[0]['params']['signatures']
+        assert expected_output_text in signatures['label']
+
+
+# @pytest.mark.slow
+@pytest.mark.parametrize('params', [
+            # Parameter, Expected Output
+            ('dict', '' if PY2 else 'dict'),
+            ('type', 'type'),
+            ('import math', 'module'),
+            (TEST_TEXT, TEST_DOCSTRING)
+        ]
+    )
+def test_get_hints(qtbot, lsp_codeeditor, params):
+    """Test that the editor is returning hover hints."""
+    code_editor, _ = lsp_codeeditor
+
+    param, expected_output_text = params
+
+    # Set text in editor
+    code_editor.set_text(param)
+
+    code_editor.moveCursor(QTextCursor.End)
+    x, y = code_editor.get_coordinates('cursor')
+
+    # The `- 5` is to put the mouse on top of the word
+    point = code_editor.calculate_real_position(QPoint(x - 5, y))
+
+    with qtbot.waitSignal(code_editor.sig_display_object_info,
+                          timeout=30000) as blocker:
+        code_editor.hide_tooltip()
+        cursor = code_editor.cursorForPosition(point)
+        line, col = cursor.blockNumber(), cursor.columnNumber()
+        code_editor.request_hover(line, col)
+        qtbot.wait(3000)
path: spyder/plugins/editor/widgets/tests/test_hints_and_calltips.py
text: @user1:
Is this line really necessary? I mean, immediately after the PyLS response comes to Spyder `sig_display_object_info` is emitted, so there shouldn't be any need fo wait longer here.

@author:
Yes, agreed (this is a left over from the checks that where actually moving the mouse)