Repository: spyder-ide/spyder-kernels

Issue:
Setting variables in recursive debugging namespaces is broken
See the following debugging session:
```
In [1]: %debug print()
NOTE: Enter 'c' at the ipdb>  prompt to continue execution.
> <string>(1)<module>()


IPdb [1]: s
--Call--
> c:\programdata\anaconda3\lib\site-packages\ipykernel\iostream.py(386)write()
    384                 parent=self.parent_header, ident=self.topic)
    385 
--> 386     def write(self, string):
    387         if self.echo is not None:
    388             try:


IPdb [2]: debugcell(0, 'C:/programdata/anaconda3/lib/site-packages/ipykernel/untitled1.py')
> c:\programdata\anaconda3\lib\site-packages\ipykernel\untitled1.py(2)<module>()
      1 # -*- coding: utf-8 -*-
----> 2 """
      3 Created on Wed Sep 29 17:20:08 2021
      4 
      5 @author: Quentin.Peter


(IPdb [1]): test = 2

(IPdb [2]): q

IPdb [3]: test
*** NameError: name 'test' is not defined
```

The problem comes from the following lines in `spyderpdb.py`:
```
        # This is necessary to allow running comprehensions with the
        # frame locals. It also fallbacks to the right globals if the
        # user wants to work with them instead.
        # See spyder-ide/spyder#13909.
        if not 'globals()' in line:
            ns = self.curframe.f_globals.copy()
            ns.update(locals)
        else:
            ns = self.curframe.f_globals
```
Here, the namespace is a copy of the original namespace. This means that the executed code will change the copy of the namespace instead of the namespace itself. So when I do `test = 2`, I add `test` to the copy of the namespace instead of the namespace itself.

Now the strange thing is: why did that work at all? Apparently, when the debugging is made in the main namespace, `self.curframe_locals` is the same object as `self.curframe.f_globals`, so by change this issue is avoided. But this doesn't work if I do the debugging from anywhere else (in my example, I step into `print()`).

So one solution would be to revert the commit, which would mean spyder-ide/spyder#13909 is opened again. 

Discussion:
ccordoba12: @impact27, could you also open a PR on the Spyder side to check that all our tests there pass with these changes? That will help us to be totally sure this doesn't break anything.

Patch:
diff --git a/setup.py b/setup.py
index cc41f1b..4fcf9dc 100644
--- a/setup.py
+++ b/setup.py
@@ -36,6 +36,7 @@ def get_version(module='spyder_kernels'):
 
 
 REQUIREMENTS = [
+    'decorator<5; python_version<"3"',
     'backports.functools-lru-cache; python_version<"3"',
     'cloudpickle',
     'ipykernel<5; python_version<"3"',
diff --git a/spyder_kernels/console/kernel.py b/spyder_kernels/console/kernel.py
index 1494751..0ceecaa 100644
--- a/spyder_kernels/console/kernel.py
+++ b/spyder_kernels/console/kernel.py
@@ -19,7 +19,6 @@ import threading
 # Third-party imports
 import ipykernel
 from ipykernel.ipkernel import IPythonKernel
-from ipykernel.zmqshell import ZMQInteractiveShell
 from traitlets.config.loader import LazyConfigValue
 
 # Local imports
@@ -30,6 +29,7 @@ from spyder_kernels.utils.iofuncs import iofunctions
 from spyder_kernels.utils.mpl import (
     MPL_BACKENDS_FROM_SPYDER, MPL_BACKENDS_TO_SPYDER, INLINE_FIGURE_FORMATS)
 from spyder_kernels.utils.nsview import get_remote_data, make_remote_view
+from spyder_kernels.console.shell import SpyderShell
 
 
 # Excluded variables from the Variable Explorer (i.e. they are not
@@ -37,27 +37,6 @@ from spyder_kernels.utils.nsview import get_remote_data, make_remote_view
 EXCLUDED_NAMES = ['In', 'Out', 'exit', 'get_ipython', 'quit']
 
 
-class SpyderShell(ZMQInteractiveShell):
-    """Spyder shell."""
-
-    def ask_exit(self):
-        """Engage the exit actions."""
-        self.kernel.frontend_comm.close_thread()
-        return super(SpyderShell, self).ask_exit()
-
-    def get_local_scope(self, stack_depth):
-        """Get local scope at given frame depth."""
-        frame = sys._getframe(stack_depth + 1)
-        if self.kernel._pdb_frame is frame:
-            # we also give the globals because they might not be in
-            # self.user_ns
-            namespace = frame.f_globals.copy()
-            namespace.update(self.kernel._pdb_locals)
-            return namespace
-        else:
-            return frame.f_locals
-
-
 class SpyderKernel(IPythonKernel):
     """Spyder kernel for Jupyter."""
 
@@ -105,7 +84,6 @@ class SpyderKernel(IPythonKernel):
                 call_id, handlers[call_id])
 
         self.namespace_view_settings = {}
