diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py index 2d10717dc70..eb7e2765aff 100644 --- a/spyder/app/tests/test_mainwindow.py +++ b/spyder/app/tests/test_mainwindow.py @@ -5521,10 +5521,11 @@ def crash_func(): # Click the run button qtbot.mouseClick(main_window.run_button, Qt.LeftButton) + # Check segfault info is printed in the console qtbot.waitUntil(lambda: 'Segmentation fault' in control.toPlainText(), timeout=SHELL_TIMEOUT) - assert 'Segmentation fault' in control.toPlainText() - assert 'in crash_func' in control.toPlainText() + qtbot.waitUntil(lambda: 'in crash_func' in control.toPlainText(), + timeout=SHELL_TIMEOUT) @flaky(max_runs=3) diff --git a/spyder/plugins/editor/widgets/codeeditor/tests/test_hints_and_calltips.py b/spyder/plugins/editor/widgets/codeeditor/tests/test_hints_and_calltips.py index 6c67f6d479b..71f8a7e65ad 100644 --- a/spyder/plugins/editor/widgets/codeeditor/tests/test_hints_and_calltips.py +++ b/spyder/plugins/editor/widgets/codeeditor/tests/test_hints_and_calltips.py @@ -16,7 +16,7 @@ import pytest # Local imports -from spyder.config.base import running_in_ci, running_in_ci_with_conda +from spyder.config.base import running_in_ci from spyder.plugins.editor.extensions.closebrackets import ( CloseBracketsExtension ) @@ -214,11 +214,9 @@ def test_get_hints_not_triggered(qtbot, completions_codeeditor, text): @pytest.mark.skipif( ( sys.platform == "darwin" - or ( - sys.platform.startswith("linux") and not running_in_ci_with_conda() - ) + or (sys.platform.startswith("linux") and running_in_ci()) ), - reason="Fails on Linux with pip packages and Mac", + reason="Fails on CIs for Linux and Mac", ) @pytest.mark.parametrize( "params", @@ -277,11 +275,9 @@ def test_get_hints_for_builtins(qtbot, completions_codeeditor, params): @pytest.mark.skipif( ( sys.platform == "darwin" - or ( - sys.platform.startswith("linux") and not running_in_ci_with_conda() - ) + or (sys.platform.startswith("linux") and running_in_ci()) ), - reason="Fails on Linux with pip packages and Mac", + reason="Fails on CIs for Linux and Mac", ) @pytest.mark.parametrize( "params", diff --git a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py index b4fc62680bd..27c412a3337 100644 --- a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py +++ b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py @@ -545,7 +545,9 @@ def test_request_syspath(ipyconsole, qtbot, tmpdir): @flaky(max_runs=10) -@pytest.mark.skipif(os.name == 'nt', reason="It doesn't work on Windows") +@pytest.mark.skipif( + not sys.platform.startswith("linux"), reason="Fails on Windows and Mac" +) def test_save_history_dbg(ipyconsole, qtbot): """Test that browsing command history is working while debugging.""" shell = ipyconsole.get_current_shellwidget() @@ -622,8 +624,7 @@ def test_save_history_dbg(ipyconsole, qtbot): @flaky(max_runs=3) -@pytest.mark.skipif(IPython.version_info < (7, 17), - reason="insert is not the same in pre 7.17 ipython") +@pytest.mark.skipif(sys.platform == "darwin", reason="Hangs on Mac") def test_dbg_input(ipyconsole, qtbot): """Test that spyder doesn't send pdb commands to unrelated input calls.""" shell = ipyconsole.get_current_shellwidget() @@ -674,8 +675,10 @@ def test_unicode_vars(ipyconsole, qtbot): @flaky(max_runs=10) @pytest.mark.no_xvfb -@pytest.mark.skipif(running_in_ci() and os.name == 'nt', - reason="Times out on Windows") +@pytest.mark.skipif( + (running_in_ci() and os.name == 'nt') or sys.platform == "darwin", + reason="Hangs on CIs for Windows and Mac" +) def test_values_dbg(ipyconsole, qtbot): """ Test that getting, setting, copying and removing values is working while @@ -727,6 +730,7 @@ def is_defined(val): @flaky(max_runs=3) +@pytest.mark.skipif(sys.platform == "darwin", reason="Hangs on Mac") def test_execute_events_dbg(ipyconsole, qtbot): """Test execute events while debugging""" @@ -850,7 +854,10 @@ def test_mpl_backend_change(ipyconsole, qtbot): @flaky(max_runs=10) -@pytest.mark.skipif(os.name == 'nt', reason="It doesn't work on Windows") +@pytest.mark.skipif( + os.name == 'nt' or sys.platform == "darwin", + reason="Doesn't work on Windows and hangs on Mac" +) def test_clear_and_reset_magics_dbg(ipyconsole, qtbot): """ Test that clear and reset magics are working while debugging @@ -1067,11 +1074,11 @@ def test_kernel_crash(ipyconsole, qtbot): qtbot.waitUntil(lambda: bool( ipyconsole.get_widget()._cached_kernel_properties[-1]._init_stderr )) + # Create a new client ipyconsole.create_new_client() # Assert that the console is showing an error - # even if the error happened before the connection error_client = ipyconsole.get_clients()[-1] qtbot.waitUntil(lambda: bool(error_client.error_text), timeout=6000) finally: diff --git a/spyder/plugins/ipythonconsole/utils/kernel_handler.py b/spyder/plugins/ipythonconsole/utils/kernel_handler.py index 986efddc756..bd73388ad2a 100644 --- a/spyder/plugins/ipythonconsole/utils/kernel_handler.py +++ b/spyder/plugins/ipythonconsole/utils/kernel_handler.py @@ -20,6 +20,7 @@ # Local imports from spyder.api.translations import _ +from spyder.config.base import running_under_pytest from spyder.plugins.ipythonconsole import ( SPYDER_KERNELS_MIN_VERSION, SPYDER_KERNELS_MAX_VERSION, SPYDER_KERNELS_VERSION, SPYDER_KERNELS_CONDA, SPYDER_KERNELS_PIP) @@ -488,7 +489,10 @@ def close(self, shutdown_kernel=True, now=False): km.stop_restarter() self.disconnect_std_pipes() - if now: + # This is probably necessary due to a weird interaction between + # `conda run --no-capture-output` and pytest capturing output + # facilities. + if now or running_under_pytest(): km.shutdown_kernel(now=True) self.after_shutdown() else: diff --git a/spyder/plugins/ipythonconsole/utils/kernelspec.py b/spyder/plugins/ipythonconsole/utils/kernelspec.py index f3720c5f9cf..4ca26655aaf 100644 --- a/spyder/plugins/ipythonconsole/utils/kernelspec.py +++ b/spyder/plugins/ipythonconsole/utils/kernelspec.py @@ -131,8 +131,11 @@ def argv(self): # runtime environment, we need to activate the environment to run # spyder-kernels kernel_cmd[:0] = [ - find_conda(), 'run', - '-p', get_conda_env_path(pyexec), + find_conda(), + 'run', + '--no-capture-output', + '--prefix', + get_conda_env_path(pyexec), ] logger.info('Kernel command: {}'.format(kernel_cmd)) diff --git a/spyder/plugins/ipythonconsole/widgets/client.py b/spyder/plugins/ipythonconsole/widgets/client.py index 56ceec5a0d1..0755234c950 100644 --- a/spyder/plugins/ipythonconsole/widgets/client.py +++ b/spyder/plugins/ipythonconsole/widgets/client.py @@ -14,6 +14,7 @@ """ # Standard library imports. +import functools import logging import os import os.path as osp @@ -23,7 +24,7 @@ # Third party imports (qtpy) from qtpy.QtCore import QUrl, QTimer, Signal, Slot -from qtpy.QtWidgets import QVBoxLayout, QWidget +from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget # Local imports from spyder.api.translations import _ @@ -121,6 +122,7 @@ def __init__(self, parent, id_, self.allow_rename = True self.error_text = None self.give_focus = give_focus + self.__on_close = lambda: None css_path = self.get_conf('css_path', section='appearance') if css_path is None: @@ -553,20 +555,53 @@ def set_color_scheme(self, color_scheme, reset=True): except AttributeError: pass - def close_client(self, is_last_client): + def close_client(self, is_last_client, close_console=False): """Close the client.""" + self.__on_close = lambda: None + debugging = False + # Needed to handle a RuntimeError. See spyder-ide/spyder#5568. try: - self.stop_button_click_handler() + # This is required after spyder-ide/spyder#21788 to prevent freezes + # when closing Spyder. That happens not only when a console is in + # debugging mode before closing, but also when a kernel restart is + # requested while debugging. + if self.shellwidget.is_debugging(): + debugging = True + self.__on_close = functools.partial( + self.finish_close, + is_last_client, + close_console, + debugging + ) + self.shellwidget.sig_prompt_ready.connect(self.__on_close) + self.shellwidget.stop_debugging() + else: + self.interrupt_kernel() except RuntimeError: pass - # Disconnect timer needed to update elapsed time + if not debugging: + self.finish_close(is_last_client, close_console, debugging) + + def finish_close(self, is_last_client, close_console, debugging): + """Actions to take to finish closing the client.""" + # Disconnect timer needed to update elapsed time and this slot in case + # it was connected. try: + self.shellwidget.sig_prompt_ready.disconnect(self.__on_close) self.timer.timeout.disconnect(self.show_time) except (RuntimeError, TypeError): pass + # This is a hack to prevent segfaults when closing Spyder and the + # client was debugging before doing it. + # It's a side effect of spyder-ide/spyder#21788 + if debugging and close_console: + for __ in range(3): + time.sleep(0.08) + QApplication.processEvents() + self.shutdown(is_last_client) # Prevent errors in our tests diff --git a/spyder/plugins/ipythonconsole/widgets/main_widget.py b/spyder/plugins/ipythonconsole/widgets/main_widget.py index 0bb6165a4ab..ce5eeadb3f5 100644 --- a/spyder/plugins/ipythonconsole/widgets/main_widget.py +++ b/spyder/plugins/ipythonconsole/widgets/main_widget.py @@ -350,10 +350,6 @@ def __init__(self, name=None, plugin=None, parent=None): # Update the list of envs at startup self.get_envs() - def on_close(self): - self.mainwindow_close = True - self.close_all_clients() - # ---- PluginMainWidget API and settings handling # ------------------------------------------------------------------------ def get_title(self): @@ -1767,8 +1763,9 @@ def close_all_clients(self): open_clients = self.clients.copy() for client in self.clients: is_last_client = ( - len(self.get_related_clients(client, open_clients)) == 0) - client.close_client(is_last_client) + len(self.get_related_clients(client, open_clients)) == 0 + ) + client.close_client(is_last_client, close_console=True) open_clients.remove(client) # Wait for all KernelHandler threads to shutdown. diff --git a/spyder/plugins/ipythonconsole/widgets/mixins.py b/spyder/plugins/ipythonconsole/widgets/mixins.py index 7c5b829d77c..7c439d8128d 100644 --- a/spyder/plugins/ipythonconsole/widgets/mixins.py +++ b/spyder/plugins/ipythonconsole/widgets/mixins.py @@ -63,11 +63,15 @@ def get_cached_kernel(self, kernel_spec, cache=True): self.close_cached_kernel() return new_kernel_handler - # Check cached kernel has the same configuration as is being asked + # Check cached kernel has the same configuration as is being asked or + # it crashed. cached_kernel_handler = None if self._cached_kernel_properties is not None: cached_kernel_handler = self._cached_kernel_properties[-1] - if not self.check_cached_kernel_spec(kernel_spec): + if ( + not self.check_cached_kernel_spec(kernel_spec) + or cached_kernel_handler._init_stderr + ): # Close the kernel self.close_cached_kernel() cached_kernel_handler = None diff --git a/spyder/plugins/projects/widgets/main_widget.py b/spyder/plugins/projects/widgets/main_widget.py index 3ba7108c7a3..ea407d6297b 100644 --- a/spyder/plugins/projects/widgets/main_widget.py +++ b/spyder/plugins/projects/widgets/main_widget.py @@ -9,6 +9,7 @@ # Standard library imports from collections import OrderedDict import configparser +from contextlib import contextmanager import logging import os import os.path as osp @@ -400,13 +401,13 @@ def open_project(self, path=None, project_type=None, restart_console=True, self._add_to_recent(path) self.set_conf('current_project_path', self.get_active_project_path()) - self._setup_menu_actions() - if workdir and osp.isdir(workdir): - self.sig_project_loaded.emit(workdir) - else: - self.sig_project_loaded.emit(path) + with self._disable_pdb_prevent_closing(): + if workdir and osp.isdir(workdir): + self.sig_project_loaded.emit(workdir) + else: + self.sig_project_loaded.emit(path) self.watcher.start(path) @@ -440,8 +441,9 @@ def close_project(self): self.set_conf('current_project_path', None) self._setup_menu_actions() - self.sig_project_closed.emit(path) - self.sig_project_closed[bool].emit(True) + with self._disable_pdb_prevent_closing(): + self.sig_project_closed.emit(path) + self.sig_project_closed[bool].emit(True) # Hide pane. self.set_conf('visible_if_project_open', self.isVisible()) @@ -1000,6 +1002,28 @@ def _get_valid_recent_projects(self, recent_projects): return valid_projects + @contextmanager + def _disable_pdb_prevent_closing(self): + """ + Context manager to disable the pdb_prevent_closing option before + opening/closing the previous/current open project files. + + Notes + ----- + * This is necessary to correctly do that when a console was left in + debugging mode. + """ + try: + pdb_prevent_closing = self.get_conf( + "pdb_prevent_closing", section="debugger" + ) + self.set_conf("pdb_prevent_closing", False, section="debugger") + yield + finally: + self.set_conf( + "pdb_prevent_closing", pdb_prevent_closing, section="debugger" + ) + # ---- Private API for the Switcher # ------------------------------------------------------------------------- def _call_fzf(self, search_text=""):