Skip to content

gh-102864: Remove access to f_locals in pdb #102865

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 71 additions & 1 deletion Lib/bdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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':
Expand Down Expand Up @@ -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
Expand Down
30 changes: 13 additions & 17 deletions Lib/pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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', '<stdin>', 'single')
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand All @@ -1230,23 +1226,23 @@ 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

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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down
15 changes: 13 additions & 2 deletions Lib/test/test_pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -1487,6 +1487,9 @@ def test_pdb_issue_gh_101673():
... '!a = 2',
... 'll',
... 'p a',
... 'u',
... 'd',
... 'p a',
... 'continue'
... ]):
... test_function()
Expand All @@ -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
> <doctest test.test_pdb.test_pdb_issue_gh_101673[1]>(10)<module>()
-> test_function()
(Pdb) d
> <doctest test.test_pdb.test_pdb_issue_gh_101673[0]>(3)test_function()->None
-> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace()
(Pdb) p a
2
(Pdb) continue
"""

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed the bug where switching frames would revert local variable changes