-        self._pdb_obj = None
         self._pdb_step = None
         self._mpl_backend_error = None
         self._running_namespace = None
@@ -233,7 +211,7 @@ class SpyderKernel(IPythonKernel):
         """
         from spyder_kernels.utils.misc import fix_reference_name
 
-        glbs = self._mglobals()
+        glbs = self.shell.user_ns
         load_func = iofunctions.load_funcs[ext]
         data, error_message = load_func(filename)
 
@@ -263,12 +241,6 @@ class SpyderKernel(IPythonKernel):
         return iofunctions.save(data, filename)
 
     # --- For Pdb
-    def is_debugging(self):
-        """
-        Check if we are currently debugging.
-        """
-        return bool(self._pdb_frame)
-
     def _do_complete(self, code, cursor_pos):
         """Call parent class do_complete"""
         return super(SpyderKernel, self).do_complete(code, cursor_pos)
@@ -279,13 +251,13 @@ class SpyderKernel(IPythonKernel):
 
         Public method of ipykernel overwritten for debugging.
         """
-        if self.is_debugging():
-            return self._pdb_obj.do_complete(code, cursor_pos)
+        if self.shell.is_debugging():
+            return self.shell.pdb_session.do_complete(code, cursor_pos)
         return self._do_complete(code, cursor_pos)
 
     def publish_pdb_state(self):
         """Publish Pdb state."""
-        if self._pdb_obj:
+        if self.shell.pdb_session:
             state = dict(namespace_view = self.get_namespace_view(),
                          var_properties = self.get_var_properties(),
                          step = self._pdb_step)
@@ -295,35 +267,36 @@ class SpyderKernel(IPythonKernel):
         """
         Handle a message from the frontend
         """
-        if self._pdb_obj:
-            self._pdb_obj.set_spyder_breakpoints(breakpoints)
+        if self.shell.pdb_session:
+            self.shell.pdb_session.set_spyder_breakpoints(breakpoints)
 
     def set_pdb_ignore_lib(self, state):
         """
         Change the "Ignore libraries while stepping" debugger setting.
         """
-        if self._pdb_obj:
-            self._pdb_obj.pdb_ignore_lib = state
+        if self.shell.pdb_session:
+            self.shell.pdb_session.pdb_ignore_lib = state
 
     def set_pdb_execute_events(self, state):
         """
         Handle a message from the frontend
         """
-        if self._pdb_obj:
-            self._pdb_obj.pdb_execute_events = state
+        if self.shell.pdb_session:
+            self.shell.pdb_session.pdb_execute_events = state
 
     def set_pdb_use_exclamation_mark(self, state):
         """
         Set an option on the current debugging session to decide wether
         the Pdb commands needs to be prefixed by '!'
         """
-        if self._pdb_obj:
-            self._pdb_obj.pdb_use_exclamation_mark = state
+        if self.shell.pdb_session:
+            self.shell.pdb_session.pdb_use_exclamation_mark = state
 
     def pdb_input_reply(self, line, echo_stack_entry=True):
         """Get a pdb command from the frontend."""
