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.


Discussion:

Resolving patch:
diff --git a/IPython/core/debugger.py b/IPython/core/debugger.py
--- 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):


Test patch:
diff --git a/IPython/core/tests/test_debugger.py b/IPython/core/tests/test_debugger.py
--- 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()