Repository: spyder-ide/spyder

Issue:
Invalid interpreter causes crash over the LSP/pyls
@goanpeca @ccordoba12 , while testing #13104 I ran into an error where if you pass an invalid interpreter seems like it is being used by the LSP (?)

This happens not only in this PR but also in master and 4.x, although in 4.x no error report raises (but you can see that under the hood something is trying to execute the path given). I tested passing as interpreter a path to an installer (GIMP installer in this case):

```python

  File "C:\Users\Daniel\Anaconda3\envs\develop\lib\site-packages\pyls_jsonrpc\endpoint.py", line 113, in consume
    self._handle_request(message['id'], message['method'], message.get('params'))
  File "C:\Users\Daniel\Anaconda3\envs\develop\lib\site-packages\pyls_jsonrpc\endpoint.py", line 182, in _handle_request
    handler_result = handler(params)
  File "C:\Users\Daniel\Anaconda3\envs\develop\lib\site-packages\pyls_jsonrpc\dispatchers.py", line 23, in handler
    return method(**(params or {}))
  File "c:\users\daniel\documents\spyder\spyder\external-deps\python-language-server\pyls\python_ls.py", line 334, in m_text_document__document_symbol
    return self.document_symbols(textDocument['uri'])
  File "c:\users\daniel\documents\spyder\spyder\external-deps\python-language-server\pyls\python_ls.py", line 250, in document_symbols
    return flatten(self._hook('pyls_document_symbols', doc_uri))
  File "c:\users\daniel\documents\spyder\spyder\external-deps\python-language-server\pyls\python_ls.py", line 156, in _hook
    return hook_handlers(config=self.config, workspace=workspace, document=doc, **kwargs)
  File "C:\Users\Daniel\Anaconda3\envs\develop\lib\site-packages\pluggy\hooks.py", line 286, in __call__
    return self._hookexec(self, self.get_hookimpls(), kwargs)
  File "C:\Users\Daniel\Anaconda3\envs\develop\lib\site-packages\pluggy\manager.py", line 93, in _hookexec
    return self._inner_hookexec(hook, methods, kwargs)
  File "C:\Users\Daniel\Anaconda3\envs\develop\lib\site-packages\pluggy\manager.py", line 337, in traced_hookexec
    return outcome.get_result()
  File "C:\Users\Daniel\Anaconda3\envs\develop\lib\site-packages\pluggy\callers.py", line 80, in get_result
    raise ex[1].with_traceback(ex[2])
  File "C:\Users\Daniel\Anaconda3\envs\develop\lib\site-packages\pluggy\callers.py", line 52, in from_call
    result = func()
  File "C:\Users\Daniel\Anaconda3\envs\develop\lib\site-packages\pluggy\manager.py", line 335, in <lambda>
    outcome = _Result.from_call(lambda: oldcall(hook, hook_impls, kwargs))
  File "C:\Users\Daniel\Anaconda3\envs\develop\lib\site-packages\pluggy\manager.py", line 87, in <lambda>
    firstresult=hook.spec.opts.get("firstresult") if hook.spec else False,
  File "C:\Users\Daniel\Anaconda3\envs\develop\lib\site-packages\pluggy\callers.py", line 208, in _multicall
    return outcome.get_result()
  File "C:\Users\Daniel\Anaconda3\envs\develop\lib\site-packages\pluggy\callers.py", line 80, in get_result
    raise ex[1].with_traceback(ex[2])
  File "C:\Users\Daniel\Anaconda3\envs\develop\lib\site-packages\pluggy\callers.py", line 187, in _multicall
    res = hook_impl.function(*args)
  File "c:\users\daniel\documents\spyder\spyder\external-deps\python-language-server\pyls\plugins\symbols.py", line 12, in pyls_document_symbols
    definitions = document.jedi_names(all_scopes=all_scopes)
  File "c:\users\daniel\documents\spyder\spyder\external-deps\python-language-server\pyls\workspace.py", line 221, in jedi_names
    script = self.jedi_script()
  File "c:\users\daniel\documents\spyder\spyder\external-deps\python-language-server\pyls\workspace.py", line 234, in jedi_script
    environment = self.get_enviroment(environment_path) if environment_path else None
  File "c:\users\daniel\documents\spyder\spyder\external-deps\python-language-server\pyls\workspace.py", line 259, in get_enviroment
    environment = jedi.api.environment.create_environment(path=environment_path, safe=False)
  File "C:\Users\Daniel\Anaconda3\envs\develop\lib\site-packages\jedi\api\environment.py", line 376, in create_environment
    return Environment(path)
  File "C:\Users\Daniel\Anaconda3\envs\develop\lib\site-packages\jedi\api\environment.py", line 67, in __init__
    self._get_subprocess()
  File "C:\Users\Daniel\Anaconda3\envs\develop\lib\site-packages\jedi\api\environment.py", line 80, in _get_subprocess
    exc))

jedi.api.environment.InvalidPythonEnvironment: Could not get version information for 'C:/Users/Daniel/Documents/Quansight/gimp-2.10.14-setup-3.exe': InternalError("The subprocess C:/Users/Daniel/Documents/Quansight/gimp-2.10.14-setup-3.exe has crashed (EOFError('Ran out of input'), stderr=).")
```

