diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py index c7ae6800fe2..a69797d893e 100644 --- a/spyder/app/tests/test_mainwindow.py +++ b/spyder/app/tests/test_mainwindow.py @@ -14,6 +14,7 @@ import tempfile from flaky import flaky +from jupyter_client.manager import KernelManager import numpy as np from numpy.testing import assert_array_equal import pytest @@ -25,6 +26,7 @@ from spyder.app.cli_options import get_options from spyder.app.mainwindow import initialize, run_spyder from spyder.py3compat import PY2 +from spyder.utils.ipython.kernelspec import SpyderKernelSpec from spyder.utils.programs import is_module_installed from spyder.utils.test import close_save_message_box @@ -74,6 +76,25 @@ def reset_run_code(qtbot, shell, code_editor, nsb): qtbot.keyClick(code_editor, Qt.Key_Home, modifier=Qt.ControlModifier) +def start_new_kernel(startup_timeout=60, kernel_name='python', spykernel=False, + **kwargs): + """Start a new kernel, and return its Manager and Client""" + km = KernelManager(kernel_name=kernel_name) + if spykernel: + km._kernel_spec = SpyderKernelSpec() + km.start_kernel(**kwargs) + kc = km.client() + kc.start_channels() + try: + kc.wait_for_ready(timeout=startup_timeout) + except RuntimeError: + kc.stop_channels() + km.shutdown_kernel() + raise + + return km, kc + + #============================================================================== # Fixtures #============================================================================== @@ -104,6 +125,80 @@ def close_window(): #============================================================================== # Tests #============================================================================== +# IMPORTANT NOTE: Please leave this test to be the first one here to +# avoid possible timeouts in Appyevor +@flaky(max_runs=3) +@pytest.mark.skipif(os.name != 'nt' or not PY2, + reason="It times out on Linux and Python 3") +@pytest.mark.timeout(timeout=60, method='thread') +@pytest.mark.use_introspection +def test_calltip(main_window, qtbot): + """Hide the calltip in the editor when a matching ')' is found.""" + # Load test file + text = 'a = [1,2,3]\n(max' + main_window.editor.new(fname="test.py", text=text) + code_editor = main_window.editor.get_focus_widget() + + # Set text to start + code_editor.set_text(text) + code_editor.go_to_line(2) + code_editor.move_cursor(5) + calltip = code_editor.calltip_widget + assert not calltip.isVisible() + + 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.keyPress(code_editor, Qt.Key_ParenRight, delay=1000) + qtbot.keyPress(code_editor, Qt.Key_Space) + assert not calltip.isVisible() + qtbot.keyPress(code_editor, Qt.Key_ParenRight, delay=1000) + qtbot.keyPress(code_editor, Qt.Key_Enter, delay=1000) + + QTimer.singleShot(1000, lambda: close_save_message_box(qtbot)) + main_window.editor.close_file() + + +@flaky(max_runs=3) +def test_connection_to_external_kernel(main_window, qtbot): + """Test that only Spyder kernels are connected to the Variable Explorer.""" + # Test with a generic kernel + km, kc = start_new_kernel() + + main_window.ipyconsole._create_client_for_kernel(kc.connection_file, None, + None, None) + shell = main_window.ipyconsole.get_current_shellwidget() + qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT) + with qtbot.waitSignal(shell.executed): + shell.execute('a = 10') + + # Assert that there are no variables in the variable explorer + main_window.variableexplorer.visibility_changed(True) + nsb = main_window.variableexplorer.get_focus_widget() + qtbot.wait(500) + assert nsb.editor.model.rowCount() == 0 + + # Test with a kernel from Spyder + spykm, spykc = start_new_kernel(spykernel=True) + main_window.ipyconsole._create_client_for_kernel(spykc.connection_file, None, + None, None) + shell = main_window.ipyconsole.get_current_shellwidget() + qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT) + with qtbot.waitSignal(shell.executed): + shell.execute('a = 10') + + # Assert that a variable is visible in the variable explorer + main_window.variableexplorer.visibility_changed(True) + nsb = main_window.variableexplorer.get_focus_widget() + qtbot.wait(500) + assert nsb.editor.model.rowCount() == 1 + + # Shutdown the kernels + spykm.shutdown_kernel(now=True) + km.shutdown_kernel(now=True) + + @flaky(max_runs=3) @pytest.mark.skipif(os.name == 'nt', reason="It times out sometimes on Windows") def test_np_threshold(main_window, qtbot): @@ -156,39 +251,6 @@ def test_change_types_in_varexp(main_window, qtbot): assert shell.get_value('a') == 10 -@flaky(max_runs=3) -@pytest.mark.skipif(os.name != 'nt' or not PY2, - reason="It times out on Linux and Python 3") -@pytest.mark.timeout(timeout=60, method='thread') -@pytest.mark.use_introspection -def test_calltip(main_window, qtbot): - """Hide the calltip in the editor when a matching ')' is found.""" - # Load test file - text = 'a = [1,2,3]\n(max' - main_window.editor.new(fname="test.py", text=text) - code_editor = main_window.editor.get_focus_widget() - - # Set text to start - code_editor.set_text(text) - code_editor.go_to_line(2) - code_editor.move_cursor(5) - calltip = code_editor.calltip_widget - assert not calltip.isVisible() - - 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.keyPress(code_editor, Qt.Key_ParenRight, delay=1000) - qtbot.keyPress(code_editor, Qt.Key_Space) - assert not calltip.isVisible() - qtbot.keyPress(code_editor, Qt.Key_ParenRight, delay=1000) - qtbot.keyPress(code_editor, Qt.Key_Enter, delay=1000) - - QTimer.singleShot(1000, lambda: close_save_message_box(qtbot)) - main_window.editor.close_file() - - @flaky(max_runs=3) @pytest.mark.skipif(os.name == 'nt' or not is_module_installed('Cython'), reason="It times out sometimes on Windows and Cython is needed") diff --git a/spyder/plugins/ipythonconsole.py b/spyder/plugins/ipythonconsole.py index b4da2735ab3..f6da2e1156a 100644 --- a/spyder/plugins/ipythonconsole.py +++ b/spyder/plugins/ipythonconsole.py @@ -1384,12 +1384,30 @@ def process_finished(self, client): if self.variableexplorer is not None: self.variableexplorer.remove_shellwidget(id(client.shellwidget)) + def connect_external_kernel(self, shellwidget): + """ + Connect an external kernel to the Variable Explorer and Help, if + it is a Spyder kernel. + """ + sw = shellwidget + kc = shellwidget.kernel_client + if self.help is not None: + self.help.set_shell(sw) + if self.variableexplorer is not None: + self.variableexplorer.add_shellwidget(sw) + sw.set_namespace_view_settings() + sw.refresh_namespacebrowser() + kc.stopped_channels.connect(lambda : + self.variableexplorer.remove_shellwidget(id(sw))) + def _create_client_for_kernel(self, connection_file, hostname, sshkey, password): # Verifying if the connection file exists try: cf_path = osp.dirname(connection_file) cf_filename = osp.basename(connection_file) + # To change a possible empty string to None + cf_path = cf_path if cf_path else None connection_file = find_connection_file(filename=cf_filename, path=cf_path) except (IOError, UnboundLocalError): @@ -1458,11 +1476,15 @@ def _create_client_for_kernel(self, connection_file, hostname, sshkey, _("Could not open ssh tunnel. The " "error was:\n\n") + to_text_string(e)) return - kernel_client.start_channels() # Assign kernel manager and client to shellwidget client.shellwidget.kernel_client = kernel_client client.shellwidget.kernel_manager = kernel_manager + kernel_client.start_channels() + if external_kernel: + client.shellwidget.sig_is_spykernel.connect( + self.connect_external_kernel) + client.shellwidget.is_spyder_kernel() # Adding a new tab for the client self.add_tab(client, name=client.get_name()) diff --git a/spyder/widgets/ipythonconsole/shell.py b/spyder/widgets/ipythonconsole/shell.py index 5de9822bc29..7b1601c21c1 100644 --- a/spyder/widgets/ipythonconsole/shell.py +++ b/spyder/widgets/ipythonconsole/shell.py @@ -45,6 +45,7 @@ class ShellWidget(NamepaceBrowserWidget, HelpWidget, DebuggingWidget): focus_changed = Signal() new_client = Signal() sig_got_reply = Signal() + sig_is_spykernel = Signal(object) sig_kernel_restarted = Signal(str) def __init__(self, ipyclient, additional_options, interpreter_versions, @@ -77,6 +78,14 @@ def is_running(self): else: return False + def is_spyder_kernel(self): + """Determine if the kernel is from Spyder.""" + code = u"getattr(get_ipython().kernel, 'set_value', False)" + if self._reading: + return + else: + self.silent_exec_method(code) + def set_cwd(self, dirname): """Set shell current working directory.""" code = u"get_ipython().kernel.set_cwd(r'{}')".format(dirname) @@ -301,6 +310,11 @@ def handle_exec_method(self, msg): else: env = None self.sig_show_env.emit(env) + elif 'getattr' in method: + if data is not None and 'text/plain' in data: + is_spyder_kernel = data['text/plain'] + if 'SpyderKernel' in is_spyder_kernel: + self.sig_is_spykernel.emit(self) else: if data is not None and 'text/plain' in data: self._kernel_reply = ast.literal_eval(data['text/plain'])