diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py index f8cce29b2b7..53eea7b1ffe 100644 --- a/spyder/app/tests/test_mainwindow.py +++ b/spyder/app/tests/test_mainwindow.py @@ -5776,5 +5776,117 @@ def test_visible_plugins(main_window, qtbot): assert set(selected) == set(visible_plugins) +@pytest.mark.slow +def test_cwd_is_synced_when_switching_consoles(main_window, qtbot, tmpdir): + """ + Test that the current working directory is synced between the IPython + console and other plugins when switching consoles. + """ + ipyconsole = main_window.ipyconsole + workdir = main_window.workingdirectory + files = main_window.get_plugin(Plugins.Explorer) + + # Wait for the window to be fully up + shell = ipyconsole.get_current_shellwidget() + qtbot.waitUntil(lambda: shell._prompt_html is not None, + timeout=SHELL_TIMEOUT) + + # Create two new clients and change their cwd's + for i in range(2): + sync_dir = tmpdir.mkdir(f'test_sync_{i}') + ipyconsole.create_new_client() + shell = ipyconsole.get_current_shellwidget() + qtbot.waitUntil(lambda: shell._prompt_html is not None, + timeout=SHELL_TIMEOUT) + with qtbot.waitSignal(shell.executed): + shell.execute(f'cd {str(sync_dir)}') + + # Switch between clients and check that the cwd is in sync with other + # plugins + for i in range(3): + ipyconsole.get_widget().tabwidget.setCurrentIndex(i) + shell_cwd = ipyconsole.get_current_shellwidget().get_cwd() + assert shell_cwd == workdir.get_workdir() == files.get_current_folder() + + +@pytest.mark.slow +def test_console_initial_cwd_is_synced(main_window, qtbot, tmpdir): + """ + Test that the initial current working directory for new consoles is synced + with other plugins. + """ + ipyconsole = main_window.ipyconsole + workdir = main_window.workingdirectory + files = main_window.get_plugin(Plugins.Explorer) + + # Wait for the window to be fully up + shell = ipyconsole.get_current_shellwidget() + qtbot.waitUntil(lambda: shell._prompt_html is not None, + timeout=SHELL_TIMEOUT) + + # Open console from Files in tmpdir + files.get_widget().treewidget.open_interpreter([str(tmpdir)]) + shell = ipyconsole.get_current_shellwidget() + qtbot.waitUntil(lambda: shell._prompt_html is not None, + timeout=SHELL_TIMEOUT) + assert shell.get_cwd() == str(tmpdir) == workdir.get_workdir() == \ + files.get_current_folder() + + # Check that a new client has the same initial cwd as the current one + ipyconsole.create_new_client() + shell = ipyconsole.get_current_shellwidget() + qtbot.waitUntil(lambda: shell._prompt_html is not None, + timeout=SHELL_TIMEOUT) + qtbot.wait(500) + assert shell.get_cwd() == str(tmpdir) == workdir.get_workdir() == \ + files.get_current_folder() + + # Check new clients with a fixed directory + ipyconsole.set_conf('console/use_cwd', False, section='workingdir') + ipyconsole.set_conf( + 'console/use_fixed_directory', + True, + section='workingdir' + ) + + fixed_dir = str(tmpdir.mkdir('fixed_dir')) + ipyconsole.set_conf( + 'console/fixed_directory', + fixed_dir, + section='workingdir' + ) + + ipyconsole.create_new_client() + shell = ipyconsole.get_current_shellwidget() + qtbot.waitUntil(lambda: shell._prompt_html is not None, + timeout=SHELL_TIMEOUT) + qtbot.wait(500) + assert shell.get_cwd() == fixed_dir == workdir.get_workdir() == \ + files.get_current_folder() + + # Check when opening projects + project_path = str(tmpdir.mkdir('test_project')) + main_window.projects.open_project(path=project_path) + qtbot.wait(500) + + shell = ipyconsole.get_current_shellwidget() + qtbot.waitUntil(lambda: shell._prompt_html is not None, + timeout=SHELL_TIMEOUT) + qtbot.wait(500) + assert shell.get_cwd() == project_path == workdir.get_workdir() == \ + files.get_current_folder() + + # Check when closing projects + main_window.projects.close_project() + qtbot.wait(500) + + shell = ipyconsole.get_current_shellwidget() + qtbot.waitUntil(lambda: shell._prompt_html is not None, + timeout=SHELL_TIMEOUT) + qtbot.wait(500) + assert shell.get_cwd() == get_home_dir() == workdir.get_workdir() == \ + files.get_current_folder() + + if __name__ == "__main__": pytest.main() diff --git a/spyder/plugins/explorer/plugin.py b/spyder/plugins/explorer/plugin.py index 38429d3b932..0070d4b78b0 100644 --- a/spyder/plugins/explorer/plugin.py +++ b/spyder/plugins/explorer/plugin.py @@ -122,7 +122,7 @@ class Explorer(SpyderDockablePlugin): New path for renamed folder. """ - sig_interpreter_opened = Signal(str) + sig_open_interpreter_requested = Signal(str) """ This signal is emitted to request opening an interpreter with the given path as working directory. @@ -177,7 +177,7 @@ def on_initialize(self): widget.sig_file_created.connect(self.sig_file_created) widget.sig_open_file_requested.connect(self.sig_open_file_requested) widget.sig_open_interpreter_requested.connect( - self.sig_interpreter_opened) + self.sig_open_interpreter_requested) widget.sig_module_created.connect(self.sig_module_created) widget.sig_removed.connect(self.sig_file_removed) widget.sig_renamed.connect(self.sig_file_renamed) @@ -207,7 +207,7 @@ def on_preferences_available(self): @on_plugin_available(plugin=Plugins.IPythonConsole) def on_ipython_console_available(self): ipyconsole = self.get_plugin(Plugins.IPythonConsole) - self.sig_interpreter_opened.connect( + self.sig_open_interpreter_requested.connect( ipyconsole.create_client_from_path) self.sig_run_requested.connect( lambda fname: @@ -240,7 +240,7 @@ def on_preferences_teardown(self): @on_plugin_teardown(plugin=Plugins.IPythonConsole) def on_ipython_console_teardown(self): ipyconsole = self.get_plugin(Plugins.IPythonConsole) - self.sig_interpreter_opened.disconnect( + self.sig_open_interpreter_requested.disconnect( ipyconsole.create_client_from_path) self.sig_run_requested.disconnect() @@ -260,6 +260,10 @@ def chdir(self, directory, emit=True): """ self.get_widget().chdir(directory, emit=emit) + def get_current_folder(self): + """Get folder displayed at the moment.""" + return self.get_widget().get_current_folder() + def refresh(self, new_path=None, force_current=True): """ Refresh history. diff --git a/spyder/plugins/explorer/widgets/explorer.py b/spyder/plugins/explorer/widgets/explorer.py index cde17b99ee5..3a7aa06f2da 100644 --- a/spyder/plugins/explorer/widgets/explorer.py +++ b/spyder/plugins/explorer/widgets/explorer.py @@ -471,6 +471,7 @@ def setup(self): self.open_interpreter_action = self.create_action( DirViewActions.OpenInterpreter, text=_("Open IPython console here"), + icon=self.create_icon('ipython_console'), triggered=lambda: self.open_interpreter(), ) diff --git a/spyder/plugins/ipythonconsole/plugin.py b/spyder/plugins/ipythonconsole/plugin.py index c92e039ae0e..c067f3449ee 100644 --- a/spyder/plugins/ipythonconsole/plugin.py +++ b/spyder/plugins/ipythonconsole/plugin.py @@ -332,7 +332,7 @@ def on_projects_available(self): def on_working_directory_available(self): working_directory = self.get_plugin(Plugins.WorkingDirectory) working_directory.sig_current_directory_changed.connect( - self._set_working_directory) + self.save_working_directory) @on_plugin_teardown(plugin=Plugins.Preferences) def on_preferences_teardown(self): @@ -379,7 +379,7 @@ def on_projects_teardown(self): def on_working_directory_teardown(self): working_directory = self.get_plugin(Plugins.WorkingDirectory) working_directory.sig_current_directory_changed.disconnect( - self._set_working_directory) + self.save_working_directory) def update_font(self): """Update font from Preferences""" @@ -426,11 +426,6 @@ def _remove_old_std_files(self): except Exception: pass - @Slot(str) - def _set_working_directory(self, new_dir): - """Set current working directory on the main widget.""" - self.get_widget().set_working_directory(new_dir) - # ---- Public API # ------------------------------------------------------------------------- @@ -784,7 +779,7 @@ def set_current_client_working_directory(self, directory): def set_working_directory(self, dirname): """ - Set current working directory for the `workingdirectory` and `explorer` + Set current working directory in the Working Directory and Files plugins. Parameters @@ -798,6 +793,18 @@ def set_working_directory(self, dirname): """ self.get_widget().set_working_directory(dirname) + @Slot(str) + def save_working_directory(self, dirname): + """ + Save current working directory on the main widget to start new clients. + + Parameters + ---------- + new_dir: str + Path to the new current working directory. + """ + self.get_widget().save_working_directory(dirname) + def update_working_directory(self): """Update working directory to console current working directory.""" self.get_widget().update_working_directory() diff --git a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py index e4c70e2a298..c5697e0de04 100644 --- a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py +++ b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py @@ -2283,7 +2283,7 @@ def get_cwd_of_new_client(): # Simulate a specific directory cwd_dir = str(tmpdir.mkdir('ipyconsole_cwd_test')) - ipyconsole.get_widget().set_working_directory(cwd_dir) + ipyconsole.get_widget().save_working_directory(cwd_dir) # Get cwd of new client and assert is the expected one assert get_cwd_of_new_client() == cwd_dir diff --git a/spyder/plugins/ipythonconsole/widgets/client.py b/spyder/plugins/ipythonconsole/widgets/client.py index f50fe7e07ab..0c5626cdaf5 100644 --- a/spyder/plugins/ipythonconsole/widgets/client.py +++ b/spyder/plugins/ipythonconsole/widgets/client.py @@ -107,7 +107,8 @@ def __init__(self, parent, id_, handlers={}, stderr_obj=None, stdout_obj=None, - fault_obj=None): + fault_obj=None, + initial_cwd=None): super(ClientWidget, self).__init__(parent) SaveHistoryMixin.__init__(self, history_filename) @@ -123,6 +124,7 @@ def __init__(self, parent, id_, self.reset_warning = reset_warning self.ask_before_restart = ask_before_restart self.ask_before_closing = ask_before_closing + self.initial_cwd = initial_cwd # --- Other attrs self.context_menu_actions = context_menu_actions @@ -220,8 +222,8 @@ def _when_prompt_is_ready(self): # To show if special console is valid self._check_special_console_error() - # Set the initial current working directory - self._set_initial_cwd() + # Set the initial current working directory in the kernel + self._set_initial_cwd_in_kernel() self.shellwidget.sig_prompt_ready.disconnect( self._when_prompt_is_ready) @@ -351,11 +353,12 @@ def _connect_control_signals(self): page_control.sig_show_find_widget_requested.connect( self.container.find_widget.show) - def _set_initial_cwd(self): - """Set initial cwd according to preferences.""" - logger.debug("Setting initial working directory") + def _set_initial_cwd_in_kernel(self): + """Set the initial cwd in the kernel.""" + logger.debug("Setting initial working directory in the kernel") cwd_path = get_home_dir() project_path = self.container.get_active_project_path() + emit_cwd_change = True # This is for the first client if self.id_['int_id'] == '1': @@ -377,7 +380,9 @@ def _set_initial_cwd(self): ) else: # For new clients - if self.get_conf( + if self.initial_cwd is not None: + cwd_path = self.initial_cwd + elif self.get_conf( 'console/use_project_or_home_directory', section='workingdir' ): @@ -386,6 +391,7 @@ def _set_initial_cwd(self): cwd_path = project_path elif self.get_conf('console/use_cwd', section='workingdir'): cwd_path = self.container.get_working_directory() + emit_cwd_change = False elif self.get_conf( 'console/use_fixed_directory', section='workingdir' @@ -397,7 +403,7 @@ def _set_initial_cwd(self): ) if osp.isdir(cwd_path): - self.shellwidget.set_cwd(cwd_path) + self.shellwidget.set_cwd(cwd_path, emit_cwd_change=emit_cwd_change) # ----- Public API -------------------------------------------------------- @property diff --git a/spyder/plugins/ipythonconsole/widgets/main_widget.py b/spyder/plugins/ipythonconsole/widgets/main_widget.py index cc063d5e418..030640d2674 100644 --- a/spyder/plugins/ipythonconsole/widgets/main_widget.py +++ b/spyder/plugins/ipythonconsole/widgets/main_widget.py @@ -1181,6 +1181,14 @@ def refresh_container(self, give_focus=False): sw.is_waiting_pdb_input(), sw.get_pdb_last_step()) self.sig_shellwidget_changed.emit(sw) + # This is necessary to sync the current client cwd with the working + # directory displayed by other plugins in Spyder (e.g. Files). + # NOTE: Instead of emitting sig_current_directory_changed directly, + # we call on_working_directory_changed to validate that the cwd + # exists (this couldn't be the case for remote kernels). + if sw.get_cwd() != self.get_working_directory(): + self.on_working_directory_changed(sw.get_cwd()) + self.update_tabs_text() self.update_actions() @@ -1429,7 +1437,7 @@ def get_current_shellwidget(self): @Slot(bool, str, bool) def create_new_client(self, give_focus=True, filename='', is_cython=False, is_pylab=False, is_sympy=False, given_name=None, - cache=True): + cache=True, initial_cwd=None): """Create a new client""" self.master_clients += 1 client_id = dict(int_id=str(self.master_clients), @@ -1466,7 +1474,8 @@ def create_new_client(self, give_focus=True, filename='', is_cython=False, handlers=self.registered_spyder_kernel_handlers, stderr_obj=stderr_obj, stdout_obj=stdout_obj, - fault_obj=fault_obj) + fault_obj=fault_obj, + initial_cwd=initial_cwd) self.add_tab( client, name=client.get_name(), filename=filename, @@ -1808,13 +1817,10 @@ def connect_client_to_kernel(self, client, km, kc): # type is Qt.DirectConnection. self._shellwidget_started(client) - @Slot(str) def create_client_from_path(self, path): """Create a client with its cwd pointing to path.""" - self.create_new_client() - sw = self.get_current_shellwidget() - sw.set_cwd(path) + self.create_new_client(initial_cwd=path) def create_client_for_file(self, filename, is_cython=False): """Create a client to execute code related to a file.""" @@ -1878,7 +1884,7 @@ def register_client(self, client, give_focus=True): # Connect to working directory shellwidget.sig_working_directory_changed.connect( - self.working_directory_changed) + self.on_working_directory_changed) # Connect client execution state to be reflected in the interface client.sig_execution_state_changed.connect(self.update_actions) @@ -2385,15 +2391,23 @@ def run_script(self, filename, wdir, args, debug, post_mortem, ) # ---- For working directory and path management - def set_working_directory(self, new_dir): + def set_working_directory(self, dirname): + """ + Set current working directory in the Working Directory and Files + plugins. """ - Set current working directory when changed by the Working Directory + if osp.isdir(dirname): + self.sig_current_directory_changed.emit(dirname) + + def save_working_directory(self, dirname): + """ + Save current working directory when changed by the Working Directory plugin. """ - self._current_working_directory = new_dir + self._current_working_directory = dirname def get_working_directory(self): - """Get current working directory.""" + """Get saved value of current working directory.""" return self._current_working_directory def set_current_client_working_directory(self, directory): @@ -2402,12 +2416,12 @@ def set_current_client_working_directory(self, directory): if shellwidget is not None: shellwidget.set_cwd(directory) - def working_directory_changed(self, dirname): + def on_working_directory_changed(self, dirname): """ Notify that the working directory was changed in the current console to other plugins. """ - if osp.isdir(dirname): + if dirname and osp.isdir(dirname): self.sig_current_directory_changed.emit(dirname) def update_working_directory(self): diff --git a/spyder/plugins/ipythonconsole/widgets/shell.py b/spyder/plugins/ipythonconsole/widgets/shell.py index e50e663372b..c8aa24d91c7 100644 --- a/spyder/plugins/ipythonconsole/widgets/shell.py +++ b/spyder/plugins/ipythonconsole/widgets/shell.py @@ -323,8 +323,18 @@ def check_spyder_kernel_callback(self, reply): self.is_spyder_kernel = True self.sig_is_spykernel.emit(self) - def set_cwd(self, dirname): - """Set shell current working directory.""" + def set_cwd(self, dirname, emit_cwd_change=False): + """ + Set shell current working directory. + + Parameters + ---------- + dirname: str + Path to the new current working directory. + emit_cwd_change: bool + Whether to emit a Qt signal that informs other panes in Spyder that + the current working directory has changed. + """ if os.name == 'nt': # Use normpath instead of replacing '\' with '\\' # See spyder-ide/spyder#10785 @@ -333,15 +343,36 @@ def set_cwd(self, dirname): if self.ipyclient.hostname is None: self.call_kernel(interrupt=self.is_debugging()).set_cwd(dirname) self._cwd = dirname + if emit_cwd_change: + self.sig_working_directory_changed.emit(self._cwd) + + def get_cwd(self): + """ + Get current working directory. + + Notes + ----- + * This doesn't ask the kernel for its working directory. Instead, it + returns the last value of it saved here. + * We do it for performance reasons because we call this method when + switching consoles to update the Working Directory toolbar. + """ + return self._cwd def update_cwd(self): - """Update current working directory in the kernel.""" + """ + Update working directory in Spyder after getting its value from the + kernel. + """ if self.kernel_client is None: return - self.call_kernel(callback=self.remote_set_cwd).get_cwd() + self.call_kernel(callback=self.on_getting_cwd).get_cwd() - def remote_set_cwd(self, cwd): - """Get current working directory from kernel.""" + def on_getting_cwd(self, cwd): + """ + If necessary, notify that the working directory was changed to other + plugins. + """ if cwd != self._cwd: self._cwd = cwd self.sig_working_directory_changed.emit(self._cwd) diff --git a/spyder/plugins/workingdirectory/plugin.py b/spyder/plugins/workingdirectory/plugin.py index 0b8bc4f9448..3b1c6683200 100644 --- a/spyder/plugins/workingdirectory/plugin.py +++ b/spyder/plugins/workingdirectory/plugin.py @@ -188,6 +188,8 @@ def chdir(self, directory, sender_plugin=None): if sender_plugin is not None: container = self.get_container() container.chdir(directory, emit=False) + if ipyconsole: + ipyconsole.save_working_directory(directory) self.save_history()