Maybe this was already an error with an issue but just in case...

_Originally posted by @dalthviz in https://github.com/spyder-ide/spyder/pull/13104#issuecomment-654554498_


Discussion:
So is it possible to set an invalid executable as a Python interpreter?
I'm not sure, the dialog with a warning indicating that the interpreter is not valid pops-up but seems like this is being bypassed and the selected interpreter starts being use by the LSP.
We're probably connecting the line edit that sets the interpreter directly to the PyLS, without waiting to check if the interpreter is valid or not.

I'll take a look a it to see if I can fix for 4.1.4. Thanks @dalthviz for noticing it!

Resolving patch:
diff --git a/spyder/preferences/configdialog.py b/spyder/preferences/configdialog.py
--- a/spyder/preferences/configdialog.py
+++ b/spyder/preferences/configdialog.py
@@ -778,7 +778,8 @@ def create_combobox(self, text, choices, option, default=NoDefault,
     def create_file_combobox(self, text, choices, option, default=NoDefault,
                              tip=None, restart=False, filters=None,
                              adjust_to_contents=False,
-                             default_line_edit=False, section=None):
+                             default_line_edit=False, section=None,
+                             validate_callback=None):
         """choices: couples (name, key)"""
         combobox = FileComboBox(self, adjust_to_contents=adjust_to_contents,
                                 default_line_edit=default_line_edit)
@@ -794,7 +795,9 @@ def create_file_combobox(self, text, choices, option, default=NoDefault,
         combobox.addItems(choices)
 
         msg = _('Invalid file path')
-        self.validate_data[edit] = (osp.isfile, msg)
+        self.validate_data[edit] = (
+            validate_callback if validate_callback else osp.isfile,
+            msg)
         browse_btn = QPushButton(ima.icon('FileIcon'), '', self)
         browse_btn.setToolTip(_("Select file"))
         browse_btn.clicked.connect(lambda: self.select_file(edit, filters))
diff --git a/spyder/preferences/maininterpreter.py b/spyder/preferences/maininterpreter.py
--- a/spyder/preferences/maininterpreter.py
+++ b/spyder/preferences/maininterpreter.py
@@ -89,13 +89,14 @@ def setup_page(self):
         pyexec_layout.addWidget(self.cus_exec_radio)
         self.validate_custom_interpreters_list()
         self.cus_exec_combo = self.create_file_combobox(
-                                  _('Recent custom interpreters'),
-                                  self.get_option('custom_interpreters_list'),
-                                  'custom_interpreter',
-                                  filters=filters,
-                                  default_line_edit=True,
-                                  adjust_to_contents=True
-                                  )
+            _('Recent custom interpreters'),
+            self.get_option('custom_interpreters_list'),
+            'custom_interpreter',
+            filters=filters,
+            default_line_edit=True,
+            adjust_to_contents=True,
+            validate_callback=programs.is_python_interpreter,
+        )
         self.def_exec_radio.toggled.connect(self.cus_exec_combo.setDisabled)
         self.cus_exec_radio.toggled.connect(self.cus_exec_combo.setEnabled)
         pyexec_layout.addWidget(self.cus_exec_combo)
