From 97fd022546fd5724b46bb50dd1e4e0788d152825 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sun, 4 Jun 2017 10:54:12 -0500 Subject: [PATCH 1/7] IPython console: Move action to create a new console to be the first one in the client options menu --- spyder/plugins/ipythonconsole.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From be83a8804b5b160cc78f4b9d06fa3be38c470674 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sun, 4 Jun 2017 10:56:51 -0500 Subject: [PATCH 2/7] IPython Console: Add action to get the sys.path contents --- spyder/utils/ipython/spyder_kernel.py | 5 ++++ spyder/widgets/ipythonconsole/client.py | 34 ++++++++++++++++++++++--- spyder/widgets/ipythonconsole/shell.py | 15 +++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/spyder/utils/ipython/spyder_kernel.py b/spyder/utils/ipython/spyder_kernel.py index b91842427ac..f1ef4860c30 100644 --- a/spyder/utils/ipython/spyder_kernel.py +++ b/spyder/utils/ipython/spyder_kernel.py @@ -242,6 +242,11 @@ 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[:] + # -- 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..79e514a1ef6 100644 --- a/spyder/widgets/ipythonconsole/client.py +++ b/spyder/widgets/ipythonconsole/client.py @@ -34,11 +34,13 @@ from spyder.utils.encoding import get_coding 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 +142,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 +197,10 @@ def configure_shellwidget(self, give_focus=True): self.shellwidget.executing.connect( self.shellwidget.change_mpl_backend) + # To show sys.path contents + self.shellwidget.sig_show_syspath.connect( + self.show_syspath) + def enable_stop_button(self): self.stop_button.setEnabled(True) @@ -251,7 +260,15 @@ def get_kernel(self): def get_options_menu(self): """Return options menu""" - return self.menu_actions + syspath_action = create_action( + self, + _("Show sys.path contents"), + icon=ima.icon('syspath'), + triggered=self.shellwidget.get_syspath + ) + additional_actions = [MENU_SEPARATOR, syspath_action] + + return self.menu_actions + additional_actions def get_toolbar_buttons(self): """Return toolbar buttons list.""" @@ -412,6 +429,17 @@ 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 + #------ 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..ed0cb21e79d 100644 --- a/spyder/widgets/ipythonconsole/shell.py +++ b/spyder/widgets/ipythonconsole/shell.py @@ -35,6 +35,7 @@ class ShellWidget(NamepaceBrowserWidget, HelpWidget, DebuggingWidget): # For NamepaceBrowserWidget sig_namespace_view = Signal(object) sig_var_properties = Signal(object) + sig_show_syspath = Signal(object) # For DebuggingWidget sig_pdb_step = Signal(str, int) @@ -85,6 +86,14 @@ 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) + # --- To handle the banner def long_banner(self): """Banner for IPython widgets with pylab message""" @@ -264,6 +273,12 @@ 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) else: if data is not None and 'text/plain' in data: self._kernel_reply = ast.literal_eval(data['text/plain']) From 01c282cf988087425914a6e835cae020dae30b45 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sun, 4 Jun 2017 12:25:20 -0500 Subject: [PATCH 3/7] Make environment variables dialog to be read only --- spyder/utils/environ.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) 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: From cff60428671b0fe581a804e91fe267ac128c78cf Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sun, 4 Jun 2017 12:27:14 -0500 Subject: [PATCH 4/7] IPython console: Add action to show environment variables --- spyder/utils/ipython/spyder_kernel.py | 7 ++++++- spyder/widgets/ipythonconsole/client.py | 22 +++++++++++++++++----- spyder/widgets/ipythonconsole/shell.py | 17 ++++++++++++++++- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/spyder/utils/ipython/spyder_kernel.py b/spyder/utils/ipython/spyder_kernel.py index f1ef4860c30..cf6214f321b 100644 --- a/spyder/utils/ipython/spyder_kernel.py +++ b/spyder/utils/ipython/spyder_kernel.py @@ -238,15 +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""" + """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 79e514a1ef6..fbfb3390611 100644 --- a/spyder/widgets/ipythonconsole/client.py +++ b/spyder/widgets/ipythonconsole/client.py @@ -32,6 +32,7 @@ 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, DialogManager, @@ -197,9 +198,9 @@ def configure_shellwidget(self, give_focus=True): self.shellwidget.executing.connect( self.shellwidget.change_mpl_backend) - # To show sys.path contents - self.shellwidget.sig_show_syspath.connect( - self.show_syspath) + # 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) @@ -260,14 +261,20 @@ def get_kernel(self): def get_options_menu(self): """Return options menu""" + 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, syspath_action] + additional_actions = [MENU_SEPARATOR, env_action, syspath_action] return self.menu_actions + additional_actions def get_toolbar_buttons(self): @@ -431,7 +438,7 @@ def update_history(self): @Slot(object) def show_syspath(self, syspath): - """Show sys.path contents""" + """Show sys.path contents.""" if syspath is not None: editor = CollectionsEditor() editor.setup(syspath, title="sys.path contents", readonly=True, @@ -440,6 +447,11 @@ def show_syspath(self, syspath): 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 ed0cb21e79d..362ae92759b 100644 --- a/spyder/widgets/ipythonconsole/shell.py +++ b/spyder/widgets/ipythonconsole/shell.py @@ -36,6 +36,7 @@ class ShellWidget(NamepaceBrowserWidget, HelpWidget, DebuggingWidget): 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) @@ -87,13 +88,21 @@ def set_cwd(self, dirname): self.silent_execute(code) def get_syspath(self): - """Ask the kernel for sys.path contents""" + """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""" @@ -279,6 +288,12 @@ def handle_exec_method(self, msg): 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']) From d675ff2be0bb1cdb6eb8f5e1613ae7d3d467ce13 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sun, 4 Jun 2017 18:55:15 -0500 Subject: [PATCH 5/7] IPython console: Fix error when running tests --- spyder/widgets/ipythonconsole/client.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spyder/widgets/ipythonconsole/client.py b/spyder/widgets/ipythonconsole/client.py index fbfb3390611..7d5d04748ba 100644 --- a/spyder/widgets/ipythonconsole/client.py +++ b/spyder/widgets/ipythonconsole/client.py @@ -275,7 +275,11 @@ def get_options_menu(self): ) additional_actions = [MENU_SEPARATOR, env_action, syspath_action] - return self.menu_actions + additional_actions + + 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.""" From ac7af8537d7f4539e88bc0f9b37f59b88f358695 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sun, 4 Jun 2017 18:58:00 -0500 Subject: [PATCH 6/7] Testing: Test that showing sys.path contents is working as expected --- spyder/plugins/tests/test_ipythonconsole.py | 34 +++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/spyder/plugins/tests/test_ipythonconsole.py b/spyder/plugins/tests/test_ipythonconsole.py index eb69fbd9b74..c0f1aaa0d10 100644 --- a/spyder/plugins/tests/test_ipythonconsole.py +++ b/spyder/plugins/tests/test_ipythonconsole.py @@ -22,6 +22,7 @@ from spyder.plugins.ipythonconsole import (IPythonConsole, KernelConnectionDialog) from spyder.utils.test import close_message_box +from spyder.widgets.variableexplorer.collectionseditor import CollectionsEditor #============================================================================== @@ -60,6 +61,39 @@ def close_console(): #============================================================================== # Tests #============================================================================== +@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): From 8c03391e3cefa232b38cb26c3f2b8ab5b83a4eed Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sun, 4 Jun 2017 19:14:27 -0500 Subject: [PATCH 7/7] Testing: Test that showing environment variables is working as expected --- spyder/plugins/tests/test_ipythonconsole.py | 28 +++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/spyder/plugins/tests/test_ipythonconsole.py b/spyder/plugins/tests/test_ipythonconsole.py index c0f1aaa0d10..0264f1b6d61 100644 --- a/spyder/plugins/tests/test_ipythonconsole.py +++ b/spyder/plugins/tests/test_ipythonconsole.py @@ -21,6 +21,7 @@ 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 @@ -61,6 +62,33 @@ 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):