-        if self._pdb_obj:
-            self._pdb_obj._disable_next_stack_entry = not echo_stack_entry
+        if self.shell.pdb_session:
+            self.shell.pdb_session._disable_next_stack_entry = (
+                not echo_stack_entry)
         self._pdb_input_line = line
         if self.eventloop:
             # Interrupting the eventloop is only implemented when a message is
@@ -340,7 +313,7 @@ class SpyderKernel(IPythonKernel):
         Runs the eventloop while debugging.
         """
         # Only works if the comm is open and this is a pdb prompt.
-        if not self.frontend_comm.is_open() or not self._pdb_frame:
+        if not self.frontend_comm.is_open() or not self.shell.is_debugging():
             return input(prompt)
 
         # Flush output before making the request.
@@ -566,16 +539,16 @@ class SpyderKernel(IPythonKernel):
         """
         ns = {}
         if self._running_namespace is None:
-            ns.update(self._mglobals())
+            ns.update(self.shell.user_ns)
         else:
+            # This is true when a file is executing.
             running_globals, running_locals = self._running_namespace
             ns.update(running_globals)
             if running_locals is not None:
                 ns.update(running_locals)
 
-        if self._pdb_frame is not None:
-            ns.update(self._pdb_locals)
-
+        # Add debugging locals
+        ns.update(self.shell._pdb_locals)
         # Add magics to ns so we can show help about them on the Help
         # plugin
         if with_magics:
@@ -583,7 +556,6 @@ class SpyderKernel(IPythonKernel):
             cell_magics = self.shell.magics_manager.magics['cell']
             ns.update(line_magics)
             ns.update(cell_magics)
-
         return ns
 
     def _get_reference_namespace(self, name):
@@ -592,22 +564,10 @@ class SpyderKernel(IPythonKernel):
 
         It returns the globals() if reference has not yet been defined
         """
-        glbs = self._mglobals()
-        if self._pdb_frame is None:
-            return glbs
-        else:
-            lcls = self._pdb_locals
-            if name in lcls:
-                return lcls
-            else:
-                return glbs
-
-    def _mglobals(self):
-        """Return current globals -- handles Pdb frames"""
-        if self._pdb_frame is not None:
-            return self._pdb_frame.f_globals
-        else:
-            return self.shell.user_ns
+        lcls = self.shell._pdb_locals
+        if name in lcls:
+            return lcls
+        return self.shell.user_ns
 
     def _get_len(self, var):
         """Return sequence length"""
@@ -668,28 +628,6 @@ class SpyderKernel(IPythonKernel):
         except:
             return None
 
-    # --- For Pdb
-    def _register_pdb_session(self, pdb_obj):
-        """Register Pdb session to use it later"""
-        self._pdb_obj = pdb_obj
-
-    @property
-    def _pdb_frame(self):
-        """Return current Pdb frame if there is any"""
-        if self._pdb_obj is not None and self._pdb_obj.curframe is not None:
-            return self._pdb_obj.curframe
-
-    @property
-    def _pdb_locals(self):
-        """
-        Return current Pdb frame locals if available. Otherwise
-        return an empty dictionary
-        """
-        if self._pdb_frame:
-            return self._pdb_obj.curframe_locals
-        else:
-            return {}
-
     # --- For the Help plugin
     def _eval(self, text):
         """