@@ -148,22 +149,6 @@ def setup_page(self):
         vlayout.addStretch(1)
         self.setLayout(vlayout)
 
-    def python_executable_changed(self, pyexec):
-        """Custom Python executable value has been changed"""
-        if not self.cus_exec_radio.isChecked():
-            return False
-        if not is_text_string(pyexec):
-            pyexec = to_text_string(pyexec.toUtf8(), 'utf-8')
-        if (not programs.is_python_interpreter(pyexec) or
-                not self.warn_python_compatibility(pyexec)):
-            QMessageBox.warning(self, _('Warning'),
-                    _("You selected an invalid Python interpreter for the "
-                      "console so the previous interpreter will stay. Please "
-                      "make sure to select a valid one."), QMessageBox.Ok)
-            self.def_exec_radio.setChecked(True)
-            return False
-        return True
-
     def warn_python_compatibility(self, pyexec):
         if not osp.isfile(pyexec):
             return
@@ -250,13 +235,29 @@ def apply_settings(self, options):
             executable = osp.normpath(executable)
             if executable.endswith('pythonw.exe'):
                 executable = executable.replace("pythonw.exe", "python.exe")
-            change = self.python_executable_changed(executable)
-            if change:
-                self.set_custom_interpreters_list(executable)
-                self.set_option('executable', executable)
-                self.set_option('custom_interpreter', executable)
+
+            # Set new interpreter
+            self.set_custom_interpreters_list(executable)
+            self.set_option('executable', executable)
+            self.set_option('custom_interpreter', executable)
+
+            # Update combobox items.
+            custom_list = self.get_option('custom_interpreters_list')
+            self.cus_exec_combo.combobox.clear()
+            self.cus_exec_combo.combobox.addItems(custom_list)
+            self.pyexec_edit.setText(executable)
+
+            # Show warning compatibility message.
+            self.warn_python_compatibility(executable)
         if not self.pyexec_edit.text():
             self.set_option('custom_interpreter', '')
-        if 'default' in options or 'custom' in options:
+        else:
+            # This prevents showing an incorrect file name in the custom
+            # line edit.
+            if not programs.is_python_interpreter(self.pyexec_edit.text()):
+                self.pyexec_edit.clear()
+                self.set_option('custom_interpreter', '')
+        if ('default' in options or 'custom' in options
+                or 'custom_interpreter' in options):
             self.main.sig_main_interpreter_changed.emit()
-        self.main.apply_settings()
+        self.main.apply_statusbar_settings()
diff --git a/spyder/widgets/comboboxes.py b/spyder/widgets/comboboxes.py
--- a/spyder/widgets/comboboxes.py
+++ b/spyder/widgets/comboboxes.py
@@ -328,6 +328,20 @@ def is_valid(self, qstr=None):
                  osp.isdir(to_text_string(qstr)))
         return valid
 
+    def tab_complete(self):
+        """
+        If there is a single option available one tab completes the option.
+        """
+        opts = self._complete_options()
+        if len(opts) == 1:
+            text = opts[0]
+            if osp.isdir(text):
+                text = text + os.sep
+            self.set_current_text(text)
+            self.hide_completer()
+        else:
+            self.completer().complete()
+
     def _complete_options(self):
         """Find available completion options."""
         text = to_text_string(self.currentText())


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
@@ -3353,6 +3353,8 @@ def test_varexp_cleared_after_reset(main_window, qtbot):
                     timeout=3000)
 
 
+@pytest.mark.slow
+@flaky(max_runs=3)
 def test_immediate_debug(main_window, qtbot):
     """
     Check if we can enter debugging immediately