Repository: spyder-ide/spyder

Issue:
Clearing the variables in a console also clears Pylab and SymPy name spaces and variables
## Issue:

Currently on 3.x if you have automatically load NumPy and Matplotlib(aka Pylab) or SymPy enabled in the IPython configuration variables and namespaces associated with those packages will be removed when you clear all the variables. This would come as a surprise to most uses because they would consider that a property of the console not a user defined variable.

## What happens in similar software:

As a reference starting ipython with the --pylab option and then running %reset removes all of pylab.  Even in isympy running %reset removes SymPy. Octave, Matlab, R and Julia don't do this because those options are in the base language.   

## Options:

All of the options below would need a way to find out the options of the current console stored in its environment variables.

1. The simplest would be to remap clearing the variables to a kernel restart on console with these options enabled.

I find that the console restarts in less than a second on my computer but it seem this is not the case on every platform. 

2. Reload the features after the variables are removed.

Should be faster than a full restart, but I fear there will be code duplication here

3. Remove only user defined variables and not the pre-imported packages

I think that this would be quite difficult and fragile




Discussion:
> The simplest would be to remap clearing the variables to a kernel restart on console with these options enabled.

The issue with this is that it is functionally quite different from just clearing user-defined variables as the user requested; it also wipes any loaded packages/modules, terminates any attaches processes, doesn't reflect any IPython magics (e.g. automatic ``pdb`` calling with ``pdb``, options to the matplotlib backend, etc), clears all scrollback including displayed input, output, and plots, resets the runtime and prompt counters, and otherwise discards all state). This is totally different from what the user expects and what it is "supposed" to do, and is more destructive than anticipated to boot. In fact, I'd consider this a bigger problem than the one it 

Why not just silently run the ``pylab`` command after clearing variables as normal, assuming you've implemented

>  a way to find out the options of the current console stored in its environment variables.

as you say all three require? That seems to be simpler, faster and cleaner than any of them, since its a if statement to check for the pylab option and a short one line command if so.
For sympy, the commands it needs to execute are pretty simple, but perhaps there is a straightforward way to call them from an existing function without a full restart.

So, basically I'm suggesting a variant of 2. I share your concerns about 3., and have even graver reservations about 1. as stated above.
> Why not just silently run the pylab command after clearing variables as normal

I like this approach. It's very simple to implement.
The first option wouldn't work anyway because the clear variable option was made because the restarts were too slow.

> So, basically I'm suggesting a variant of 2.

I think this is way to go as well.  The current functions that run pylab and sympy just run those simple commands which are unlikely to change, but there is some code that deals with matplotlib backend selection that is a bit more complicated. I looks like the setting for the inline figures are still there after clearing all variables.  So that might be just fine.


Resolving patch:
diff --git a/spyder/plugins/ipythonconsole/plugin.py b/spyder/plugins/ipythonconsole/plugin.py
--- a/spyder/plugins/ipythonconsole/plugin.py
+++ b/spyder/plugins/ipythonconsole/plugin.py
@@ -1064,9 +1064,9 @@ def execute_code(self, lines, current_client=True, clear_variables=False):
                         sw.sig_prompt_ready.disconnect()
                     except TypeError:
                         pass
-                    sw.reset_namespace(warning=False, silent=True)
+                    sw.reset_namespace(warning=False)
                 elif current_client and clear_variables:
-                    sw.reset_namespace(warning=False, silent=True)
+                    sw.reset_namespace(warning=False)
                 # Needed to handle an error when kernel_client is none
                 # See issue 6308
                 try:
diff --git a/spyder/plugins/ipythonconsole/widgets/shell.py b/spyder/plugins/ipythonconsole/widgets/shell.py
--- a/spyder/plugins/ipythonconsole/widgets/shell.py
+++ b/spyder/plugins/ipythonconsole/widgets/shell.py
@@ -11,6 +11,7 @@
 import ast
 import os
 import uuid
+from textwrap import dedent
 
 from qtpy.QtCore import Signal
 from qtpy.QtWidgets import QMessageBox
@@ -231,13 +232,14 @@ def clear_console(self):
 
     def _reset_namespace(self):
         warning = CONF.get('ipython_console', 'show_reset_namespace_warning')
-        self.reset_namespace(silent=True, warning=warning)
+        self.reset_namespace(warning=warning)
 
-    def reset_namespace(self, warning=False, silent=True, message=False):
+    def reset_namespace(self, warning=False, message=False):
         """Reset the namespace by removing all names defined by the user."""
         reset_str = _("Remove all variables")
         warn_str = _("All user-defined variables will be removed. "
                      "Are you sure you want to proceed?")
