Repository: spyder-ide/spyder

Issue:
Shortcuts to open pylab and sympy consoles 
Here is the idea in a picture:

![new console](https://user-images.githubusercontent.com/10513354/39685560-af50d186-5180-11e8-8eeb-c4f39dc9c76e.png)

I find that a pylab(matplotlib and numpy) and sympy consoles are not really compatible with a lot of overlapping commands such as "plot".  

I do like using sympy as a super powered graphing calculator but I can't activate it by default in the config because it's too crazy to have as a default console, and going through the config to enable it when I need it is a pain.

Unfortunately Sympy is broken in spyder due to https://github.com/sympy/sympy/issues/13319 so I won't be able to test.

My work mate who is trying to ditch Matlab and a TI-83(graphing calc) thought this would be great. I think I can do it without too many changes. What do you think? 


Discussion:
Sounds like a fine idea, at least to me. If you're willing to implement it. I'm pretty sure we'd be willing to at least a review a PR. @ccordoba12 ?
I really haven't considered it, but it sounds like a good idea @bcolsen! Thanks for the suggestion!

Could you implement this for Spyder 4? Also, I have a small suggestion: instead of *Open a new Pylab IPython console*, we should show *Open a new Pylab console*. We could also simplify the first entry to *Open a new console* too (now that there's no confusion between Python and IPython consoles).
And the same for Sympy as well, following that reasoning...

Resolving patch:
diff --git a/spyder/plugins/ipythonconsole.py b/spyder/plugins/ipythonconsole.py
--- a/spyder/plugins/ipythonconsole.py
+++ b/spyder/plugins/ipythonconsole.py
@@ -821,13 +821,35 @@ def get_plugin_actions(self):
         """Return a list of actions related to plugin."""
         create_client_action = create_action(
                                    self,
-                                   _("Open an &IPython console"),
+                                   _("New console (default settings)"),
                                    icon=ima.icon('ipython_console'),
                                    triggered=self.create_new_client,
                                    context=Qt.WidgetWithChildrenShortcut)
         self.register_shortcut(create_client_action, context="ipython_console",
                                name="New tab")
 
+        create_pylab_action = create_action(
+                                   self,
+                                   _("New Pylab console (data plotting)"),
+                                   icon=ima.icon('ipython_console'),
+                                   triggered=self.create_pylab_client,
+                                   context=Qt.WidgetWithChildrenShortcut)
+
+        create_sympy_action = create_action(
+                                   self,
+                                   _("New SymPy console (symbolic math)"),
+                                   icon=ima.icon('ipython_console'),
+                                   triggered=self.create_sympy_client,
+                                   context=Qt.WidgetWithChildrenShortcut)
+
+        create_cython_action = create_action(
+                                   self,
+                                   _("New Cython console (Python with "
+                                     "C extensions)"),
+                                   icon=ima.icon('ipython_console'),
+                                   triggered=self.create_cython_client,
+                                   context=Qt.WidgetWithChildrenShortcut)
+
         restart_action = create_action(self, _("Restart kernel"),
                                        icon=ima.icon('restart'),
                                        triggered=self.restart_kernel,
@@ -847,12 +869,17 @@ def get_plugin_actions(self):
         # Add the action to the 'Consoles' menu on the main window
         main_consoles_menu = self.main.consoles_menu_actions
         main_consoles_menu.insert(0, create_client_action)
+        main_consoles_menu.insert(1, create_pylab_action)
+        main_consoles_menu.insert(2, create_sympy_action)
+        main_consoles_menu.insert(3, create_cython_action)
         main_consoles_menu += [MENU_SEPARATOR, restart_action,
                                connect_to_kernel_action,
                                MENU_SEPARATOR]
 
         # Plugin actions
-        self.menu_actions = [create_client_action, MENU_SEPARATOR,
+        self.menu_actions = [create_client_action, create_pylab_action,
+                             create_sympy_action, create_cython_action,
+                             MENU_SEPARATOR,
                              restart_action, connect_to_kernel_action,
                              MENU_SEPARATOR, rename_tab_action,
                              MENU_SEPARATOR]
@@ -1033,7 +1060,8 @@ def write_to_stdin(self, line):
     @Slot(bool, str)
     @Slot(bool, bool)
     @Slot(bool, str, bool)
-    def create_new_client(self, give_focus=True, filename='', is_cython=False):
+    def create_new_client(self, give_focus=True, filename='', is_cython=False,
+                          is_pylab=False, is_sympy=False, given_name=None):
         """Create a new client"""
         self.master_clients += 1
         client_id = dict(int_id=to_text_string(self.master_clients),
@@ -1044,13 +1072,16 @@ def create_new_client(self, give_focus=True, filename='', is_cython=False):
         client = ClientWidget(self, id_=client_id,
                               history_filename=get_conf_path('history.py'),
                               config_options=self.config_options(),
-                              additional_options=self.additional_options(),
+                              additional_options=self.additional_options(
+                                      is_pylab=is_pylab,
+                                      is_sympy=is_sympy),
                               interpreter_versions=self.interpreter_versions(),
                               connection_file=cf,
                               menu_actions=self.menu_actions,
                               options_button=self.options_button,
                               show_elapsed_time=show_elapsed_time,
-                              reset_warning=reset_warning)
+                              reset_warning=reset_warning,
+                              given_name=given_name)
         if self.testing:
             client.stderr_dir = self.test_dir
         self.add_tab(client, name=client.get_name(), filename=filename)
@@ -1084,11 +1115,24 @@ def create_new_client(self, give_focus=True, filename='', is_cython=False):
                                      "<tt>conda install ipykernel cloudpickle</tt>"))
                 return
 
-        self.connect_client_to_kernel(client, is_cython=is_cython)
+        self.connect_client_to_kernel(client, is_cython=is_cython,
+                                      is_pylab=is_pylab, is_sympy=is_sympy)
         if client.shellwidget.kernel_manager is None:
             return
         self.register_client(client)
 
+    def create_pylab_client(self):
+        """Force creation of Pylab client"""
+        self.create_new_client(is_pylab=True, given_name="Pylab")
+
+    def create_sympy_client(self):
+        """Force creation of SymPy client"""
+        self.create_new_client(is_sympy=True, given_name="SymPy")
+
+    def create_cython_client(self):
+        """Force creation of Cython client"""
+        self.create_new_client(is_cython=True, given_name="Cython")
+
     @Slot()
     def create_client_for_kernel(self):
         """Create a client connected to an existing kernel"""
@@ -1100,7 +1144,8 @@ def create_client_for_kernel(self):
             self._create_client_for_kernel(connection_file, hostname, sshkey,
                                            password)
 
-    def connect_client_to_kernel(self, client, is_cython=False):
+    def connect_client_to_kernel(self, client, is_cython=False,
+                                 is_pylab=False, is_sympy=False):
         """Connect a client to its kernel"""
         connection_file = client.connection_file
 
@@ -1112,7 +1157,9 @@ def connect_client_to_kernel(self, client, is_cython=False):
         km, kc = self.create_kernel_manager_and_kernel_client(
                      connection_file,
                      stderr_file,
-                     is_cython=is_cython)
+                     is_cython=is_cython,
+                     is_pylab=is_pylab,
+                     is_sympy=is_sympy)
 
         # An error occurred if this is True
         if is_string(km) and kc is None:
@@ -1259,7 +1306,7 @@ def interpreter_versions(self):
 
         return versions
 
-    def additional_options(self):
+    def additional_options(self, is_pylab=False, is_sympy=False):
         """
         Additional options for shell widgets that are not defined
         in JupyterWidget config options
@@ -1271,6 +1318,13 @@ def additional_options(self):
             show_banner=self.get_option('show_banner')
         )
 
+        if is_pylab is True:
+            options['autoload_pylab'] = True
+            options['sympy'] = False
+        if is_sympy is True:
+            options['autoload_pylab'] = False
+            options['sympy'] = True
+
         return options
 
     def register_client(self, client, give_focus=True):
@@ -1522,20 +1576,28 @@ def tunnel_to_kernel(self, connection_info, hostname, sshkey=None,
                             timeout)
         return tuple(lports)
 
-    def create_kernel_spec(self, is_cython=False):
+    def create_kernel_spec(self, is_cython=False,
+                           is_pylab=False, is_sympy=False):
         """Create a kernel spec for our own kernels"""
         # Before creating our kernel spec, we always need to
         # set this value in spyder.ini
         if not self.testing:
             CONF.set('main', 'spyder_pythonpath',
                      self.main.get_spyder_pythonpath())
-        return SpyderKernelSpec(is_cython=is_cython)
+        return SpyderKernelSpec(is_cython=is_cython,
+                                is_pylab=is_pylab,
+                                is_sympy=is_sympy)
 
     def create_kernel_manager_and_kernel_client(self, connection_file,
-                                                stderr_file, is_cython=False):
+                                                stderr_file,
+                                                is_cython=False,
+                                                is_pylab=False,
+                                                is_sympy=False):
         """Create kernel manager and client."""
         # Kernel spec
-        kernel_spec = self.create_kernel_spec(is_cython=is_cython)
+        kernel_spec = self.create_kernel_spec(is_cython=is_cython,
+                                              is_pylab=is_pylab,
+                                              is_sympy=is_sympy)
         if not kernel_spec.env.get('PYTHONPATH'):
             error_msg = _("This error is most probably caused by installing "
                           "Spyder in a directory with non-ascii characters "
diff --git a/spyder/utils/ipython/kernelspec.py b/spyder/utils/ipython/kernelspec.py
--- a/spyder/utils/ipython/kernelspec.py
+++ b/spyder/utils/ipython/kernelspec.py
@@ -28,9 +28,12 @@ class SpyderKernelSpec(KernelSpec):
 
     spy_path = get_module_source_path('spyder')
 
-    def __init__(self, is_cython=False, **kwargs):
+    def __init__(self, is_cython=False, is_pylab=False,
+                 is_sympy=False, **kwargs):
         super(SpyderKernelSpec, self).__init__(**kwargs)
         self.is_cython = is_cython
+        self.is_pylab = is_pylab
+        self.is_sympy = is_sympy
 
         self.display_name = 'Python 2 (Spyder)' if PY2 else 'Python 3 (Spyder)'
         self.language = 'python2' if PY2 else 'python3'
@@ -129,10 +132,22 @@ def env(self):
             'SPY_GREEDY_O': CONF.get('ipython_console', 'greedy_completer'),
             'SPY_JEDI_O': CONF.get('ipython_console', 'jedi_completer'),
             'SPY_SYMPY_O': CONF.get('ipython_console', 'symbolic_math'),
-            'SPY_RUN_CYTHON': self.is_cython,
             'SPY_TESTING': running_under_pytest() or SAFE_MODE
         }
 
+        if self.is_pylab is True:
+            env_vars['SPY_AUTOLOAD_PYLAB_O'] = True
+            env_vars['SPY_SYMPY_O'] = False
+            env_vars['SPY_RUN_CYTHON'] = False
+        if self.is_sympy is True:
+            env_vars['SPY_AUTOLOAD_PYLAB_O'] = False
+            env_vars['SPY_SYMPY_O'] = True
+            env_vars['SPY_RUN_CYTHON'] = False
+        if self.is_cython is True:
+            env_vars['SPY_AUTOLOAD_PYLAB_O'] = False
+            env_vars['SPY_SYMPY_O'] = False
+            env_vars['SPY_RUN_CYTHON'] = True
+
         # Add our PYTHONPATH to env_vars
         env_vars.update(pypath)
 
@@ -159,5 +174,4 @@ def env(self):
                                                  encoding='utf-8')
             else:
                 env_vars[key] = to_text_string(var)
-
         return env_vars
diff --git a/spyder/widgets/ipythonconsole/client.py b/spyder/widgets/ipythonconsole/client.py
--- a/spyder/widgets/ipythonconsole/client.py
+++ b/spyder/widgets/ipythonconsole/client.py
@@ -297,6 +297,9 @@ def get_name(self):
             # Adding id to name
             client_id = self.id_['int_id'] + u'/' + self.id_['str_id']
             name = name + u' ' + client_id
+        elif self.given_name in ["Pylab", "SymPy", "Cython"]:
+            client_id = self.id_['int_id'] + u'/' + self.id_['str_id']
+            name = self.given_name + u' ' + client_id
         else:
             name = self.given_name + u'/' + self.id_['str_id']
         return name


Test patch:
diff --git a/spyder/plugins/tests/test_ipythonconsole.py b/spyder/plugins/tests/test_ipythonconsole.py
--- a/spyder/plugins/tests/test_ipythonconsole.py
+++ b/spyder/plugins/tests/test_ipythonconsole.py
@@ -27,6 +27,7 @@
 from qtpy import PYQT5
 from qtpy.QtCore import Qt
 import zmq
+import sympy
 
 # Local imports
 from spyder.config.gui import get_color_scheme
@@ -88,12 +89,26 @@ def ipyconsole(qtbot, request):
     if auto_backend:
         CONF.set('ipython_console', 'pylab/backend', 1)
 
+    # Start a Pylab client if requested
+    pylab_client = request.node.get_marker('pylab_client')
+    is_pylab = True if pylab_client else False
+
+    # Start a Sympy client if requested
+    sympy_client = request.node.get_marker('sympy_client')
+    is_sympy = True if sympy_client else False
+
+    # Start a Cython client if requested
+    cython_client = request.node.get_marker('cython_client')
+    is_cython = True if cython_client else False
+
     # Create the console and a new client
     console = IPythonConsole(parent=None,
                              testing=True,
                              test_dir=test_dir,
                              test_no_stderr=test_no_stderr)
-    console.create_new_client()
+    console.create_new_client(is_pylab=is_pylab,
+                              is_sympy=is_sympy,
+                              is_cython=is_cython)
 
     # Close callback
     def close_console():
@@ -132,6 +147,67 @@ def test_auto_backend(ipyconsole, qtbot):
     assert 'Error' not in control.toPlainText()
 
 
+@pytest.mark.slow
+@flaky(max_runs=3)
+@pytest.mark.pylab_client
+def test_pylab_client(ipyconsole, qtbot):
+    """Test that the Pylab console is working correctly."""
+    # Wait until the window is fully up
+    shell = ipyconsole.get_current_shellwidget()
+    qtbot.waitUntil(lambda: shell._prompt_html is not None,
+                    timeout=SHELL_TIMEOUT)
+
+    # This is here to generate further errors
+    with qtbot.waitSignal(shell.executed):
+        shell.execute("e")
+
+    # Assert there are no errors in the console
+    control = ipyconsole.get_focus_widget()
+    assert 'Error' not in control.toPlainText()
+
+
+@pytest.mark.slow
+@flaky(max_runs=3)
+@pytest.mark.sympy_client
+@pytest.mark.xfail('1.0' < sympy.__version__ < '1.2',
+                   reason="A bug with sympy 1.1.1 and IPython-Qtconsole")
+def test_sympy_client(ipyconsole, qtbot):
+    """Test that the SymPy console is working correctly."""
+    # Wait until the window is fully up
+    shell = ipyconsole.get_current_shellwidget()
+    qtbot.waitUntil(lambda: shell._prompt_html is not None,
+                    timeout=SHELL_TIMEOUT)
+
+    # This is here to generate further errors
+    with qtbot.waitSignal(shell.executed):
+        shell.execute("x")
+
+    # Assert there are no errors in the console
+    control = ipyconsole.get_focus_widget()
+    assert 'Error' not in control.toPlainText()
+
+
+@pytest.mark.slow
+@flaky(max_runs=3)
+@pytest.mark.cython_client
+def test_cython_client(ipyconsole, qtbot):
+    """Test that the Cython console is working correctly."""
+    # Wait until the window is fully up
+    shell = ipyconsole.get_current_shellwidget()
+    qtbot.waitUntil(lambda: shell._prompt_html is not None,
+                    timeout=SHELL_TIMEOUT)
+
+    # This is here to generate further errors
+    with qtbot.waitSignal(shell.executed):
+        shell.execute("%%cython\n"
+                      "cdef int ctest(int x, int y):\n"
+                      "    return x + y")
+
+    # Assert there are no errors in the console
+    control = ipyconsole.get_focus_widget()
+    assert 'Error' not in control.toPlainText()
+
+
 @pytest.mark.slow
 @flaky(max_runs=3)
 def test_tab_rename_for_slaves(ipyconsole, qtbot):