Skip to content

Commit

Permalink
Merge pull request #17760 from ccordoba12/issue-17737
Browse files Browse the repository at this point in the history
PR: Fix setting working directory at startup and for new IPython consoles
  • Loading branch information
dalthviz authored May 3, 2022
2 parents b415aa8 + 1b6beb4 commit 504e7c7
Show file tree
Hide file tree
Showing 10 changed files with 300 additions and 130 deletions.
49 changes: 41 additions & 8 deletions spyder/api/plugin_registration/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@

# Standard library imports
import logging
from typing import Any, Union, Optional
import warnings

from spyder.api.exceptions import SpyderAPIError
from spyder.api.plugins import Plugins

logger = logging.getLogger(__name__)

Expand All @@ -38,15 +39,47 @@ def __init__(self):
for method_name in dir(self):
method = getattr(self, method_name, None)
if hasattr(method, '_plugin_listen'):
info = method._plugin_listen
logger.debug(f'Method {method_name} is watching plugin {info}')
self._plugin_listeners[info] = method_name
plugin_listen = method._plugin_listen

# Check if plugin is listed among REQUIRES and OPTIONAL.
# Note: We can't do this validation for the Layout plugin
# because it depends on all plugins through the Plugins.All
# wildcard.
if (
self.NAME != Plugins.Layout and
(plugin_listen not in self.REQUIRES + self.OPTIONAL)
):
raise SpyderAPIError(
f"Method {method_name} of {self} is trying to watch "
f"plugin {plugin_listen}, but that plugin is not "
f"listed in REQUIRES nor OPTIONAL."
)

logger.debug(
f'Method {method_name} is watching plugin {plugin_listen}'
)
self._plugin_listeners[plugin_listen] = method_name

if hasattr(method, '_plugin_teardown'):
info = method._plugin_teardown
plugin_teardown = method._plugin_teardown

# Check if plugin is listed among REQUIRES and OPTIONAL.
# Note: We can't do this validation for the Layout plugin
# because it depends on all plugins through the Plugins.All
# wildcard.
if (
self.NAME != Plugins.Layout and
(plugin_teardown not in self.REQUIRES + self.OPTIONAL)
):
raise SpyderAPIError(
f"Method {method_name} of {self} is trying to watch "
f"plugin {plugin_teardown}, but that plugin is not "
f"listed in REQUIRES nor OPTIONAL."
)

logger.debug(f'Method {method_name} will handle plugin '
f'teardown for {info}')
self._plugin_teardown_listeners[info] = method_name
f'teardown for {plugin_teardown}')
self._plugin_teardown_listeners[plugin_teardown] = method_name

