Skip to content

gh-90095: Make .pdbrc work properly and add some reasonable tests #110496

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 11 commits into from
Mar 11, 2024
47 changes: 13 additions & 34 deletions Lib/pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,26 +363,9 @@ def setup(self, f, tb):
self._chained_exceptions[self._chained_exception_index],
)

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 @@ -571,12 +554,10 @@ def interaction(self, frame, tb_or_exc):
if isinstance(tb_or_exc, BaseException):
assert tb is not None, "main exception must have a traceback"
with self._hold_exceptions(_chained_exceptions):
if self.setup(frame, tb):
# 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, tb)
# 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 @@ -712,7 +693,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()

# Replace all the convenience variables
Expand All @@ -737,13 +718,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 @@ -757,9 +737,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
149 changes: 87 additions & 62 deletions Lib/test/test_pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -2618,13 +2618,29 @@ def _run_pdb(self, pdb_args, commands,

def run_pdb_script(self, script, commands,
expected_returncode=0,
extra_env=None):
extra_env=None,
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, extra_env)

homesave = None
if remove_home:
homesave = os.environ.pop('HOME', None)
try:
stdout, stderr = self._run_pdb([filename], commands, expected_returncode, extra_env)
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 @@ -2904,37 +2920,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 @@ -2949,40 +3008,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.