Repository: spyder-ide/spyder

Issue:
Error when trying to plot local variables in variable explorer when using Spyder 5.3.2
my python file:

import numpy as np
def func():
    bb = np.arange(10)
    return bb      # breakpoint here

a = np.arange(5)
cc = func()
put a breakpoint before return bb and debug the file
the program stops before return, and now two variables in the variable explorer:
* global variable a = array([0, 1, 2, 3, 4])
* local variable bb = array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
right click on a and select plot, no problem. BUT, right click on bb and select bb, report ***KeyError: 'bb'

I find version 5.3.1 hasn't this problem. Only appears in 5.3.2.


Discussion:
Hi @bburne thank you for the feedback! I was able to reproduce this with Spyder 5.3.2:

![imagen](https://user-images.githubusercontent.com/16781833/181808202-c03ee68d-5b5e-45f6-84e7-a5f2f6e31ea5.png)

Pinging @ccordoba12 and @impact27 (maybe they could know what is happening here)
Yes so what is happening is:
When the kernel tries to get the value of 'bb', it will look at the current namespace. But what is the current namespace? Usually it is the shell namespace. While debugging however, if we are stopped on a frame, it will return the frame namespace instead. The problem is that when you run a command, the debugger is not stopped at a prompt, and therefore looks at the global user namespace.
Thanks for the explanation and submitting a fix on spyder-kernels ! 👍🏼 
@impact27, could you add a test here to check that plotting from the Variable Explorer works while debugging? I mean, with the `%varexp` magic?

That way we'll be sure new changes don't break this feature in the future.
Yes I could do that 
Great! Thanks for you help with that too.

Resolving patch:
diff --git a/external-deps/spyder-kernels/spyder_kernels/console/kernel.py b/external-deps/spyder-kernels/spyder_kernels/console/kernel.py
--- a/external-deps/spyder-kernels/spyder_kernels/console/kernel.py
+++ b/external-deps/spyder-kernels/spyder_kernels/console/kernel.py
@@ -344,7 +344,7 @@ def pdb_input_reply(self, line, echo_stack_entry=True):
             # Interrupting the eventloop is only implemented when a message is
             # received on the shell channel, but this message is queued and
             # won't be processed because an `execute` message is being
-            # processed. Therefore we process the message here (comm channel)
+            # processed. Therefore we process the message here (control chan.)
             # and request a dummy message to be sent on the shell channel to
             # stop the eventloop. This will call back `_interrupt_eventloop`.
             self.frontend_call().request_interrupt_eventloop()
@@ -535,7 +535,6 @@ def close_all_mpl_figures(self):
         try:
             import matplotlib.pyplot as plt
             plt.close('all')
-            del plt
         except:
             pass
 
@@ -594,7 +593,7 @@ def _get_current_namespace(self, with_magics=False):
         both locals() and globals() for current frame when debugging
         """
         ns = {}
-        if self.shell.is_debugging() and self.shell.pdb_session.prompt_waiting:
+        if self.shell.is_debugging() and self.shell.pdb_session.curframe:
             # Stopped at a pdb prompt
             ns.update(self.shell.user_ns)
             ns.update(self.shell._pdb_locals)
diff --git a/external-deps/spyder-kernels/spyder_kernels/console/start.py b/external-deps/spyder-kernels/spyder_kernels/console/start.py
--- a/external-deps/spyder-kernels/spyder_kernels/console/start.py
+++ b/external-deps/spyder-kernels/spyder_kernels/console/start.py
@@ -270,11 +270,9 @@ def varexp(line):
         import guiqwt.pyplot as pyplot
     except:
         import matplotlib.pyplot as pyplot
-    __fig__ = pyplot.figure();
-    __items__ = getattr(pyplot, funcname[2:])(
-        ip.kernel._get_current_namespace()[name])
+    pyplot.figure();
+    getattr(pyplot, funcname[2:])(ip.kernel._get_current_namespace()[name])
     pyplot.show()
-    del __fig__, __items__
 
 
 def main():
diff --git a/external-deps/spyder-kernels/spyder_kernels/customize/spyderpdb.py b/external-deps/spyder-kernels/spyder_kernels/customize/spyderpdb.py
--- a/external-deps/spyder-kernels/spyder_kernels/customize/spyderpdb.py
+++ b/external-deps/spyder-kernels/spyder_kernels/customize/spyderpdb.py
@@ -105,9 +105,6 @@ def __init__(self, completekey='tab', stdin=None, stdout=None,
         # Keep track of remote filename
         self.remote_filename = None
 
-        # State of the prompt
-        self.prompt_waiting = False
-
         # Line received from the frontend
         self._cmd_input_line = None
 
@@ -263,8 +260,12 @@ def default(self, line):
                     if out is not None:
                         sys.stdout.flush()
                         sys.stderr.flush()
-                        frontend_request(blocking=False).show_pdb_output(
-                            repr(out))
+                        try:
+                            frontend_request(blocking=False).show_pdb_output(
+                                repr(out))
+                        except (CommError, TimeoutError):
+                            # Fallback
+                            print("pdb out> ", repr(out))
 
             finally:
                 if execute_events:
@@ -360,7 +361,10 @@ def do_where(self, arg):
         Take a number as argument as an (optional) number of context line to
         print"""
         super(SpyderPdb, self).do_where(arg)
-        frontend_request(blocking=False).do_where()
+        try:
+            frontend_request(blocking=False).do_where()
+        except (CommError, TimeoutError):
+            logger.debug("Could not send where request to the frontend.")
 
     do_w = do_where
 
@@ -660,11 +664,9 @@ def cmdloop(self, intro=None):
                 line = self.cmdqueue.pop(0)
             else:
                 try:
-                    self.prompt_waiting = True
                     line = self.cmd_input(self.prompt)
                 except EOFError:
                     line = 'EOF'
-            self.prompt_waiting = False
             line = self.precmd(line)
             stop = self.onecmd(line)
             stop = self.postcmd(stop, line)


Test patch:
diff --git a/external-deps/spyder-kernels/spyder_kernels/console/tests/test_console_kernel.py b/external-deps/spyder-kernels/spyder_kernels/console/tests/test_console_kernel.py
--- a/external-deps/spyder-kernels/spyder_kernels/console/tests/test_console_kernel.py
+++ b/external-deps/spyder-kernels/spyder_kernels/console/tests/test_console_kernel.py
@@ -831,7 +831,6 @@ def test_do_complete(kernel):
     # test pdb
     pdb_obj = SpyderPdb()
     pdb_obj.curframe = inspect.currentframe()
-    pdb_obj.prompt_waiting = True
     pdb_obj.completenames = lambda *ignore: ['baba']
     kernel.shell.pdb_session = pdb_obj
     match = kernel.do_complete('ba', 2)
@@ -890,7 +889,6 @@ def test_comprehensions_with_locals_in_pdb(kernel):
     """
     pdb_obj = SpyderPdb()
     pdb_obj.curframe = inspect.currentframe()
-    pdb_obj.prompt_waiting = True
     pdb_obj.curframe_locals = pdb_obj.curframe.f_locals
     kernel.shell.pdb_session = pdb_obj
 
@@ -917,7 +915,6 @@ def test_comprehensions_with_locals_in_pdb_2(kernel):
     """
     pdb_obj = SpyderPdb()
     pdb_obj.curframe = inspect.currentframe()
-    pdb_obj.prompt_waiting = True
     pdb_obj.curframe_locals = pdb_obj.curframe.f_locals
     kernel.shell.pdb_session = pdb_obj
 
@@ -944,7 +941,6 @@ def test_namespaces_in_pdb(kernel):
     kernel.shell.user_ns["test"] = 0
     pdb_obj = SpyderPdb()
     pdb_obj.curframe = inspect.currentframe()
-    pdb_obj.prompt_waiting = True
     pdb_obj.curframe_locals = pdb_obj.curframe.f_locals
     kernel.shell.pdb_session = pdb_obj
 
@@ -1026,7 +1022,6 @@ def test_functions_with_locals_in_pdb_2(kernel):
     baba = 1
     pdb_obj = SpyderPdb()
     pdb_obj.curframe = inspect.currentframe()
-    pdb_obj.prompt_waiting = True
     pdb_obj.curframe_locals = pdb_obj.curframe.f_locals
     kernel.shell.pdb_session = pdb_obj
 
@@ -1064,7 +1059,6 @@ def test_locals_globals_in_pdb(kernel):
     a = 1
     pdb_obj = SpyderPdb()
     pdb_obj.curframe = inspect.currentframe()
-    pdb_obj.prompt_waiting = True
     pdb_obj.curframe_locals = pdb_obj.curframe.f_locals
     kernel.shell.pdb_session = pdb_obj
 
@@ -1143,5 +1137,49 @@ def test_get_interactive_backend(backend):
             assert value == '0'
 
 
+@flaky(max_runs=3)
+@pytest.mark.skipif(
+    sys.version_info[0] < 3,
+    reason="Fails with python 2")
+def test_debug_namespace(tmpdir):
+    """
+    Test that the kernel uses the proper namespace while debugging.
+    """
+    # Command to start the kernel
+    cmd = "from spyder_kernels.console import start; start.main()"
+
+    with setup_kernel(cmd) as client:
+        # Write code to a file
+        d = tmpdir.join("pdb-ns-test.py")
+        d.write('def func():\n    bb = "hello"\n    breakpoint()\nfunc()')
+
+        # Run code file `d`
+        msg_id = client.execute("runfile(r'{}')".format(to_text_string(d)))
+
+        # make sure that 'bb' returns 'hello'
+        client.get_stdin_msg(timeout=TIMEOUT)
+        client.input('bb')
+
+        t0 = time.time()
+        while True:
+            assert time.time() - t0 < 5
+            msg = client.get_iopub_msg(timeout=TIMEOUT)
+            if msg.get('msg_type') == 'stream':
+                if 'hello' in msg["content"].get("text"):
+                    break
+
+         # make sure that get_value('bb') returns 'hello'
+        client.get_stdin_msg(timeout=TIMEOUT)
+        client.input("get_ipython().kernel.get_value('bb')")
+
+        t0 = time.time()
+        while True:
+            assert time.time() - t0 < 5
+            msg = client.get_iopub_msg(timeout=TIMEOUT)
+            if msg.get('msg_type') == 'stream':
+                if 'hello' in msg["content"].get("text"):
+                    break
+
+
 if __name__ == "__main__":
     pytest.main()
diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py
--- a/spyder/app/tests/test_mainwindow.py
+++ b/spyder/app/tests/test_mainwindow.py
@@ -4033,39 +4033,39 @@ def test_running_namespace_refresh(main_window, qtbot, tmpdir):
     # Clear all breakpoints
     main_window.editor.clear_all_breakpoints()
 
-    # Run file inside a debugger
-    with qtbot.waitSignal(shell.executed):
-        shell.execute(
-            "debugfile(" + repr(str(file1)) + ")"
-        )
-
     shell.execute(
         "runfile(" + repr(str(file2)) + ")"
     )
 
-
     # Check nothing is in the variableexplorer
     nsb = main_window.variableexplorer.current_widget()
     assert len(nsb.editor.source_model._data) == 0
 
     # Wait a bit, refresh, and make sure we captured an in-between value
-    qtbot.wait(300)
+    qtbot.wait(500)
     nsb.refresh_table()
     qtbot.waitUntil(lambda: len(nsb.editor.source_model._data) == 1)
     assert 0 < int(nsb.editor.source_model._data['j']['view']) < 9
 
-    # Wait until continue and stop on the breakpoint
-    qtbot.waitUntil(lambda: "IPdb [2]:" in shell._control.toPlainText())
+    qtbot.waitSignal(shell.executed)
 
-    # Verify that we are still on debugging
-    assert shell.is_waiting_pdb_input()
+    with qtbot.waitSignal(shell.executed):
+        shell.execute(
+            "del j"
+        )
+    qtbot.waitUntil(lambda: len(nsb.editor.source_model._data) == 0)
+
+    # Run file inside a debugger
+    with qtbot.waitSignal(shell.executed):
+        shell.execute(
+            "debugfile(" + repr(str(file1)) + ")"
+        )
 
     # continue
     shell.execute("c")
-    qtbot.wait(300)
+    qtbot.wait(500)
     nsb.refresh_table()
-    qtbot.waitUntil(lambda: len(nsb.editor.source_model._data) == 2)
-    assert nsb.editor.source_model._data['j']['view'] == '9'
+    qtbot.waitUntil(lambda: len(nsb.editor.source_model._data) == 1)
     assert 0 < int(nsb.editor.source_model._data['i']['view']) < 9
 
 
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
@@ -2356,5 +2356,35 @@ def test_startup_run_lines_project_directory(ipyconsole, qtbot, tmpdir):
         section='ipython_console')
 
 
+def test_varexp_magic_dbg_locals(ipyconsole, qtbot):
+    """Test that %varexp is working while debugging locals."""
+
+    # Wait until the window is fully up
+    shell = ipyconsole.get_current_shellwidget()
+    qtbot.waitUntil(
+        lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT)
+
+    with qtbot.waitSignal(shell.executed):
+        shell.execute("def f():\n    li = [1, 2]\n    return li")
+
+    with qtbot.waitSignal(shell.executed):
+        shell.execute("%debug f()")
+
+
+    # Get to an object that can be plotted
+    for _ in range(4):
+        with qtbot.waitSignal(shell.executed):
+            shell.execute("!s")
+
+    # Generate the plot
+    with qtbot.waitSignal(shell.executed):
+        shell.execute("%varexp --plot li")
+
+    qtbot.wait(1000)
+
+    # Assert that there's a plot in the console
+    assert shell._control.toHtml().count('img src') == 1
+
+
 if __name__ == "__main__":
     pytest.main()