+        kernel_env = self.kernel_manager._kernel_spec.env
 
         if warning:
             box = MessageCheckBox(icon=QMessageBox.Warning, parent=self)
@@ -264,16 +266,26 @@ def reset_namespace(self, warning=False, silent=True, message=False):
             if self._reading:
                 self.dbg_exec_magic('reset', '-f')
             else:
-                if silent:
-                    if message:
-                        self.reset()
-                        self._append_html(_("<br><br>Removing all variables..."
-                                            "\n<hr>"),
-                                          before_prompt=False)
-                    self.silent_execute("%reset -f")
-                    self.refresh_namespacebrowser()
-                else:
-                    self.execute("%reset -f")
+                if message:
+                    self.reset()
+                    self._append_html(_("<br><br>Removing all variables..."
+                                        "\n<hr>"),
+                                      before_prompt=False)
+                self.silent_execute("%reset -f")
+                if kernel_env.get('SPY_AUTOLOAD_PYLAB_O') == 'True':
+                    self.silent_execute("from pylab import *")
+                if kernel_env.get('SPY_SYMPY_O') == 'True':
+                    sympy_init = """
+                        from __future__ import division
+                        from sympy import *
+                        x, y, z, t = symbols('x y z t')
+                        k, m, n = symbols('k m n', integer=True)
+                        f, g, h = symbols('f g h', cls=Function)
+                        init_printing()"""
+                    self.silent_execute(dedent(sympy_init))
+                if kernel_env.get('SPY_RUN_CYTHON') == 'True':
+                    self.silent_execute("%reload_ext Cython")
+                self.refresh_namespacebrowser()
 
                 if not self.external_kernel:
                     self.silent_execute(
diff --git a/spyder/plugins/variableexplorer/widgets/namespacebrowser.py b/spyder/plugins/variableexplorer/widgets/namespacebrowser.py
--- a/spyder/plugins/variableexplorer/widgets/namespacebrowser.py
+++ b/spyder/plugins/variableexplorer/widgets/namespacebrowser.py
@@ -346,8 +346,7 @@ def import_data(self, filenames=None):
     @Slot()
     def reset_namespace(self):
         warning = CONF.get('ipython_console', 'show_reset_namespace_warning')
-        self.shellwidget.reset_namespace(warning=warning, silent=True,
-                                         message=True)
+        self.shellwidget.reset_namespace(warning=warning, message=True)
 
     @Slot()
     def save_data(self, filename=None):


Test patch:
diff --git a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py
--- a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py
+++ b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py
@@ -163,6 +163,18 @@ def test_pylab_client(ipyconsole, qtbot):
     control = ipyconsole.get_focus_widget()
     assert 'Error' not in control.toPlainText()
 
+    # Reset the console namespace
+    shell.reset_namespace(warning=False)
+    qtbot.wait(1000)
+
+    # See that `e` is still defined from numpy after reset
+    with qtbot.waitSignal(shell.executed):
+        shell.execute("e")
+
+    # Assert there are no errors after restting the console
+    control = ipyconsole.get_focus_widget()
+    assert 'Error' not in control.toPlainText()
+
 
 @pytest.mark.slow
 @flaky(max_runs=3)
@@ -184,6 +196,18 @@ def test_sympy_client(ipyconsole, qtbot):
     control = ipyconsole.get_focus_widget()
     assert 'Error' not in control.toPlainText()
 
+    # Reset the console namespace
+    shell.reset_namespace(warning=False)
+    qtbot.wait(1000)
+
+    # See that `e` is still defined from sympy after reset
+    with qtbot.waitSignal(shell.executed):
+        shell.execute("x")
+
+    # Assert there are no errors after restting the console
+    control = ipyconsole.get_focus_widget()
+    assert 'Error' not in control.toPlainText()
+
 
 @pytest.mark.slow
 @flaky(max_runs=3)
@@ -205,6 +229,20 @@ def test_cython_client(ipyconsole, qtbot):
     control = ipyconsole.get_focus_widget()
     assert 'Error' not in control.toPlainText()
 
+    # Reset the console namespace
+    shell.reset_namespace(warning=False)
+    qtbot.wait(1000)
+
+    # See that cython is still enabled after reset
+    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 after restting the console
+    control = ipyconsole.get_focus_widget()
+    assert 'Error' not in control.toPlainText()
+
 
 @pytest.mark.slow
 @flaky(max_runs=3)
@@ -792,7 +830,7 @@ def test_clear_and_reset_magics_dbg(ipyconsole, qtbot):
     qtbot.wait(500)
     assert shell.get_value('bb') == 10
 
-    shell.reset_namespace(warning=False, silent=True)
+    shell.reset_namespace(warning=False)
     qtbot.wait(1000)
 
     qtbot.keyClicks(control, '!bb')