def _on_plugin_available(self, plugin: str):
"""
Expand Down
1 change: 1 addition & 0 deletions spyder/config/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@
'console/use_project_or_home_directory': False,
'console/use_cwd': True,
'console/use_fixed_directory': False,
'startup/use_project_or_home_directory': True,
'startup/use_fixed_directory': False,
}),
('tours',
Expand Down
13 changes: 8 additions & 5 deletions spyder/plugins/ipythonconsole/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ class IPythonConsole(SpyderDockablePlugin):
# This is required for the new API
NAME = 'ipython_console'
REQUIRES = [Plugins.Console, Plugins.Preferences]
OPTIONAL = [Plugins.Editor, Plugins.History, Plugins.MainMenu]
OPTIONAL = [Plugins.Editor, Plugins.History, Plugins.MainMenu,
Plugins.Projects, Plugins.WorkingDirectory]
TABIFY = [Plugins.History]
WIDGET_CLASS = IPythonConsoleWidget
CONF_SECTION = NAME
Expand Down Expand Up @@ -325,8 +326,6 @@ def on_editor_available(self):
@on_plugin_available(plugin=Plugins.Projects)
def on_projects_available(self):
projects = self.get_plugin(Plugins.Projects)
widget = self.get_widget()
widget.projects_available = True
projects.sig_project_loaded.connect(self._on_project_loaded)
projects.sig_project_closed.connect(self._on_project_closed)

Expand Down Expand Up @@ -369,8 +368,6 @@ def on_editor_teardown(self):
@on_plugin_teardown(plugin=Plugins.Projects)
def on_projects_teardown(self):
projects = self.get_plugin(Plugins.Projects)
widget = self.get_widget()
widget.projects_available = False
projects.sig_project_loaded.disconnect(self._on_project_loaded)
projects.sig_project_closed.disconnect(self._on_project_closed)

Expand Down Expand Up @@ -428,6 +425,12 @@ def _remove_old_std_files(self):
except Exception:
pass

def _get_working_directory(self):
"""Get current working directory from its plugin."""
workdir = self.get_plugin(Plugins.WorkingDirectory)
if workdir:
return workdir.get_workdir()

# ---- Public API
# -------------------------------------------------------------------------

Expand Down
108 changes: 92 additions & 16 deletions spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def get_conda_test_env(test_env_name=u'spytest-ž'):
# Qt Test Fixtures
# =============================================================================
@pytest.fixture
def ipyconsole(qtbot, request):
def ipyconsole(qtbot, request, tmpdir):
"""IPython console fixture."""
configuration = CONF
no_web_widgets = request.node.get_closest_marker('no_web_widgets')
Expand Down Expand Up @@ -128,18 +128,24 @@ def __getattr__(self, attr):
# Tests assume inline backend
configuration.set('ipython_console', 'pylab/backend', 0)

# Start in a new working directory the console
# Start the console in a fixed working directory
use_startup_wdir = request.node.get_closest_marker('use_startup_wdir')
if use_startup_wdir:
new_wdir = osp.join(os.getcwd(), NEW_DIR)
if not osp.exists(new_wdir):
os.mkdir(new_wdir)
configuration.set('workingdir', 'console/use_fixed_directory', True)
configuration.set('workingdir', 'console/fixed_directory', new_wdir)
new_wdir = str(tmpdir.mkdir(NEW_DIR))
configuration.set(
'workingdir',
'startup/use_project_or_home_directory',
False
)
configuration.set('workingdir', 'startup/use_fixed_directory', True)
configuration.set('workingdir', 'startup/fixed_directory', new_wdir)
else:
configuration.set('workingdir', 'console/use_fixed_directory', False)
configuration.set(
'workingdir', 'console/fixed_directory', get_home_dir())
'workingdir',
'startup/use_project_or_home_directory',
True
)
configuration.set('workingdir', 'startup/use_fixed_directory', False)

# Test the console with a non-ascii temp dir
non_ascii_dir = request.node.get_closest_marker('non_ascii_dir')
Expand Down Expand Up @@ -213,6 +219,9 @@ def __getattr__(self, attr):
is_cython=is_cython)
window.setCentralWidget(console.get_widget())

# Return working directory from the plugin
console._get_working_directory = lambda: get_home_dir()

# Set exclamation mark to True
configuration.set('ipython_console', 'pdb_use_exclamation_mark', True)

Expand Down Expand Up @@ -1427,12 +1436,12 @@ def test_remove_old_std_files(ipyconsole, qtbot):
)


@flaky(max_runs=10)
@flaky(max_runs=3)
@pytest.mark.use_startup_wdir
@pytest.mark.skipif(sys.platform.startswith('linux'),
reason="Too flaky on Linux")
def test_console_working_directory(ipyconsole, qtbot):
"""Test for checking the working directory."""
def test_startup_working_directory(ipyconsole, qtbot):
"""
Test that the fixed startup working directory option works as expected.
"""
shell = ipyconsole.get_current_shellwidget()
with qtbot.waitSignal(shell.executed):
shell.execute('import os; cwd = os.getcwd()')
Expand Down Expand Up @@ -1574,7 +1583,6 @@ def check_value(name, value):


@flaky(max_runs=10)
@pytest.mark.use_startup_wdir
def test_pdb_multiline(ipyconsole, qtbot):
"""Test entering a multiline statment into pdb"""
shell = ipyconsole.get_current_shellwidget()
Expand Down Expand Up @@ -1833,7 +1841,6 @@ def test_stdout_poll(ipyconsole, qtbot):


@flaky(max_runs=10)
@pytest.mark.use_startup_wdir
def test_startup_code_pdb(ipyconsole, qtbot):
"""Test that startup code for pdb works."""
shell = ipyconsole.get_current_shellwidget()
Expand Down Expand Up @@ -2237,5 +2244,74 @@ def test_no_infowidget(ipyconsole):
assert client.infowidget is None


@flaky(max_runs=3)
def test_cwd_console_options(ipyconsole, qtbot, tmpdir):
"""
Test that the working directory options for new consoles work as expected.
"""
def get_cwd_of_new_client():
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('import os; cwd = os.getcwd()')

return shell.get_value('cwd')

# --- Check use_project_or_home_directory
ipyconsole.set_conf(
'console/use_project_or_home_directory',
True,
section='workingdir',
)

# Simulate that there's a project open
project_dir = str(tmpdir.mkdir('ipyconsole_project_test'))
ipyconsole.get_widget().update_active_project_path(project_dir)

# Get cwd of new client and assert is the expected one
assert get_cwd_of_new_client() == project_dir

# Reset option
ipyconsole.set_conf(
'console/use_project_or_home_directory',
False,
section='workingdir',
)

# --- Check current working directory
ipyconsole.set_conf('console/use_cwd', True, section='workingdir')

# Simulate a specific directory
cwd_dir = str(tmpdir.mkdir('ipyconsole_cwd_test'))
ipyconsole._get_working_directory = lambda: cwd_dir

# Get cwd of new client and assert is the expected one
assert get_cwd_of_new_client() == cwd_dir

# Reset option
ipyconsole.set_conf('console/use_cwd', False, section='workingdir')

# --- Check fixed working directory
ipyconsole.set_conf(
'console/use_fixed_directory',
True,
section='workingdir'
)

# Simulate a fixed directory
fixed_dir = str(tmpdir.mkdir('ipyconsole_fixed_test'))
ipyconsole.set_conf(
'console/fixed_directory',
fixed_dir,
section='workingdir'
)

# Get cwd of new client and assert is the expected one
assert get_cwd_of_new_client() == fixed_dir


if __name__ == "__main__":
pytest.main()
62 changes: 56 additions & 6 deletions spyder/plugins/ipythonconsole/widgets/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,8 @@
This is the widget used on all its tabs.
"""


