Repository: spyder-ide/spyder

Issue:
Help pane doesn't accept aliased modules
**Steps to reproduce:**
* Import some module in Python with an alias - e.g.
```python
import numpy as np
```
(not `import numpy`)
* Type the name of one of the imported module's function/classes in the help pane along with its prefix - e.g. `np.arange`
* Press enter.

**Expected behavior:** should show the help for the function/class that was requested. Should also ideally not need an enter keypress, like it was in 3.3.1.

**Actual behavior:** deletes what was just written and goes back to what was previously there, or to blank if nothing was written before. As such, cannot get help for a function inside an aliased package unless importing it like
```python
from numpy import arange
```
Will also not take it with the package's full name - e.g. `numpy.arange` would also not work.

**Setup:** Spyder 3.3.4, running on windows 10, installed from Anaconda, Python 3.6.8.


Discussion:
I can confirm this in master as well. 

`ctrl+i` on `np.arange` in the console works as expected.
Is this a regression ? or this has been a bug for some time?
> Is this a regression ? or this has been a bug for some time?

It's a regression, was working fine in 3.3.3 when copy-pasted, and fine in 3.3.1 when written.
@dalthviz, please take a look at this one. I think it's related with the changes you made in 3.3.4 to try to fix another problem in the search field of the Help pane.

Resolving patch:
diff --git a/spyder/plugins/help.py b/spyder/plugins/help.py
--- a/spyder/plugins/help.py
+++ b/spyder/plugins/help.py
@@ -68,21 +68,24 @@ def is_valid(self, qstr=None):
         if not re.search(r'^[a-zA-Z0-9_\.]*$', str(qstr), 0):
             return False
         objtxt = to_text_string(qstr)
+        shell_is_defined = False
         if self.help.get_option('automatic_import'):
             shell = self.help.internal_shell
             if shell is not None:
-                return shell.is_defined(objtxt, force_import=True)
-        shell = self.help.get_shell()
-        if shell is not None:
-            try:
-                return shell.is_defined(objtxt)
-            except socket.error:
-                shell = self.help.get_shell()
+                shell_is_defined = shell.is_defined(objtxt, force_import=True)
+        if not shell_is_defined:
+            shell = self.help.get_shell()
+            if shell is not None:
                 try:
-                    return shell.is_defined(objtxt)
+                    shell_is_defined = shell.is_defined(objtxt)
                 except socket.error:
-                    # Well... too bad!
-                    pass
+                    shell = self.help.get_shell()
+                    try:
+                        shell_is_defined = shell.is_defined(objtxt)
+                    except socket.error:
+                        # Well... too bad!
+                        pass
+        return shell_is_defined
 
     def validate_current_text(self):
         self.validate(self.currentText())
@@ -91,7 +94,7 @@ def validate(self, qstr, editing=True):
         """Reimplemented to avoid formatting actions"""
         valid = self.is_valid(qstr)
         if self.hasFocus() and valid is not None:
-            if editing:
+            if editing and not valid:
                 # Combo box text is being modified: invalidate the entry
                 self.show_tip(self.tips[valid])
                 self.valid.emit(False, False)
@@ -397,7 +400,7 @@ def __init__(self, parent=None):
         self.combo.setMaxCount(self.get_option('max_history_entries'))
         self.combo.addItems( self.load_history() )
         self.combo.setItemText(0, '')
-        self.combo.valid.connect(lambda valid: self.force_refresh(valid))
+        self.combo.valid.connect(self.force_refresh)
 
         # Plain text docstring option
         self.docstring = True
@@ -749,8 +752,11 @@ def handle_link_clicks(self, url):
         else:
             self.rich_text.webview.load(QUrl(url))
 
-    #------ Public API ---------------------------------------------------------
-    def force_refresh(self, valid=True):
+    # ------ Public API -------------------------------------------------------
+    @Slot()
+    @Slot(bool)
+    @Slot(bool, bool)
+    def force_refresh(self, valid=True, editing=True):
         if valid:
             if self.source_is_console():
                 self.set_object_text(None, force_refresh=True)


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
@@ -266,6 +266,58 @@ def test_filter_numpy_warning(main_window, qtbot):
     CONF.set('variable_explorer', 'minmax', False)
 
 
+@pytest.mark.slow
+@flaky(max_runs=3)
+@pytest.mark.skipif(PY2, reason="Times out in PY2")
+def test_get_help_combo(main_window, qtbot):
+    """
+    Test that Help can display docstrings for names typed in its combobox.
+    """
+    shell = main_window.ipyconsole.get_current_shellwidget()
+    qtbot.waitUntil(lambda: shell._prompt_html is not None,
+                    timeout=SHELL_TIMEOUT)
+
+    help_plugin = main_window.help
+    webview = help_plugin.rich_text.webview._webview
+    if WEBENGINE:
+        webpage = webview.page()
+    else:
+        webpage = webview.page().mainFrame()
+
+    # --- From the console ---
+    # Write some object in the console
+    with qtbot.waitSignal(shell.executed):
+        shell.execute('import numpy as np')
+
+    # Get help - numpy
+    help_plugin.combo.selected()
+
+    qtbot.keyClicks(help_plugin.combo, 'numpy', delay=100)
+
+    # Check that a expected text is part of the page
+    qtbot.waitUntil(lambda: check_text(webpage, "NumPy"), timeout=6000)
+
+    # Get help - numpy.arange
+    qtbot.keyClick(help_plugin.combo, Qt.Key_Right)
+    qtbot.keyClicks(help_plugin.combo, '.arange', delay=100)
+
+    # Check that a expected text is part of the page
+    qtbot.waitUntil(lambda: check_text(webpage, "arange"), timeout=6000)
+
+    # Get help - np
+    qtbot.keyClicks(help_plugin.combo, 'np', delay=100)
+
+    # Check that a expected text is part of the page
+    qtbot.waitUntil(lambda: check_text(webpage, "NumPy"), timeout=6000)
+
+    # Get help - np.arange
+    qtbot.keyClick(help_plugin.combo, Qt.Key_Right)
+    qtbot.keyClicks(help_plugin.combo, '.arange', delay=100)
+
+    # Check that a expected text is part of the page
+    qtbot.waitUntil(lambda: check_text(webpage, "arange"), timeout=6000)
+
+
 @pytest.mark.slow
 @flaky(max_runs=3)
 @pytest.mark.use_introspection