Repository: ipython/ipython

Issue:
accessing f_locals in debugger resets namespace
This is the pendant of https://github.com/deshaw/pyflyby/pull/144, see there for a longer explanation.

We do access `current_frame.f_locals` in a couple of place which reset the namespace changes.

Comments:

Patch:
diff --git a/IPython/core/debugger.py b/IPython/core/debugger.py
index 36f0e71a6..e2676a61d 100644
--- a/IPython/core/debugger.py
+++ b/IPython/core/debugger.py
@@ -331,7 +331,7 @@ def _hidden_predicate(self, frame):
             if frame in (self.curframe, getattr(self, "initial_frame", None)):
                 return False
             else:
-                return frame.f_locals.get("__tracebackhide__", False)
+                return self._get_frame_locals(frame).get("__tracebackhide__", False)
 
         return False
 
@@ -435,6 +435,28 @@ def print_stack_entry(self, frame_lineno, prompt_prefix='\n-> ',
         self.shell.hooks.synchronize_with_editor(filename, lineno, 0)
         # vds: <<
 
+    def _get_frame_locals(self, frame):
+        """ "
+        Acessing f_local of current frame reset the namespace, so we want to avoid
+        that or the following can happend
+
+        ipdb> foo
+        "old"
+        ipdb> foo = "new"
+        ipdb> foo
+        "new"
+        ipdb> where
+        ipdb> foo
+        "old"
+
+        So if frame is self.current_frame we instead return self.curframe_locals
+
+        """
+        if frame is self.curframe:
+            return self.curframe_locals
+        else:
+            return frame.f_locals
+
     def format_stack_entry(self, frame_lineno, lprefix=': ', context=None):
         if context is None:
             context = self.context
@@ -459,10 +481,11 @@ def format_stack_entry(self, frame_lineno, lprefix=': ', context=None):
         frame, lineno = frame_lineno
 
         return_value = ''
-        if '__return__' in frame.f_locals:
-            rv = frame.f_locals['__return__']
-            #return_value += '->'
-            return_value += reprlib.repr(rv) + '\n'
+        loc_frame = self._get_frame_locals(frame)
+        if "__return__" in loc_frame:
+            rv = loc_frame["__return__"]
+            # return_value += '->'
+            return_value += reprlib.repr(rv) + "\n"
         ret.append(return_value)
 
         #s = filename + '(' + `lineno` + ')'
@@ -474,10 +497,10 @@ def format_stack_entry(self, frame_lineno, lprefix=': ', context=None):
         else:
             func = "<lambda>"
 
-        call = ''
-        if func != '?':
-            if '__args__' in frame.f_locals:
-                args = reprlib.repr(frame.f_locals['__args__'])
+        call = ""
+        if func != "?":
+            if "__args__" in loc_frame:
+                args = reprlib.repr(loc_frame["__args__"])
             else:
                 args = '()'
             call = tpl_call % (func, args)
@@ -671,7 +694,7 @@ def do_list(self, arg):
 
     def getsourcelines(self, obj):
         lines, lineno = inspect.findsource(obj)
-        if inspect.isframe(obj) and obj.f_globals is obj.f_locals:
+        if inspect.isframe(obj) and obj.f_globals is self._get_frame_locals(obj):
             # must be a module frame: do not try to cut a block out of it
             return lines, 1
         elif inspect.ismodule(obj):
diff --git a/IPython/core/magics/auto.py b/IPython/core/magics/auto.py
index a18542f43..56aa4f72e 100644
--- a/IPython/core/magics/auto.py
+++ b/IPython/core/magics/auto.py
@@ -104,16 +104,32 @@ def autocall(self, parameter_s=''):
         # all-random (note for auto-testing)
         """
 
+        valid_modes = {
+            0: "Off",
+            1: "Smart",
+            2: "Full",
+        }
+
+        def errorMessage() -> str:
+            error = "Valid modes: "
+            for k, v in valid_modes.items():
+                error += str(k) + "->" + v + ", "
+            error = error[:-2]  # remove tailing `, ` after last element
+            return error
+
         if parameter_s:
+            if not parameter_s in map(str, valid_modes.keys()):
+                error(errorMessage())
+                return
             arg = int(parameter_s)
         else:
             arg = 'toggle'
 
-        if not arg in (0, 1, 2, 'toggle'):
-            error('Valid modes: (0->Off, 1->Smart, 2->Full')
+        if not arg in (*list(valid_modes.keys()), "toggle"):
+            error(errorMessage())
             return
 
-        if arg in (0, 1, 2):
+        if arg in (valid_modes.keys()):
             self.shell.autocall = arg
         else: # toggle
             if self.shell.autocall:
@@ -125,4 +141,4 @@ def autocall(self, parameter_s=''):
                 except AttributeError:
                     self.shell.autocall = self._magic_state.autocall_save = 1
 
-        print("Automatic calling is:",['OFF','Smart','Full'][self.shell.autocall])
+        print("Automatic calling is:", list(valid_modes.values())[self.shell.autocall])
diff --git a/IPython/core/magics/osm.py b/IPython/core/magics/osm.py
index dcfa2adf8..c2d3963cd 100644
--- a/IPython/core/magics/osm.py
+++ b/IPython/core/magics/osm.py
@@ -439,11 +439,11 @@ def env(self, parameter_s=''):
 
         Usage:\\
 
-          %env: lists all environment variables/values
-          %env var: get value for var
-          %env var val: set value for var
-          %env var=val: set value for var
-          %env var=$val: set value for var, using python expansion if possible
+          :``%env``: lists all environment variables/values
+          :``%env var``: get value for var
+          :``%env var val``: set value for var
+          :``%env var=val``: set value for var
+          :``%env var=$val``: set value for var, using python expansion if possible
         """
         if parameter_s.strip():
             split = '=' if '=' in parameter_s else ' '
diff --git a/IPython/utils/pickleutil.py b/IPython/utils/pickleutil.py
deleted file mode 100644
index 785e6f6c8..000000000
--- a/IPython/utils/pickleutil.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from warnings import warn
-
-warn("IPython.utils.pickleutil has moved to ipykernel.pickleutil", stacklevel=2)
-
-from ipykernel.pickleutil import *


Test patch:
diff --git a/IPython/core/tests/test_debugger.py b/IPython/core/tests/test_debugger.py
index 2525e658a..35e77e4ac 100644
--- a/IPython/core/tests/test_debugger.py
+++ b/IPython/core/tests/test_debugger.py
@@ -277,14 +277,14 @@ def test_xmode_skip():
 
     block = dedent(
         """
-def f():
-    __tracebackhide__ = True
-    g()
+    def f():
+        __tracebackhide__ = True
+        g()
 
-def g():
-    raise ValueError
+    def g():
+        raise ValueError
 
-f()
+    f()
     """
     )
 
@@ -295,15 +295,15 @@ def g():
 
     block = dedent(
         """
-def f():
-    __tracebackhide__ = True
-    g()
+    def f():
+        __tracebackhide__ = True
+        g()
 
-def g():
-    from IPython.core.debugger import set_trace
-    set_trace()
+    def g():
+        from IPython.core.debugger import set_trace
+        set_trace()
 
-f()
+    f()
     """
     )
 
@@ -321,3 +321,70 @@ def g():
     child.expect("ipdb>")
 
     child.close()
+
+
+@skip_win32
+def test_where_erase_value():
+    """Test that `where` does not access f_locals and erase values."""
+    import pexpect
+
+    env = os.environ.copy()
+    env["IPY_TEST_SIMPLE_PROMPT"] = "1"
+
+    child = pexpect.spawn(
+        sys.executable, ["-m", "IPython", "--colors=nocolor"], env=env
+    )
+    child.timeout = 15 * IPYTHON_TESTING_TIMEOUT_SCALE
+
+    child.expect("IPython")
+    child.expect("\n")
+    child.expect_exact("In [1]")
+
+    block = dedent(
+        """
+    def simple_f():
+         myvar = 1
+         print(myvar)
+         1/0
+         print(myvar)
+    simple_f()    """
+    )
+
+    for line in block.splitlines():
+        child.sendline(line)
+        child.expect_exact(line)
+    child.expect_exact("ZeroDivisionError")
+    child.expect_exact("In [2]:")
+
+    child.sendline("%debug")
+
+    ##
+    child.expect("ipdb>")
+
+    child.sendline("myvar")
+    child.expect("1")
+
+    ##
+    child.expect("ipdb>")
+
+    child.sendline("myvar = 2")
+
+    ##
+    child.expect_exact("ipdb>")
+
+    child.sendline("myvar")
+
+    child.expect_exact("2")
+
+    ##
+    child.expect("ipdb>")
+    child.sendline("where")
+
+    ##
+    child.expect("ipdb>")
+    child.sendline("myvar")
+
+    child.expect_exact("2")
+    child.expect("ipdb>")
+
+    child.close()