# Standard library imports.
# Fix for spyder-ide/spyder#1356.
from __future__ import absolute_import

import logging
import os
import os.path as osp
import re
Expand All @@ -31,7 +28,8 @@
# Local imports
from spyder.api.translations import get_translation
from spyder.api.widgets.mixins import SpyderWidgetMixin
from spyder.config.base import get_module_source_path, running_under_pytest
from spyder.config.base import (
get_home_dir, get_module_source_path, running_under_pytest)
from spyder.utils.icon_manager import ima
from spyder.utils import sourcecode
from spyder.utils.image_path_manager import get_image_path
Expand All @@ -46,8 +44,9 @@
from spyder.widgets.mixins import SaveHistoryMixin


# Localization
# Localization and logging
_ = get_translation('spyder')
logger = logging.getLogger(__name__)

# -----------------------------------------------------------------------------
# Templates
Expand Down Expand Up @@ -222,6 +221,9 @@ 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()

self.shellwidget.sig_prompt_ready.disconnect(
self._when_prompt_is_ready)
self.shellwidget.sig_remote_execute.disconnect(
Expand Down Expand Up @@ -350,6 +352,54 @@ 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")
cwd_path = get_home_dir()
project_path = self.container.get_active_project_path()

# This is for the first client
if self.id_['int_id'] == '1':
if self.get_conf(
'startup/use_project_or_home_directory',
section='workingdir'
):
cwd_path = get_home_dir()
if project_path is not None:
cwd_path = project_path
elif self.get_conf(
'startup/use_fixed_directory',
section='workingdir'
):
cwd_path = self.get_conf(
'startup/fixed_directory',
default=get_home_dir(),
section='workingdir'
)
else:
# For new clients
if self.get_conf(
'console/use_project_or_home_directory',
section='workingdir'
):
cwd_path = get_home_dir()
if project_path is not None:
cwd_path = project_path
elif self.get_conf('console/use_cwd', section='workingdir'):
cwd_path = self.container.get_working_directory()
elif self.get_conf(
'console/use_fixed_directory',
section='workingdir'
):
cwd_path = self.get_conf(
'console/fixed_directory',
default=get_home_dir(),
section='workingdir'
)

if osp.isdir(cwd_path):
self.shellwidget.set_cwd(cwd_path)

# ----- Public API --------------------------------------------------------
@property
def kernel_id(self):
Expand Down
Loading

0 comments on commit 504e7c7

Please sign in to comment.