diff --git a/spyder/plugins/ipythonconsole.py b/spyder/plugins/ipythonconsole.py index 86ed72b2fcb..02b431a6e9e 100644 --- a/spyder/plugins/ipythonconsole.py +++ b/spyder/plugins/ipythonconsole.py @@ -775,8 +775,8 @@ def get_plugin_actions(self): connect_to_kernel_action] # Plugin actions - self.menu_actions = [restart_action, MENU_SEPARATOR, - create_client_action, connect_to_kernel_action] + self.menu_actions = [create_client_action, MENU_SEPARATOR, + restart_action, connect_to_kernel_action] return self.menu_actions diff --git a/spyder/plugins/tests/test_ipythonconsole.py b/spyder/plugins/tests/test_ipythonconsole.py index eb69fbd9b74..0264f1b6d61 100644 --- a/spyder/plugins/tests/test_ipythonconsole.py +++ b/spyder/plugins/tests/test_ipythonconsole.py @@ -21,7 +21,9 @@ from spyder.py3compat import PY2 from spyder.plugins.ipythonconsole import (IPythonConsole, KernelConnectionDialog) +from spyder.utils.environ import listdict2envdict from spyder.utils.test import close_message_box +from spyder.widgets.variableexplorer.collectionseditor import CollectionsEditor #============================================================================== @@ -60,6 +62,66 @@ def close_console(): #============================================================================== # Tests #============================================================================== +@flaky(max_runs=3) +@pytest.mark.skipif(os.name == 'nt', reason="It doesn't work on Windows") +def test_get_env(ipyconsole, qtbot): + """Test that showing env var contents is working as expected.""" + shell = ipyconsole.get_current_shellwidget() + qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT) + + # Add a new entry to os.environ + with qtbot.waitSignal(shell.executed): + shell.execute("import os; os.environ['FOO'] = 'bar'" ) + + # Ask for os.environ contents + with qtbot.waitSignal(shell.sig_show_env): + shell.get_env() + + # Get env contents from the generated widget + top_level_widgets = QApplication.topLevelWidgets() + for w in top_level_widgets: + if isinstance(w, CollectionsEditor): + env_contents = w.get_value() + qtbot.keyClick(w, Qt.Key_Enter) + + # Assert that our added entry is part of os.environ + env_contents = listdict2envdict(env_contents) + assert env_contents['FOO'] == 'bar' + + +@flaky(max_runs=3) +@pytest.mark.skipif(os.name == 'nt', reason="It doesn't work on Windows") +def test_get_syspath(ipyconsole, qtbot): + """Test that showing sys.path contents is working as expected.""" + shell = ipyconsole.get_current_shellwidget() + qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT) + + # Add a new entry to sys.path + with qtbot.waitSignal(shell.executed): + tmp_dir = tempfile.mkdtemp() + shell.execute("import sys, tempfile; sys.path.append('%s')" % tmp_dir) + + # Ask for sys.path contents + with qtbot.waitSignal(shell.sig_show_syspath): + shell.get_syspath() + + # Get sys.path contents from the generated widget + top_level_widgets = QApplication.topLevelWidgets() + for w in top_level_widgets: + if isinstance(w, CollectionsEditor): + syspath_contents = w.get_value() + qtbot.keyClick(w, Qt.Key_Enter) + + # Assert that our added entry is part of sys.path + assert tmp_dir in syspath_contents + + # Remove temporary directory + try: + os.rmdir(tmp_dir) + except: + pass + + @flaky(max_runs=3) @pytest.mark.skipif(os.name == 'nt', reason="It doesn't work on Windows") def test_browse_history_dbg(ipyconsole, qtbot): diff --git a/spyder/utils/environ.py b/spyder/utils/environ.py index 27ab8138015..aefe498cb5e 100644 --- a/spyder/utils/environ.py +++ b/spyder/utils/environ.py @@ -38,26 +38,21 @@ def listdict2envdict(listdict): class RemoteEnvDialog(CollectionsEditor): - """Remote process environment variables Dialog""" - def __init__(self, get_environ_func, set_environ_func, parent=None): + """Remote process environment variables dialog.""" + + def __init__(self, environ, parent=None): super(RemoteEnvDialog, self).__init__(parent) - self.setup(envdict2listdict(get_environ_func()), - title="os.environ", width=600, icon=ima.icon('environ')) - self.set_environ = set_environ_func - def accept(self): - """Reimplement Qt method""" - self.set_environ(listdict2envdict(self.get_value())) - QDialog.accept(self) + self.setup(envdict2listdict(environ), + title=_("Environment variables"), + width=700, + readonly=True, + icon=ima.icon('environ')) class EnvDialog(RemoteEnvDialog): """Environment variables Dialog""" def __init__(self): - def get_environ_func(): - return dict(os.environ) - def set_environ_func(env): - os.environ = env - RemoteEnvDialog.__init__(self, get_environ_func, set_environ_func) + RemoteEnvDialog.__init__(self, dict(os.environ)) # For Windows only @@ -124,7 +119,7 @@ def __init__(self, parent=None): def accept(self): """Reimplement Qt method""" - set_user_env( listdict2envdict(self.get_value()), parent=self ) + set_user_env(listdict2envdict(self.get_value()), parent=self) QDialog.accept(self) except ImportError: diff --git a/spyder/utils/ipython/spyder_kernel.py b/spyder/utils/ipython/spyder_kernel.py index b91842427ac..cf6214f321b 100644 --- a/spyder/utils/ipython/spyder_kernel.py +++ b/spyder/utils/ipython/spyder_kernel.py @@ -238,10 +238,20 @@ def get_source(self, objtxt): if valid: return getsource(obj) + # --- Additional methods def set_cwd(self, dirname): """Set current working directory.""" return os.chdir(dirname) + def get_syspath(self): + """Return sys.path contents.""" + import sys + return sys.path[:] + + def get_env(self): + """Get environment variables.""" + return os.environ.copy() + # -- Private API --------------------------------------------------- # --- For the Variable Explorer def _get_current_namespace(self, with_magics=False): diff --git a/spyder/widgets/ipythonconsole/client.py b/spyder/widgets/ipythonconsole/client.py index 8b6e6c956c8..7d5d04748ba 100644 --- a/spyder/widgets/ipythonconsole/client.py +++ b/spyder/widgets/ipythonconsole/client.py @@ -32,13 +32,16 @@ from spyder.utils import icon_manager as ima from spyder.utils import sourcecode from spyder.utils.encoding import get_coding +from spyder.utils.environ import RemoteEnvDialog from spyder.utils.programs import TEMPDIR from spyder.utils.qthelpers import (add_actions, create_action, - create_toolbutton) + create_toolbutton, DialogManager, + MENU_SEPARATOR) from spyder.py3compat import to_text_string from spyder.widgets.browser import WebView -from spyder.widgets.mixins import SaveHistoryMixin from spyder.widgets.ipythonconsole import ShellWidget +from spyder.widgets.mixins import SaveHistoryMixin +from spyder.widgets.variableexplorer.collectionseditor import CollectionsEditor #----------------------------------------------------------------------------- @@ -140,6 +143,9 @@ def __init__(self, plugin, name, history_filename, config_options, document = self.get_control().document() document.contentsChange.connect(self._hide_loading_page) + # --- Dialog manager + self.dialog_manager = DialogManager() + #------ Public API -------------------------------------------------------- @property def kernel_id(self): @@ -192,6 +198,10 @@ def configure_shellwidget(self, give_focus=True): self.shellwidget.executing.connect( self.shellwidget.change_mpl_backend) + # To show env and sys.path contents + self.shellwidget.sig_show_syspath.connect(self.show_syspath) + self.shellwidget.sig_show_env.connect(self.show_env) + def enable_stop_button(self): self.stop_button.setEnabled(True) @@ -251,7 +261,25 @@ def get_kernel(self): def get_options_menu(self): """Return options menu""" - return self.menu_actions + env_action = create_action( + self, + _("Show environment variables"), + icon=ima.icon('environ'), + triggered=self.shellwidget.get_env + ) + syspath_action = create_action( + self, + _("Show sys.path contents"), + icon=ima.icon('syspath'), + triggered=self.shellwidget.get_syspath + ) + + additional_actions = [MENU_SEPARATOR, env_action, syspath_action] + + if self.menu_actions is not None: + return self.menu_actions + additional_actions + else: + return additional_actions def get_toolbar_buttons(self): """Return toolbar buttons list.""" @@ -412,6 +440,22 @@ def reset_namespace(self): def update_history(self): self.history = self.shellwidget._history + @Slot(object) + def show_syspath(self, syspath): + """Show sys.path contents.""" + if syspath is not None: + editor = CollectionsEditor() + editor.setup(syspath, title="sys.path contents", readonly=True, + width=600, icon=ima.icon('syspath')) + self.dialog_manager.show(editor) + else: + return + + @Slot(object) + def show_env(self, env): + """Show environment variables.""" + self.dialog_manager.show(RemoteEnvDialog(env)) + #------ Private API ------------------------------------------------------- def _create_loading_page(self): """Create html page to show while the kernel is starting""" diff --git a/spyder/widgets/ipythonconsole/shell.py b/spyder/widgets/ipythonconsole/shell.py index 70f10e380e2..362ae92759b 100644 --- a/spyder/widgets/ipythonconsole/shell.py +++ b/spyder/widgets/ipythonconsole/shell.py @@ -35,6 +35,8 @@ class ShellWidget(NamepaceBrowserWidget, HelpWidget, DebuggingWidget): # For NamepaceBrowserWidget sig_namespace_view = Signal(object) sig_var_properties = Signal(object) + sig_show_syspath = Signal(object) + sig_show_env = Signal(object) # For DebuggingWidget sig_pdb_step = Signal(str, int) @@ -85,6 +87,22 @@ def set_cwd(self, dirname): else: self.silent_execute(code) + def get_syspath(self): + """Ask the kernel for sys.path contents.""" + code = u"get_ipython().kernel.get_syspath()" + if self._reading: + return + else: + self.silent_exec_method(code) + + def get_env(self): + """Ask the kernel for environment variables.""" + code = u"get_ipython().kernel.get_env()" + if self._reading: + return + else: + self.silent_exec_method(code) + # --- To handle the banner def long_banner(self): """Banner for IPython widgets with pylab message""" @@ -264,6 +282,18 @@ def handle_exec_method(self, msg): else: properties = None self.sig_var_properties.emit(properties) + elif 'get_syspath' in method: + if data is not None and 'text/plain' in data: + syspath = ast.literal_eval(data['text/plain']) + else: + syspath = None + self.sig_show_syspath.emit(syspath) + elif 'get_env' in method: + if data is not None and 'text/plain' in data: + env = ast.literal_eval(data['text/plain']) + else: + env = None + self.sig_show_env.emit(env) else: if data is not None and 'text/plain' in data: self._kernel_reply = ast.literal_eval(data['text/plain'])