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 3543f53282db15..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 @@ -368,6 +364,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 @@ -436,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') @@ -564,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 @@ -728,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: @@ -1004,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 @@ -1175,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") @@ -1214,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 @@ -1230,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 @@ -1246,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: @@ -1348,7 +1344,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 @@ -1472,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): @@ -1743,7 +1739,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 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 """ 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