Skip to content

[3.11] gh-90095: Make .pdbrc work properly and add some reasonable te… #116660

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

Merged
merged 1 commit into from
Mar 12, 2024
Merged
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
46 changes: 13 additions & 33 deletions Lib/pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,26 +295,10 @@ def setup(self, f, tb):
# 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
def execRcLines(self):
if not self.rcLines:
return
# local copy because of recursion
rcLines = self.rcLines
rcLines.reverse()
# execute every line only once
self.rcLines = []
while rcLines:
line = rcLines.pop().strip()
if line and line[0] != '#':
if self.onecmd(line):
# if onecmd returns True, the command wants to exit
# from the interaction, save leftover rc lines
# to execute before next interaction
self.rcLines += reversed(rcLines)
return True
if self.rcLines:
self.cmdqueue = self.rcLines
self.rcLines = []

# Override Bdb methods

Expand Down Expand Up @@ -425,12 +409,10 @@ def interaction(self, frame, traceback):
pass
else:
Pdb._previous_sigint_handler = None
if self.setup(frame, traceback):
# no interaction desired at this time (happens if .pdbrc contains
# a command like "continue")
self.forget()
return
self.print_stack_entry(self.stack[self.curindex])
self.setup(frame, traceback)
# if we have more commands to process, do not show the stack entry
if not self.cmdqueue:
self.print_stack_entry(self.stack[self.curindex])
self._cmdloop()
self.forget()

Expand Down Expand Up @@ -484,7 +466,7 @@ def precmd(self, line):
if marker >= 0:
# queue up everything after marker
next = line[marker+2:].lstrip()
self.cmdqueue.append(next)
self.cmdqueue.insert(0, next)
line = line[:marker].rstrip()
return line

Expand All @@ -504,13 +486,12 @@ def handle_command_def(self, line):
"""Handles one command line during command list definition."""
cmd, arg, line = self.parseline(line)
if not cmd:
return
return False
if cmd == 'silent':
self.commands_silent[self.commands_bnum] = True
return # continue to handle other cmd def in the cmd list
return False # continue to handle other cmd def in the cmd list
elif cmd == 'end':
self.cmdqueue = []
return 1 # end of cmd list
return True # end of cmd list
cmdlist = self.commands[self.commands_bnum]
if arg:
cmdlist.append(cmd+' '+arg)
Expand All @@ -524,9 +505,8 @@ def handle_command_def(self, line):
# one of the resuming commands
if func.__name__ in self.commands_resuming:
self.commands_doprompt[self.commands_bnum] = False
self.cmdqueue = []
return 1
return
return True
return False

# interface abstraction functions

Expand Down
150 changes: 88 additions & 62 deletions Lib/test/test_pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -1785,13 +1785,30 @@ def _run_pdb(self, pdb_args, commands, expected_returncode=0):
)
return stdout, stderr

def run_pdb_script(self, script, commands, expected_returncode=0):
def run_pdb_script(self, script, commands,
expected_returncode=0,
pdbrc=None,
remove_home=False):
"""Run 'script' lines with pdb and the pdb 'commands'."""
filename = 'main.py'
with open(filename, 'w') as f:
f.write(textwrap.dedent(script))

if pdbrc is not None:
with open('.pdbrc', 'w') as f:
f.write(textwrap.dedent(pdbrc))
self.addCleanup(os_helper.unlink, '.pdbrc')
self.addCleanup(os_helper.unlink, filename)
return self._run_pdb([filename], commands, expected_returncode)

homesave = None
if remove_home:
homesave = os.environ.pop('HOME', None)
try:
stdout, stderr = self._run_pdb([filename], commands, expected_returncode)
finally:
if homesave is not None:
os.environ['HOME'] = homesave
return stdout, stderr

def run_pdb_module(self, script, commands):
"""Runs the script code as part of a module"""
Expand Down Expand Up @@ -2031,37 +2048,80 @@ def test_issue26053(self):
self.assertRegex(res, "Restarting .* with arguments:\na b c")
self.assertRegex(res, "Restarting .* with arguments:\nd e f")