diff --git a/spyder_kernels/console/shell.py b/spyder_kernels/console/shell.py
new file mode 100644
index 0000000..ae8d2c3
--- /dev/null
+++ b/spyder_kernels/console/shell.py
@@ -0,0 +1,92 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) 2009- Spyder Kernels Contributors
+#
+# Licensed under the terms of the MIT License
+# (see spyder_kernels/__init__.py for details)
+# -----------------------------------------------------------------------------
+
+"""
+Spyder shell for Jupyter kernels.
+"""
+
+# Standard library imports
+import sys
+
+# Third-party imports
+from ipykernel.zmqshell import ZMQInteractiveShell
+
+
+class SpyderShell(ZMQInteractiveShell):
+    """Spyder shell."""
+
+    def __init__(self, *args, **kwargs):
+        # Create _pdb_obj before __init__
+        self._pdb_obj = None
+        super(SpyderShell, self).__init__(*args, **kwargs)
+
+    def ask_exit(self):
+        """Engage the exit actions."""
+        self.kernel.frontend_comm.close_thread()
+        return super(SpyderShell, self).ask_exit()
+
+    # --- For Pdb namespace integration
+    def get_local_scope(self, stack_depth):
+        """Get local scope at given frame depth."""
+        frame = sys._getframe(stack_depth + 1)
+        if self._pdb_frame is frame:
+            # Avoid calling f_locals on _pdb_frame
+            return self._pdb_obj.curframe_locals
+        else:
+            return frame.f_locals
+
+    def get_global_scope(self, stack_depth):
+        """Get global scope at given frame depth."""
+        frame = sys._getframe(stack_depth + 1)
+        return frame.f_globals
+
+    def is_debugging(self):
+        """
+        Check if we are currently debugging.
+        """
+        return bool(self._pdb_frame)
+
+    @property
+    def pdb_session(self):
+        """Get current pdb session."""
+        return self._pdb_obj
+
+    @pdb_session.setter
+    def pdb_session(self, pdb_obj):
+        """Register Pdb session to use it later"""
+        self._pdb_obj = pdb_obj
+
+    @property
+    def _pdb_frame(self):
+        """Return current Pdb frame if there is any"""
+        if self.pdb_session is not None:
+            return self.pdb_session.curframe
+
+    @property
+    def _pdb_locals(self):
+        """
+        Return current Pdb frame locals if available. Otherwise
+        return an empty dictionary
+        """
+        if self._pdb_frame is not None:
+            return self._pdb_obj.curframe_locals
+        else:
+            return {}
+
+    @property
+    def user_ns(self):
+        """Get the current namespace."""
+        if self._pdb_frame is not None:
+            return self._pdb_frame.f_globals
+        else:
+            return self.__user_ns
+
+    @user_ns.setter
+    def user_ns(self, namespace):
+        """Set user_ns."""
+        self.__user_ns = namespace
diff --git a/spyder_kernels/customize/namespace_manager.py b/spyder_kernels/customize/namespace_manager.py
index 9250c6d..1f29388 100755
--- a/spyder_kernels/customize/namespace_manager.py
+++ b/spyder_kernels/customize/namespace_manager.py
@@ -12,31 +12,6 @@ from IPython.core.getipython import get_ipython
 from spyder_kernels.py3compat import PY2
 
 
-def _get_globals_locals():
-    """Return current namespace."""
-    if get_ipython().kernel.is_debugging():
-        pdb = get_ipython().kernel._pdb_obj
-        ns_locals = pdb.curframe_locals
-        ns_globals = pdb.curframe.f_globals
-    else:
-        ns_locals = None
-        ns_globals = get_ipython().user_ns
-    return ns_globals, ns_locals
-
-
-def _set_globals_locals(ns_globals, ns_locals):
-    """Update current namespace."""
-    if get_ipython().kernel.is_debugging():
-        pdb = get_ipython().kernel._pdb_obj
-        pdb.curframe.f_globals.update(ns_globals)
-        if ns_locals:
-            pdb.curframe_locals.update(ns_locals)
-    else:
-        get_ipython().user_ns.update(ns_globals)
-        if ns_locals:
-            get_ipython().user_ns.update(ns_locals)
-
-
 class NamespaceManager(object):
     """
     Get a namespace and set __file__ to filename for this namespace.
@@ -61,14 +36,18 @@ class NamespaceManager(object):
         Prepare the namespace.
         """
         # Save previous __file__
+        ipython_shell = get_ipython()
         if self.ns_globals is None:
             if self.current_namespace:
-                self.ns_globals, self.ns_locals = _get_globals_locals()
+                # stack_depth = parent of calling function
+                stack_depth = 2
+                self.ns_globals = ipython_shell.get_global_scope(stack_depth)
+                self.ns_locals = ipython_shell.get_local_scope(stack_depth)
                 if '__file__' in self.ns_globals:
                     self._previous_filename = self.ns_globals['__file__']
                 self.ns_globals['__file__'] = self.filename
             else:
