From 1283c2fce9d77329483e76ee9e19a7d22acaa918 Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Mon, 20 Mar 2023 21:36:34 -0700 Subject: [PATCH 1/8] Remove access to f_locals in pdb --- Lib/pdb.py | 54 ++++++++++++++++++++++++++++++++++---------- Lib/test/test_pdb.py | 11 +++++++++ 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index 3543f53282db15..c7d5b32e1976f8 100755 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -286,8 +286,10 @@ def setup(self, f, tb): self.curframe = self.stack[self.curindex][0] # The f_locals dictionary is updated from the actual frame # locals whenever the .f_locals accessor is called, so we - # cache it here to ensure that modifications are not overwritten. - self.curframe_locals = self.curframe.f_locals + # need to cache it for all frames and never access it again + # during debugging + self.frame_locals = [pair[0].f_locals for pair in self.stack] + self.curframe_locals = self.frame_locals[self.curindex] return self.execRcLines() # Can be executed earlier than 'setup' if desired @@ -347,7 +349,7 @@ def bp_commands(self, frame): self.onecmd(line) self.lastcmd = lastcmd_back if not self.commands_silent[currentbp]: - self.print_stack_entry(self.stack[self.curindex]) + self.print_stack_entry(self.curindex) if self.commands_doprompt[currentbp]: self._cmdloop() self.forget() @@ -422,7 +424,7 @@ def interaction(self, frame, traceback): # a command like "continue") self.forget() return - self.print_stack_entry(self.stack[self.curindex]) + self.print_stack_entry(self.curindex) self._cmdloop() self.forget() @@ -1004,8 +1006,8 @@ def _select_frame(self, number): assert 0 <= number < len(self.stack) self.curindex = number self.curframe = self.stack[self.curindex][0] - self.curframe_locals = self.curframe.f_locals - self.print_stack_entry(self.stack[self.curindex]) + self.curframe_locals = self.frame_locals[self.curindex] + self.print_stack_entry(self.curindex) self.lineno = None def do_up(self, arg): @@ -1162,7 +1164,7 @@ def do_jump(self, arg): # new position self.curframe.f_lineno = arg self.stack[self.curindex] = self.stack[self.curindex][0], arg - self.print_stack_entry(self.stack[self.curindex]) + self.print_stack_entry(self.curindex) except ValueError as e: self.error('Jump failed: %s' % e) do_j = do_jump @@ -1536,19 +1538,47 @@ def complete_unalias(self, text, line, begidx, endidx): def print_stack_trace(self): try: - for frame_lineno in self.stack: - self.print_stack_entry(frame_lineno) + for frame_idx in range(len(self.stack)): + self.print_stack_entry(frame_idx) except KeyboardInterrupt: pass - def print_stack_entry(self, frame_lineno, prompt_prefix=line_prefix): - frame, lineno = frame_lineno + def print_stack_entry(self, frame_idx, prompt_prefix=line_prefix): + frame, lineno = self.stack[frame_idx] if frame is self.curframe: prefix = '> ' else: prefix = ' ' self.message(prefix + - self.format_stack_entry(frame_lineno, prompt_prefix)) + self.format_stack_entry(frame, + lineno, + self.frame_locals[frame_idx], + prompt_prefix)) + + def format_stack_entry(self, frame, lineno, f_locals, lprefix=': '): + """Return a string with information about a stack entry. + + The return string contains the canonical filename, the function name + or '', the input arguments, the return value, and the + line of code (if it exists). + + """ + import linecache, reprlib + filename = self.canonic(frame.f_code.co_filename) + s = '%s(%r)' % (filename, lineno) + if frame.f_code.co_name: + s += frame.f_code.co_name + else: + s += "" + s += '()' + if '__return__' in f_locals: + rv = f_locals['__return__'] + s += '->' + s += reprlib.repr(rv) + line = linecache.getline(filename, lineno, frame.f_globals) + if line: + s += lprefix + line.strip() + return s # Provide help diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index d91bd0b2f03a0f..ea3a9839ac04be 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -1487,6 +1487,9 @@ def test_pdb_issue_gh_101673(): ... '!a = 2', ... 'll', ... 'p a', + ... 'u', + ... 'd', + ... 'p a', ... 'continue' ... ]): ... test_function() @@ -1500,6 +1503,14 @@ def test_pdb_issue_gh_101673(): 3 -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() (Pdb) p a 2 + (Pdb) u + > (10)() + -> test_function() + (Pdb) d + > (3)test_function()->None + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) p a + 2 (Pdb) continue """ From fb82b6ac42ecfcf0b2901233b4dd412401c8d474 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Tue, 21 Mar 2023 04:38:43 +0000 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2023-03-21-04-38-42.gh-issue-102864.FskZue.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2023-03-21-04-38-42.gh-issue-102864.FskZue.rst diff --git a/Misc/NEWS.d/next/Library/2023-03-21-04-38-42.gh-issue-102864.FskZue.rst b/Misc/NEWS.d/next/Library/2023-03-21-04-38-42.gh-issue-102864.FskZue.rst new file mode 100644 index 00000000000000..d86e2f9257a480 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-03-21-04-38-42.gh-issue-102864.FskZue.rst @@ -0,0 +1 @@ +Fixed the bug where switching frames would revert local variable changes From 3d85f0a3b52712e853cc17683fc5a584c4e33220 Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Mon, 20 Mar 2023 23:40:41 -0700 Subject: [PATCH 3/8] Polish comments --- Lib/pdb.py | 3 +++ Lib/test/test_pdb.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index c7d5b32e1976f8..9545412dd53cc1 100755 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -1562,6 +1562,9 @@ def format_stack_entry(self, frame, lineno, f_locals, lprefix=': '): or '', the input arguments, the return value, and the line of code (if it exists). + This function is overwritten because accessing frame.f_locals will + refresh the dictionary. We need to provide cached f_locals to avoid + reverting local variable changes to it """ import linecache, reprlib filename = self.canonic(frame.f_code.co_filename) diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index ea3a9839ac04be..190bdef91c540c 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -1475,9 +1475,9 @@ def test_pdb_issue_gh_94215(): """ def test_pdb_issue_gh_101673(): - """See GH-101673 + """See GH-101673 and GH-102864 - Make sure ll won't revert local variable assignment + Make sure ll and switching frames won't revert local variable assignment >>> def test_function(): ... a = 1 From d346454fad085e2a57aa47d312555ca8a5e954ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Wed, 22 Mar 2023 18:42:47 +0100 Subject: [PATCH 4/8] Don't overload `format_stack_entry` --- Lib/pdb.py | 47 +++++++++++++++-------------------------------- 1 file changed, 15 insertions(+), 32 deletions(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index 9545412dd53cc1..3b05584cdb1586 100755 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -1549,39 +1549,9 @@ def print_stack_entry(self, frame_idx, prompt_prefix=line_prefix): prefix = '> ' else: prefix = ' ' + fp = _FrameProxy(frame, self.frame_locals[frame_idx]) self.message(prefix + - self.format_stack_entry(frame, - lineno, - self.frame_locals[frame_idx], - prompt_prefix)) - - def format_stack_entry(self, frame, lineno, f_locals, lprefix=': '): - """Return a string with information about a stack entry. - - The return string contains the canonical filename, the function name - or '', the input arguments, the return value, and the - line of code (if it exists). - - This function is overwritten because accessing frame.f_locals will - refresh the dictionary. We need to provide cached f_locals to avoid - reverting local variable changes to it - """ - import linecache, reprlib - filename = self.canonic(frame.f_code.co_filename) - s = '%s(%r)' % (filename, lineno) - if frame.f_code.co_name: - s += frame.f_code.co_name - else: - s += "" - s += '()' - if '__return__' in f_locals: - rv = f_locals['__return__'] - s += '->' - s += reprlib.repr(rv) - line = linecache.getline(filename, lineno, frame.f_globals) - if line: - s += lprefix + line.strip() - return s + self.format_stack_entry((fp, lineno), prompt_prefix)) # Provide help @@ -1862,6 +1832,19 @@ def main(): " will be restarted") +class _FrameProxy: + def __init__(self, frame, f_locals): + self.__frame = frame + self.__f_locals = f_locals + + def __getattr__(self, name): + if name[:1] == "_": + raise AttributeError(name) + if name == "f_locals": + return self.__f_locals + return getattr(self.__frame, name) + + # When invoked as main program, invoke the debugger on a script if __name__ == '__main__': import pdb From f2587e66e0e25d2db42fbf4a53da82b095f0d9eb Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Wed, 22 Mar 2023 15:54:15 -0700 Subject: [PATCH 5/8] Use proxy for all frames in pdb --- Lib/pdb.py | 78 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 43 insertions(+), 35 deletions(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index 3b05584cdb1586..19be7b60a2414d 100755 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -276,6 +276,9 @@ def forget(self): def setup(self, f, tb): self.forget() self.stack, self.curindex = self.get_stack(f, tb) + # proxy all the frames because we only want to read f_locals once + self.stack = [(_FrameProxy(frame), lineno) for frame, lineno in self.stack] + while tb: # when setting up post-mortem debugging with a traceback, save all # the original line numbers to be displayed along the current line @@ -284,12 +287,6 @@ def setup(self, f, tb): self.tb_lineno[tb.tb_frame] = lineno tb = tb.tb_next self.curframe = self.stack[self.curindex][0] - # The f_locals dictionary is updated from the actual frame - # locals whenever the .f_locals accessor is called, so we - # need to cache it for all frames and never access it again - # during debugging - self.frame_locals = [pair[0].f_locals for pair in self.stack] - self.curframe_locals = self.frame_locals[self.curindex] return self.execRcLines() # Can be executed earlier than 'setup' if desired @@ -349,7 +346,7 @@ def bp_commands(self, frame): self.onecmd(line) self.lastcmd = lastcmd_back if not self.commands_silent[currentbp]: - self.print_stack_entry(self.curindex) + self.print_stack_entry(self.stack[self.curindex]) if self.commands_doprompt[currentbp]: self._cmdloop() self.forget() @@ -424,7 +421,7 @@ def interaction(self, frame, traceback): # a command like "continue") self.forget() return - self.print_stack_entry(self.curindex) + self.print_stack_entry(self.stack[self.curindex]) self._cmdloop() self.forget() @@ -438,7 +435,7 @@ def displayhook(self, obj): def default(self, line): if line[:1] == '!': line = line[1:] - locals = self.curframe_locals + locals = self.curframe.f_locals globals = self.curframe.f_globals try: code = compile(line + '\n', '', 'single') @@ -566,7 +563,7 @@ def _complete_expression(self, text, line, begidx, endidx): # Collect globals and locals. It is usually not really sensible to also # complete builtins, and they clutter the namespace quite heavily, so we # leave them out. - ns = {**self.curframe.f_globals, **self.curframe_locals} + ns = {**self.curframe.f_globals, **self.curframe.f_locals} if '.' in text: # Walk an attribute chain up to the last part, similar to what # rlcompleter does. This will bail if any of the parts are not @@ -730,7 +727,7 @@ def do_break(self, arg, temporary = 0): try: func = eval(arg, self.curframe.f_globals, - self.curframe_locals) + self.curframe.f_locals) except: func = arg try: @@ -1006,8 +1003,7 @@ def _select_frame(self, number): assert 0 <= number < len(self.stack) self.curindex = number self.curframe = self.stack[self.curindex][0] - self.curframe_locals = self.frame_locals[self.curindex] - self.print_stack_entry(self.curindex) + self.print_stack_entry(self.stack[self.curindex]) self.lineno = None def do_up(self, arg): @@ -1070,7 +1066,7 @@ def do_until(self, arg): return else: lineno = None - self.set_until(self.curframe, lineno) + self.set_until(self.curframe.frame, lineno) return 1 do_unt = do_until @@ -1089,7 +1085,7 @@ def do_next(self, arg): Continue execution until the next line in the current function is reached or it returns. """ - self.set_next(self.curframe) + self.set_next(self.curframe.frame) return 1 do_n = do_next @@ -1118,7 +1114,7 @@ def do_return(self, arg): """r(eturn) Continue execution until the current function returns. """ - self.set_return(self.curframe) + self.set_return(self.curframe.frame) return 1 do_r = do_return @@ -1164,7 +1160,7 @@ def do_jump(self, arg): # new position self.curframe.f_lineno = arg self.stack[self.curindex] = self.stack[self.curindex][0], arg - self.print_stack_entry(self.curindex) + self.print_stack_entry(self.stack[self.curindex]) except ValueError as e: self.error('Jump failed: %s' % e) do_j = do_jump @@ -1177,7 +1173,7 @@ def do_debug(self, arg): """ sys.settrace(None) globals = self.curframe.f_globals - locals = self.curframe_locals + locals = self.curframe.f_locals p = Pdb(self.completekey, self.stdin, self.stdout) p.prompt = "(%s) " % self.prompt.strip() self.message("ENTERING RECURSIVE DEBUGGER") @@ -1216,7 +1212,7 @@ def do_args(self, arg): Print the argument list of the current function. """ co = self.curframe.f_code - dict = self.curframe_locals + dict = self.curframe.f_locals n = co.co_argcount + co.co_kwonlyargcount if co.co_flags & inspect.CO_VARARGS: n = n+1 if co.co_flags & inspect.CO_VARKEYWORDS: n = n+1 @@ -1232,15 +1228,15 @@ def do_retval(self, arg): """retval Print the return value for the last return of a function. """ - if '__return__' in self.curframe_locals: - self.message(repr(self.curframe_locals['__return__'])) + if '__return__' in self.curframe.f_locals: + self.message(repr(self.curframe.f_locals['__return__'])) else: self.error('Not yet returned!') do_rv = do_retval def _getval(self, arg): try: - return eval(arg, self.curframe.f_globals, self.curframe_locals) + return eval(arg, self.curframe.f_globals, self.curframe.f_locals) except: self._error_exc() raise @@ -1248,7 +1244,7 @@ def _getval(self, arg): def _getval_except(self, arg, frame=None): try: if frame is None: - return eval(arg, self.curframe.f_globals, self.curframe_locals) + return eval(arg, self.curframe.f_globals, self.curframe.f_locals) else: return eval(arg, frame.f_globals, frame.f_locals) except: @@ -1350,7 +1346,7 @@ def do_longlist(self, arg): filename = self.curframe.f_code.co_filename breaklist = self.get_file_breaks(filename) try: - lines, lineno = inspect.getsourcelines(self.curframe) + lines, lineno = inspect.getsourcelines(self.curframe.frame) except OSError as err: self.error(err) return @@ -1378,7 +1374,7 @@ def _print_lines(self, lines, start, breaks=(), frame=None): """Print a range of lines.""" if frame: current_lineno = frame.f_lineno - exc_lineno = self.tb_lineno.get(frame, -1) + exc_lineno = self.tb_lineno.get(frame.frame, -1) else: current_lineno = exc_lineno = -1 for lineno, line in enumerate(lines, start): @@ -1474,7 +1470,7 @@ def do_interact(self, arg): Start an interactive interpreter whose global namespace contains all the (global and local) names found in the current scope. """ - ns = {**self.curframe.f_globals, **self.curframe_locals} + ns = {**self.curframe.f_globals, **self.curframe.f_locals} code.interact("*interactive*", local=ns) def do_alias(self, arg): @@ -1538,20 +1534,19 @@ def complete_unalias(self, text, line, begidx, endidx): def print_stack_trace(self): try: - for frame_idx in range(len(self.stack)): - self.print_stack_entry(frame_idx) + for frame_lineno in self.stack: + self.print_stack_entry(frame_lineno) except KeyboardInterrupt: pass - def print_stack_entry(self, frame_idx, prompt_prefix=line_prefix): - frame, lineno = self.stack[frame_idx] + def print_stack_entry(self, frame_lineno, prompt_prefix=line_prefix): + frame, lineno = frame_lineno if frame is self.curframe: prefix = '> ' else: prefix = ' ' - fp = _FrameProxy(frame, self.frame_locals[frame_idx]) self.message(prefix + - self.format_stack_entry((fp, lineno), prompt_prefix)) + self.format_stack_entry(frame_lineno, prompt_prefix)) # Provide help @@ -1833,17 +1828,30 @@ def main(): class _FrameProxy: - def __init__(self, frame, f_locals): + """ + Every time we read f_locals from the FrameObject, it will be refreshed + by the current FAST variables. To avoid missing our changes to the + local variables, we use proxy to only read f_locals once + """ + def __init__(self, frame): self.__frame = frame - self.__f_locals = f_locals + self.__f_locals = frame.f_locals def __getattr__(self, name): - if name[:1] == "_": + if name.startswith("_"): raise AttributeError(name) if name == "f_locals": return self.__f_locals + if name == "frame": + return self.__frame return getattr(self.__frame, name) + def __setattr__(self, name, value): + if name.startswith("_"): + super().__setattr__(name, value) + else: + setattr(self.__frame, name, value) + # When invoked as main program, invoke the debugger on a script if __name__ == '__main__': From e358cd8740a867f26060304ba1f761201b9cf24b Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Wed, 22 Mar 2023 17:46:42 -0700 Subject: [PATCH 6/8] Implemented proxy for Frame and Traceback --- Lib/bdb.py | 72 +++++++++++++++++++++++++++++++++++++++++++- Lib/pdb.py | 65 +++++++++++++-------------------------- Lib/test/test_pdb.py | 15 ++------- 3 files changed, 94 insertions(+), 58 deletions(-) diff --git a/Lib/bdb.py b/Lib/bdb.py index 7f9b09514ffd00..048a7adadd63c2 100644 --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -5,11 +5,77 @@ import os from inspect import CO_GENERATOR, CO_COROUTINE, CO_ASYNC_GENERATOR -__all__ = ["BdbQuit", "Bdb", "Breakpoint"] +__all__ = ["BdbQuit", "Bdb", "Breakpoint", "FrameProxy", "TracebackProxy"] GENERATOR_AND_COROUTINE_FLAGS = CO_GENERATOR | CO_COROUTINE | CO_ASYNC_GENERATOR +class FrameProxy: + """ + Every time we read f_locals from the FrameObject, it will be refreshed + by the current FAST variables. To avoid missing our changes to the + local variables, we use proxy to only read f_locals once + """ + _frames = {} + + def __new__(cls, frame): + if frame is None: + return None + if isinstance(frame, FrameProxy): + # If it's already proxied, return directly + return frame + if frame in cls._frames: + return cls._frames[frame] + return super().__new__(cls) + + def __init__(self, frame): + if not isinstance(frame, FrameProxy): + self.__frame = frame + self.__f_locals = frame.f_locals + self._frames[frame] = self + + def __getattr__(self, name): + if name.startswith("_"): + raise AttributeError(name) + if name == "f_locals": + return self.__f_locals + if name == "frame": + return self.__frame + if name == "f_back": + return FrameProxy(self.__frame.f_back) + return getattr(self.__frame, name) + + def __setattr__(self, name, value): + if name.startswith("_"): + super().__setattr__(name, value) + else: + setattr(self.__frame, name, value) + + +class TracebackProxy: + def __new__(cls, tb): + if tb is None: + return None + if isinstance(tb, TracebackProxy): + # If it's already proxied, return directly + return tb + return super().__new__(cls) + + def __init__(self, tb): + if not isinstance(tb, TracebackProxy): + self.__traceback = tb + + def __getattr__(self, name): + if name.startswith("_"): + raise AttributeError(name) + if name == "tb_frame": + return FrameProxy(self.__traceback.tb_frame) + if name == "tb_next": + tb = self.__traceback.tb_next + return TracebackProxy(tb) + return getattr(self.__traceback, name) + + class BdbQuit(Exception): """Exception to give up completely.""" @@ -57,6 +123,7 @@ def reset(self): """Set values of attributes as ready to start debugging.""" import linecache linecache.checkcache() + FrameProxy._frames = {} self.botframe = None self._set_stopinfo(None, None) @@ -84,6 +151,8 @@ def trace_dispatch(self, frame, event, arg): The arg parameter depends on the previous event. """ + + frame = FrameProxy(frame) if self.quitting: return # None if event == 'line': @@ -328,6 +397,7 @@ def set_trace(self, frame=None): if frame is None: frame = sys._getframe().f_back self.reset() + frame = FrameProxy(frame) while frame: frame.f_trace = self.trace_dispatch self.botframe = frame diff --git a/Lib/pdb.py b/Lib/pdb.py index 19be7b60a2414d..cded8662dd01b2 100755 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -276,9 +276,6 @@ def forget(self): def setup(self, f, tb): self.forget() self.stack, self.curindex = self.get_stack(f, tb) - # proxy all the frames because we only want to read f_locals once - self.stack = [(_FrameProxy(frame), lineno) for frame, lineno in self.stack] - while tb: # when setting up post-mortem debugging with a traceback, save all # the original line numbers to be displayed along the current line @@ -287,6 +284,10 @@ def setup(self, f, tb): self.tb_lineno[tb.tb_frame] = lineno tb = tb.tb_next self.curframe = self.stack[self.curindex][0] + # The f_locals dictionary is updated from the actual frame + # locals whenever the .f_locals accessor is called, so we + # cache it here to ensure that modifications are not overwritten. + self.curframe_locals = self.curframe.f_locals return self.execRcLines() # Can be executed earlier than 'setup' if desired @@ -367,6 +368,7 @@ def user_exception(self, frame, exc_info): if self._wait_for_mainpyfile: return exc_type, exc_value, exc_traceback = exc_info + exc_traceback = bdb.TracebackProxy(exc_traceback) frame.f_locals['__exception__'] = exc_type, exc_value # An 'Internal StopIteration' exception is an exception debug event @@ -435,7 +437,7 @@ def displayhook(self, obj): def default(self, line): if line[:1] == '!': line = line[1:] - locals = self.curframe.f_locals + locals = self.curframe_locals globals = self.curframe.f_globals try: code = compile(line + '\n', '', 'single') @@ -563,7 +565,7 @@ def _complete_expression(self, text, line, begidx, endidx): # Collect globals and locals. It is usually not really sensible to also # complete builtins, and they clutter the namespace quite heavily, so we # leave them out. - ns = {**self.curframe.f_globals, **self.curframe.f_locals} + ns = {**self.curframe.f_globals, **self.curframe_locals} if '.' in text: # Walk an attribute chain up to the last part, similar to what # rlcompleter does. This will bail if any of the parts are not @@ -727,7 +729,7 @@ def do_break(self, arg, temporary = 0): try: func = eval(arg, self.curframe.f_globals, - self.curframe.f_locals) + self.curframe_locals) except: func = arg try: @@ -1003,6 +1005,7 @@ def _select_frame(self, number): assert 0 <= number < len(self.stack) self.curindex = number self.curframe = self.stack[self.curindex][0] + self.curframe_locals = self.curframe.f_locals self.print_stack_entry(self.stack[self.curindex]) self.lineno = None @@ -1066,7 +1069,7 @@ def do_until(self, arg): return else: lineno = None - self.set_until(self.curframe.frame, lineno) + self.set_until(self.curframe, lineno) return 1 do_unt = do_until @@ -1085,7 +1088,7 @@ def do_next(self, arg): Continue execution until the next line in the current function is reached or it returns. """ - self.set_next(self.curframe.frame) + self.set_next(self.curframe) return 1 do_n = do_next @@ -1114,7 +1117,7 @@ def do_return(self, arg): """r(eturn) Continue execution until the current function returns. """ - self.set_return(self.curframe.frame) + self.set_return(self.curframe) return 1 do_r = do_return @@ -1173,7 +1176,7 @@ def do_debug(self, arg): """ sys.settrace(None) globals = self.curframe.f_globals - locals = self.curframe.f_locals + locals = self.curframe_locals p = Pdb(self.completekey, self.stdin, self.stdout) p.prompt = "(%s) " % self.prompt.strip() self.message("ENTERING RECURSIVE DEBUGGER") @@ -1212,7 +1215,7 @@ def do_args(self, arg): Print the argument list of the current function. """ co = self.curframe.f_code - dict = self.curframe.f_locals + dict = self.curframe_locals n = co.co_argcount + co.co_kwonlyargcount if co.co_flags & inspect.CO_VARARGS: n = n+1 if co.co_flags & inspect.CO_VARKEYWORDS: n = n+1 @@ -1228,15 +1231,15 @@ def do_retval(self, arg): """retval Print the return value for the last return of a function. """ - if '__return__' in self.curframe.f_locals: - self.message(repr(self.curframe.f_locals['__return__'])) + if '__return__' in self.curframe_locals: + self.message(repr(self.curframe_locals['__return__'])) else: self.error('Not yet returned!') do_rv = do_retval def _getval(self, arg): try: - return eval(arg, self.curframe.f_globals, self.curframe.f_locals) + return eval(arg, self.curframe.f_globals, self.curframe_locals) except: self._error_exc() raise @@ -1244,7 +1247,7 @@ def _getval(self, arg): def _getval_except(self, arg, frame=None): try: if frame is None: - return eval(arg, self.curframe.f_globals, self.curframe.f_locals) + return eval(arg, self.curframe.f_globals, self.curframe_locals) else: return eval(arg, frame.f_globals, frame.f_locals) except: @@ -1374,7 +1377,7 @@ def _print_lines(self, lines, start, breaks=(), frame=None): """Print a range of lines.""" if frame: current_lineno = frame.f_lineno - exc_lineno = self.tb_lineno.get(frame.frame, -1) + exc_lineno = self.tb_lineno.get(frame, -1) else: current_lineno = exc_lineno = -1 for lineno, line in enumerate(lines, start): @@ -1470,7 +1473,7 @@ def do_interact(self, arg): Start an interactive interpreter whose global namespace contains all the (global and local) names found in the current scope. """ - ns = {**self.curframe.f_globals, **self.curframe.f_locals} + ns = {**self.curframe.f_globals, **self.curframe_locals} code.interact("*interactive*", local=ns) def do_alias(self, arg): @@ -1741,7 +1744,7 @@ def pm(): tb = sys.last_exc.__traceback__ else: tb = sys.last_traceback - post_mortem(tb) + post_mortem(bdb.TracebackProxy(tb)) # Main program for testing @@ -1827,32 +1830,6 @@ def main(): " will be restarted") -class _FrameProxy: - """ - Every time we read f_locals from the FrameObject, it will be refreshed - by the current FAST variables. To avoid missing our changes to the - local variables, we use proxy to only read f_locals once - """ - def __init__(self, frame): - self.__frame = frame - self.__f_locals = frame.f_locals - - def __getattr__(self, name): - if name.startswith("_"): - raise AttributeError(name) - if name == "f_locals": - return self.__f_locals - if name == "frame": - return self.__frame - return getattr(self.__frame, name) - - def __setattr__(self, name, value): - if name.startswith("_"): - super().__setattr__(name, value) - else: - setattr(self.__frame, name, value) - - # When invoked as main program, invoke the debugger on a script if __name__ == '__main__': import pdb diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index 190bdef91c540c..d91bd0b2f03a0f 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -1475,9 +1475,9 @@ def test_pdb_issue_gh_94215(): """ def test_pdb_issue_gh_101673(): - """See GH-101673 and GH-102864 + """See GH-101673 - Make sure ll and switching frames won't revert local variable assignment + Make sure ll won't revert local variable assignment >>> def test_function(): ... a = 1 @@ -1487,9 +1487,6 @@ def test_pdb_issue_gh_101673(): ... '!a = 2', ... 'll', ... 'p a', - ... 'u', - ... 'd', - ... 'p a', ... 'continue' ... ]): ... test_function() @@ -1503,14 +1500,6 @@ def test_pdb_issue_gh_101673(): 3 -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() (Pdb) p a 2 - (Pdb) u - > (10)() - -> test_function() - (Pdb) d - > (3)test_function()->None - -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() - (Pdb) p a - 2 (Pdb) continue """ From a70d520004d4aecc369d95e4bf5dc8e81ce2948a Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Wed, 22 Mar 2023 17:50:03 -0700 Subject: [PATCH 7/8] Replace curframe_f_locals to curframe.f_locals --- Lib/pdb.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index cded8662dd01b2..af6cc914c28cc2 100755 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -284,10 +284,6 @@ def setup(self, f, tb): self.tb_lineno[tb.tb_frame] = lineno tb = tb.tb_next self.curframe = self.stack[self.curindex][0] - # The f_locals dictionary is updated from the actual frame - # locals whenever the .f_locals accessor is called, so we - # cache it here to ensure that modifications are not overwritten. - self.curframe_locals = self.curframe.f_locals return self.execRcLines() # Can be executed earlier than 'setup' if desired @@ -437,7 +433,7 @@ def displayhook(self, obj): def default(self, line): if line[:1] == '!': line = line[1:] - locals = self.curframe_locals + locals = self.curframe.f_locals globals = self.curframe.f_globals try: code = compile(line + '\n', '', 'single') @@ -565,7 +561,7 @@ def _complete_expression(self, text, line, begidx, endidx): # Collect globals and locals. It is usually not really sensible to also # complete builtins, and they clutter the namespace quite heavily, so we # leave them out. - ns = {**self.curframe.f_globals, **self.curframe_locals} + ns = {**self.curframe.f_globals, **self.curframe.f_locals} if '.' in text: # Walk an attribute chain up to the last part, similar to what # rlcompleter does. This will bail if any of the parts are not @@ -729,7 +725,7 @@ def do_break(self, arg, temporary = 0): try: func = eval(arg, self.curframe.f_globals, - self.curframe_locals) + self.curframe.f_locals) except: func = arg try: @@ -1005,7 +1001,6 @@ def _select_frame(self, number): assert 0 <= number < len(self.stack) self.curindex = number self.curframe = self.stack[self.curindex][0] - self.curframe_locals = self.curframe.f_locals self.print_stack_entry(self.stack[self.curindex]) self.lineno = None @@ -1176,7 +1171,7 @@ def do_debug(self, arg): """ sys.settrace(None) globals = self.curframe.f_globals - locals = self.curframe_locals + locals = self.curframe.f_locals p = Pdb(self.completekey, self.stdin, self.stdout) p.prompt = "(%s) " % self.prompt.strip() self.message("ENTERING RECURSIVE DEBUGGER") @@ -1215,7 +1210,7 @@ def do_args(self, arg): Print the argument list of the current function. """ co = self.curframe.f_code - dict = self.curframe_locals + dict = self.curframe.f_locals n = co.co_argcount + co.co_kwonlyargcount if co.co_flags & inspect.CO_VARARGS: n = n+1 if co.co_flags & inspect.CO_VARKEYWORDS: n = n+1 @@ -1231,15 +1226,15 @@ def do_retval(self, arg): """retval Print the return value for the last return of a function. """ - if '__return__' in self.curframe_locals: - self.message(repr(self.curframe_locals['__return__'])) + if '__return__' in self.curframe.f_locals: + self.message(repr(self.curframe.f_locals['__return__'])) else: self.error('Not yet returned!') do_rv = do_retval def _getval(self, arg): try: - return eval(arg, self.curframe.f_globals, self.curframe_locals) + return eval(arg, self.curframe.f_globals, self.curframe.f_locals) except: self._error_exc() raise @@ -1247,7 +1242,7 @@ def _getval(self, arg): def _getval_except(self, arg, frame=None): try: if frame is None: - return eval(arg, self.curframe.f_globals, self.curframe_locals) + return eval(arg, self.curframe.f_globals, self.curframe.f_locals) else: return eval(arg, frame.f_globals, frame.f_locals) except: @@ -1473,7 +1468,7 @@ def do_interact(self, arg): Start an interactive interpreter whose global namespace contains all the (global and local) names found in the current scope. """ - ns = {**self.curframe.f_globals, **self.curframe_locals} + ns = {**self.curframe.f_globals, **self.curframe.f_locals} code.interact("*interactive*", local=ns) def do_alias(self, arg): From ee4e54536ee8b0796c945520e44317e80f0901e9 Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Wed, 22 Mar 2023 18:02:17 -0700 Subject: [PATCH 8/8] Add tests --- Lib/test/test_pdb.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index d91bd0b2f03a0f..190bdef91c540c 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -1475,9 +1475,9 @@ def test_pdb_issue_gh_94215(): """ def test_pdb_issue_gh_101673(): - """See GH-101673 + """See GH-101673 and GH-102864 - Make sure ll won't revert local variable assignment + Make sure ll and switching frames won't revert local variable assignment >>> def test_function(): ... a = 1 @@ -1487,6 +1487,9 @@ def test_pdb_issue_gh_101673(): ... '!a = 2', ... 'll', ... 'p a', + ... 'u', + ... 'd', + ... 'p a', ... 'continue' ... ]): ... test_function() @@ -1500,6 +1503,14 @@ def test_pdb_issue_gh_101673(): 3 -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() (Pdb) p a 2 + (Pdb) u + > (10)() + -> test_function() + (Pdb) d + > (3)test_function()->None + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) p a + 2 (Pdb) continue """