def test_readrc_kwarg(self):
def test_pdbrc_basic(self):
script = textwrap.dedent("""
import pdb; pdb.Pdb(readrc=False).set_trace()
a = 1
b = 2
""")

print('hello')
pdbrc = textwrap.dedent("""
# Comments should be fine
n
p f"{a+8=}"
""")

save_home = os.environ.pop('HOME', None)
try:
with os_helper.temp_cwd():
with open('.pdbrc', 'w') as f:
f.write("invalid\n")

with open('main.py', 'w') as f:
f.write(script)

cmd = [sys.executable, 'main.py']
proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
stderr=subprocess.PIPE,
)
with proc:
stdout, stderr = proc.communicate(b'q\n')
self.assertNotIn(b"NameError: name 'invalid' is not defined",
stdout)
stdout, stderr = self.run_pdb_script(script, 'q\n', pdbrc=pdbrc, remove_home=True)
self.assertIn("a+8=9", stdout)

finally:
if save_home is not None:
os.environ['HOME'] = save_home
def test_pdbrc_alias(self):
script = textwrap.dedent("""
class A:
def __init__(self):
self.attr = 1
a = A()
b = 2
""")

pdbrc = textwrap.dedent("""
alias pi for k in %1.__dict__.keys(): print(f"%1.{k} = {%1.__dict__[k]}")
until 6
pi a
""")

stdout, stderr = self.run_pdb_script(script, 'q\n', pdbrc=pdbrc, remove_home=True)
self.assertIn("a.attr = 1", stdout)

def test_pdbrc_semicolon(self):
script = textwrap.dedent("""
class A:
def __init__(self):
self.attr = 1
a = A()
b = 2
""")

pdbrc = textwrap.dedent("""
b 5;;c;;n
""")

stdout, stderr = self.run_pdb_script(script, 'q\n', pdbrc=pdbrc, remove_home=True)
self.assertIn("-> b = 2", stdout)

def test_pdbrc_commands(self):
script = textwrap.dedent("""
class A:
def __init__(self):
self.attr = 1
a = A()
b = 2
""")

pdbrc = textwrap.dedent("""
b 6
commands 1 ;; p a;; end
c
""")

stdout, stderr = self.run_pdb_script(script, 'q\n', pdbrc=pdbrc, remove_home=True)
self.assertIn("<__main__.A object at", stdout)

def test_readrc_kwarg(self):
script = textwrap.dedent("""
print('hello')
""")

stdout, stderr = self.run_pdb_script(script, 'q\n', pdbrc='invalid', remove_home=True)
self.assertIn("NameError: name 'invalid' is not defined", stdout)

def test_readrc_homedir(self):
save_home = os.environ.pop("HOME", None)
Expand All @@ -2076,40 +2136,6 @@ def test_readrc_homedir(self):
if save_home is not None:
os.environ["HOME"] = save_home

def test_read_pdbrc_with_ascii_encoding(self):
script = textwrap.dedent("""
import pdb; pdb.Pdb().set_trace()
print('hello')
""")
save_home = os.environ.pop('HOME', None)
try:
with os_helper.temp_cwd():
with open('.pdbrc', 'w', encoding='utf-8') as f:
f.write("Fran\u00E7ais")

with open('main.py', 'w', encoding='utf-8') as f:
f.write(script)

cmd = [sys.executable, 'main.py']
env = {'PYTHONIOENCODING': 'ascii'}
if sys.platform == 'win32':
env['PYTHONLEGACYWINDOWSSTDIO'] = 'non-empty-string'
proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
stderr=subprocess.PIPE,
env={**os.environ, **env}
)
with proc:
stdout, stderr = proc.communicate(b'c\n')
self.assertIn(b"UnicodeEncodeError: \'ascii\' codec can\'t encode character "
b"\'\\xe7\' in position 21: ordinal not in range(128)", stderr)

finally:
if save_home is not None:
os.environ['HOME'] = save_home

def test_header(self):
stdout = StringIO()
header = 'Nobody expects... blah, blah, blah'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Make .pdbrc and -c work with any valid pdb commands.