-                ipython_shell = get_ipython()
+
                 main_mod = ipython_shell.new_main_mod(
                     self.filename, '__main__')
                 self.ns_globals = main_mod.__dict__
@@ -80,7 +59,7 @@ class NamespaceManager(object):
                 self._reset_main = True
 
         # Save current namespace for access by variable explorer
-        get_ipython().kernel._running_namespace = (
+        ipython_shell.kernel._running_namespace = (
             self.ns_globals, self.ns_locals)
 
         if (self._file_code is not None
@@ -104,14 +83,21 @@ class NamespaceManager(object):
         """
         Reset the namespace.
         """
-        get_ipython().kernel._running_namespace = None
+        ipython_shell = get_ipython()
+        ipython_shell.kernel._running_namespace = None
         if self._previous_filename:
             self.ns_globals['__file__'] = self._previous_filename
         elif '__file__' in self.ns_globals:
             self.ns_globals.pop('__file__')
 
         if not self.current_namespace:
-            _set_globals_locals(self.ns_globals, self.ns_locals)
+            # stack_depth = parent of calling function
+            stack_depth = 2
+            ns_globals = ipython_shell.get_global_scope(stack_depth)
+            ns_locals = ipython_shell.get_local_scope(stack_depth)
+            ns_globals.update(self.ns_globals)
+            if ns_locals and self.ns_locals:
+                ns_locals.update(self.ns_locals)
 
         if self._previous_main:
             sys.modules['__main__'] = self._previous_main
diff --git a/spyder_kernels/customize/spydercustomize.py b/spyder_kernels/customize/spydercustomize.py
index a64c0c0..40265ec 100644
--- a/spyder_kernels/customize/spydercustomize.py
+++ b/spyder_kernels/customize/spydercustomize.py
@@ -469,9 +469,9 @@ def exec_code(code, filename, ns_globals, ns_locals=None, post_mortem=False):
             ipython_shell.showtraceback(exception_only=True)
     except BaseException as error:
         if (isinstance(error, bdb.BdbQuit)
-                and ipython_shell.kernel._pdb_obj):
+                and ipython_shell.pdb_session):
             # Ignore BdbQuit if we are debugging, as it is expected.
-            ipython_shell.kernel._pdb_obj = None
+            ipython_shell.pdb_session = None
         elif post_mortem and isinstance(error, Exception):
             error_type, error, tb = sys.exc_info()
             post_mortem_excepthook(error_type, error, tb)
diff --git a/spyder_kernels/customize/spyderpdb.py b/spyder_kernels/customize/spyderpdb.py
index 6a7672e..1feb7b8 100755
--- a/spyder_kernels/customize/spyderpdb.py
+++ b/spyder_kernels/customize/spyderpdb.py
@@ -34,6 +34,18 @@ else:
 logger = logging.getLogger(__name__)
 
 
+def uses_comprehension(code):
+    """Check if given code uses comprehensions."""
+    comprehension_statements = (
+        ast.ListComp,
+        ast.SetComp,
+        ast.GeneratorExp,
+        ast.DictComp
+    )
+    nodes = ast.walk(ast.parse(code))
+    return any(isinstance(node, comprehension_statements) for node in nodes)
+
+
 class DebugWrapper(object):
     """
     Notifies the frontend when debugging starts/stops
@@ -126,26 +138,21 @@ class SpyderPdb(ipyPdb, object):  # Inherits `object` to call super() in PY2
                        " command instead")
             return
         locals = self.curframe_locals
-
-        # This is necessary to allow running comprehensions with the
-        # frame locals. It also fallbacks to the right globals if the
-        # user wants to work with them instead.
-        # See spyder-ide/spyder#13909.
-        if not 'globals()' in line:
-            ns = self.curframe.f_globals.copy()
-            ns.update(locals)
-        else:
-            ns = self.curframe.f_globals
+        globals = self.curframe.f_globals
 
         if self.pdb_use_exclamation_mark:
             # Find pdb commands executed without !
             cmd, arg, line = self.parseline(line)
             if cmd:
                 cmd_in_namespace = (
-                    cmd in ns or cmd in builtins.__dict__)
+                    cmd in globals
+                    or cmd in locals
+                    or cmd in builtins.__dict__
+                )
                 # Special case for quit and exit
                 if cmd in ("quit", "exit"):
-                    if cmd in ns and isinstance(ns[cmd], ZMQExitAutocall):
+                    if cmd in globals and isinstance(
+                            globals[cmd], ZMQExitAutocall):
                         # Use the pdb call
                         cmd_in_namespace = False
                 cmd_func = getattr(self, 'do_' + cmd, None)
@@ -184,7 +191,31 @@ class SpyderPdb(ipyPdb, object):  # Inherits `object` to call super() in PY2
                 sys.displayhook = self.displayhook
                 if execute_events:
                      get_ipython().events.trigger('pre_execute')
-                exec(code, ns, locals)
+
+                 # Mitigates a CPython bug (https://bugs.python.org/issue41918)
+                 # that prevents running comprehensions with the frame locals
+                 # in Pdb.
+                 # See https://bugs.python.org/issue21161 and
+                 # spyder-ide/spyder#13909.
+                if uses_comprehension(line):
+                    # There are three potential problems with this approach:
+                    # 1. If the code access a globals variable that is
+                    #    masked by a locals variable, it will get the locals
+                    #    one.
+                    # 2. Any edit to that variable will be lost.
+                    # 3. The globals will appear to contain all the locals
+                    #    variables.
+                    fake_globals = globals.copy()
+                    fake_globals.update(locals)
+                    locals_keys = locals.keys()
+                    exec(code, fake_globals, locals)
+                    # Avoid mixing locals and globals
+                    for key in locals_keys:
+                        fake_globals.pop(key, None)
+                    globals.update(fake_globals)
+                else:
+                    exec(code, globals, locals)
+
                 if execute_events:
                      get_ipython().events.trigger('post_execute')
             finally:
@@ -504,8 +535,7 @@ class SpyderPdb(ipyPdb, object):  # Inherits `object` to call super() in PY2
         Register Pdb session after reset.
         """
         super(SpyderPdb, self).reset()
-        kernel = get_ipython().kernel
-        kernel._register_pdb_session(self)
+        get_ipython().pdb_session = self
 
     def do_debug(self, arg):
         """
@@ -528,8 +558,7 @@ class SpyderPdb(ipyPdb, object):  # Inherits `object` to call super() in PY2
                 exc_info = sys.exc_info()[:2]
                 self.error(
                     traceback.format_exception_only(*exc_info)[-1].strip())
-        kernel = get_ipython().kernel
-        kernel._register_pdb_session(self)
+        get_ipython().pdb_session = self
 
     def user_return(self, frame, return_value):
         """This function is called when a return trap is set here."""
@@ -737,10 +766,10 @@ class SpyderPdb(ipyPdb, object):  # Inherits `object` to call super() in PY2
 
 def enter_debugger(filename, continue_if_has_breakpoints, code_format):
     """Enter debugger. Code format should be a format that accept filename."""
-    kernel = get_ipython().kernel
-    recursive = kernel.is_debugging()
+    shell = get_ipython()
+    recursive = shell.is_debugging()
     if recursive:
-        parent_debugger = kernel._pdb_obj
+        parent_debugger = shell.pdb_session
         sys.settrace(None)
         globals = parent_debugger.curframe.f_globals
         locals = parent_debugger.curframe_locals
@@ -770,7 +799,7 @@ def enter_debugger(filename, continue_if_has_breakpoints, code_format):
         # Reset parent debugger
         sys.settrace(parent_debugger.trace_dispatch)
         parent_debugger.lastcmd = debugger.lastcmd
-        kernel._register_pdb_session(parent_debugger)
+        shell.pdb_session = parent_debugger
     else:
         # The breakpoint might not be in the cell
         debugger.run(code)


Test patch:
diff --git a/spyder_kernels/console/tests/test_console_kernel.py b/spyder_kernels/console/tests/test_console_kernel.py
index 745bc62..f020ebd 100644
--- a/spyder_kernels/console/tests/test_console_kernel.py
+++ b/spyder_kernels/console/tests/test_console_kernel.py
@@ -30,7 +30,7 @@ from jupyter_client import BlockingKernelClient
 import numpy as np
 
 # Local imports
-from spyder_kernels.py3compat import PY3, to_text_string
+from spyder_kernels.py3compat import PY2, PY3, to_text_string
 from spyder_kernels.utils.iofuncs import iofunctions
 from spyder_kernels.utils.test_utils import get_kernel, get_log_text
 from spyder_kernels.customize.spyderpdb import SpyderPdb
@@ -754,9 +754,10 @@ def test_do_complete(kernel):
     pdb_obj = SpyderPdb()
     pdb_obj.curframe = inspect.currentframe()
     pdb_obj.completenames = lambda *ignore: ['baba']
-    kernel._pdb_obj = pdb_obj
+    kernel.shell.pdb_session = pdb_obj
     match = kernel.do_complete('ba', 2)
     assert 'baba' in match['matches']
+    pdb_obj.curframe = None
 
 
 @pytest.mark.parametrize("exclude_callables_and_modules", [True, False])
@@ -811,20 +812,72 @@ def test_comprehensions_with_locals_in_pdb(kernel):
     pdb_obj = SpyderPdb()
     pdb_obj.curframe = inspect.currentframe()
     pdb_obj.curframe_locals = pdb_obj.curframe.f_locals
-    kernel._pdb_obj = pdb_obj
+    kernel.shell.pdb_session = pdb_obj
 
     # Create a local variable.
-    kernel._pdb_obj.default('zz = 10')
+    kernel.shell.pdb_session.default('zz = 10')
     assert kernel.get_value('zz') == 10
 
     # Run a list comprehension with this variable.
-    kernel._pdb_obj.default("compr = [zz * i for i in [1, 2, 3]]")
+    kernel.shell.pdb_session.default("compr = [zz * i for i in [1, 2, 3]]")
     assert kernel.get_value('compr') == [10, 20, 30]
 
     # Check that the variable is not reported as being part of globals.
-    kernel._pdb_obj.default("in_globals = 'zz' in globals()")
+    kernel.shell.pdb_session.default("in_globals = 'zz' in globals()")
     assert kernel.get_value('in_globals') == False
 
+    pdb_obj.curframe = None
+    pdb_obj.curframe_locals = None
+
+
+def test_namespaces_in_pdb(kernel):
+    """
+    Test namespaces in pdb
+    """
+    # Define get_ipython for timeit
+    get_ipython = lambda: kernel.shell
+    kernel.shell.user_ns["test"] = 0
+    pdb_obj = SpyderPdb()
+    pdb_obj.curframe = inspect.currentframe()
+    pdb_obj.curframe_locals = pdb_obj.curframe.f_locals
+    kernel.shell.pdb_session = pdb_obj
+
+    # Check adding something to globals works
+    pdb_obj.default("globals()['test2'] = 0")
+    assert pdb_obj.curframe.f_globals["test2"] == 0
+
+    if PY2:
+        # no error method in py2
+        pdb_obj.curframe = None
+        pdb_obj.curframe_locals = None
+        return
+
+    # Create wrapper to check for errors
+    old_error = pdb_obj.error
+    pdb_obj._error_occured = False
+    def error_wrapper(*args, **kwargs):
+        print(args, kwargs)
+        pdb_obj._error_occured = True
+        return old_error(*args, **kwargs)
+    pdb_obj.error = error_wrapper
+
+    # Test globals are visible
+    pdb_obj.curframe.f_globals["test3"] = 0
+    pdb_obj.default("%timeit test3")
+    assert not pdb_obj._error_occured
+
+    # Test locals are visible
+    pdb_obj.curframe_locals["test4"] = 0
+    pdb_obj.default("%timeit test4")
+    assert not pdb_obj._error_occured
+
+    # Test user namespace is not visible
+    pdb_obj.default("%timeit test")
+    assert pdb_obj._error_occured
+
+    pdb_obj.curframe = None
+    pdb_obj.curframe_locals = None
+
 
 if __name__ == "__main__":
     pytest.main()