Skip to content
Open
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
7 changes: 7 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,13 @@ os.path
(Contributed by Petr Viktorin for :cve:`2025-4517`.)


pdb
---

* Use the new interactive shell as the default input shell for :mod:`pdb`.
(Contributed by Tian Gao in :gh:`145379`.)


pickle
------

Expand Down
135 changes: 130 additions & 5 deletions Lib/pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,89 @@ def get_default_backend():
return _default_backend


def _pyrepl_available():
"""return whether pdb should use _pyrepl for input"""
if not os.getenv("PYTHON_BASIC_REPL"):
from _pyrepl.main import CAN_USE_PYREPL

return CAN_USE_PYREPL
return False


class PdbPyReplInput:
def __init__(self, pdb_instance, stdin, stdout, prompt):
import _pyrepl.readline

self.pdb_instance = pdb_instance
self.prompt = prompt
self.console = code.InteractiveConsole()
if not (os.isatty(stdin.fileno())):
raise ValueError("stdin is not a TTY")
self.readline_wrapper = _pyrepl.readline._ReadlineWrapper(
f_in=stdin.fileno(),
f_out=stdout.fileno(),
config=_pyrepl.readline.ReadlineConfig(
completer_delims=frozenset(' \t\n`@#%^&*()=+[{]}\\|;:\'",<>?')
)
)

def readline(self):
from _pyrepl.simple_interact import _more_lines

def more_lines(text):
cmd, _, line = self.pdb_instance.parseline(text)
if not line or not cmd:
return False
func = getattr(self.pdb_instance, 'do_' + cmd, None)
if func is not None:
return False
return _more_lines(self.console, text)

try:
pyrepl_completer = self.readline_wrapper.get_completer()
self.readline_wrapper.set_completer(self.complete)
return (
self.readline_wrapper.multiline_input(
more_lines,
self.prompt,
'... ' + ' ' * (len(self.prompt) - 4)
) + '\n'
)
except EOFError:
return 'EOF'
finally:
self.readline_wrapper.set_completer(pyrepl_completer)

def complete(self, text, state):
"""
This function is very similar to cmd.Cmd.complete.
However, cmd.Cmd.complete assumes that we use readline module, but
pyrepl does not use it.
"""
if state == 0:
origline = self.readline_wrapper.get_line_buffer()
line = origline.lstrip()
stripped = len(origline) - len(line)
begidx = self.readline_wrapper.get_begidx() - stripped
endidx = self.readline_wrapper.get_endidx() - stripped
if begidx>0:
cmd, args, foo = self.pdb_instance.parseline(line)
if not cmd:
compfunc = self.pdb_instance.completedefault
else:
try:
compfunc = getattr(self.pdb_instance, 'complete_' + cmd)
except AttributeError:
compfunc = self.pdb_instance.completedefault
else:
compfunc = self.pdb_instance.completenames
self.completion_matches = compfunc(text, line, begidx, endidx)
try:
return self.completion_matches[state]
except IndexError:
return None


class Pdb(bdb.Bdb, cmd.Cmd):
_previous_sigint_handler = None

Expand Down Expand Up @@ -382,6 +465,12 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None,
except ImportError:
pass

self.pyrepl_input = None
if _pyrepl_available():
try:
self.pyrepl_input = PdbPyReplInput(self, self.stdin, self.stdout, self.prompt)
except Exception:
pass
self.allow_kbdint = False
self.nosigint = nosigint
# Consider these characters as part of the command so when the users type
Expand Down Expand Up @@ -620,14 +709,40 @@ def user_exception(self, frame, exc_info):
self.message('%s%s' % (prefix, self._format_exc(exc_value)))
self.interaction(frame, exc_traceback)

@contextmanager
def _replace_attribute(self, attrs):
original_attrs = {}
for attr, value in attrs.items():
original_attrs[attr] = getattr(self, attr)
setattr(self, attr, value)
try:
yield
finally:
for attr, value in original_attrs.items():
setattr(self, attr, value)

@contextmanager
def _maybe_use_pyrepl_as_stdin(self):
if self.pyrepl_input is None:
yield
return

with self._replace_attribute({
'stdin': self.pyrepl_input,
'use_rawinput': False,
'prompt': '',
}):
yield

# General interaction function
def _cmdloop(self):
while True:
try:
# keyboard interrupts allow for an easy way to cancel
# the current command, so allow them during interactive input
self.allow_kbdint = True
self.cmdloop()
with self._maybe_use_pyrepl_as_stdin():
self.cmdloop()
self.allow_kbdint = False
break
except KeyboardInterrupt:
Expand Down Expand Up @@ -2360,10 +2475,20 @@ def do_interact(self, arg):
contains all the (global and local) names found in the current scope.
"""
ns = {**self.curframe.f_globals, **self.curframe.f_locals}
with self._enable_rlcompleter(ns):
console = _PdbInteractiveConsole(ns, message=self.message)
console.interact(banner="*pdb interact start*",
exitmsg="*exit from pdb interact command*")
console = _PdbInteractiveConsole(ns, message=self.message)
if self.pyrepl_input is not None:
from _pyrepl.simple_interact import run_multiline_interactive_console
self.message("*pdb interact start*")
try:
run_multiline_interactive_console(console)
except SystemExit:
pass
self.message("*exit from pdb interact command*")
else:
with self._enable_rlcompleter(ns):
console = _PdbInteractiveConsole(ns, message=self.message)
console.interact(banner="*pdb interact start*",
exitmsg="*exit from pdb interact command*")

def do_alias(self, arg):
"""alias [name [command]]
Expand Down
103 changes: 76 additions & 27 deletions Lib/test/test_pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import io
import os
import pdb
import re
import sys
import types
import codecs
Expand Down Expand Up @@ -5006,6 +5007,20 @@ def setUpClass(cls):
if readline.backend == "editline":
raise unittest.SkipTest("libedit readline is not supported for pdb")

def _run_pty(self, script, input, env=None):
if env is None:
# By default, we use basic repl for the test.
# Subclass can overwrite this method and set env to use advanced REPL
env = os.environ | {'PYTHON_BASIC_REPL': '1'}
output = run_pty(script, input, env=env)
# filter all control characters
# Strip ANSI CSI sequences (good enough for most REPL/prompt output)
output = re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", output.decode("utf-8"))
return output

def _pyrepl_available(self):
return pdb._pyrepl_available()

def test_basic_completion(self):
script = textwrap.dedent("""
import pdb; pdb.Pdb().set_trace()
Expand All @@ -5017,12 +5032,12 @@ def test_basic_completion(self):
# then add ntin and complete 'contin' to 'continue'
input = b"co\t\tntin\t\n"

output = run_pty(script, input)
output = self._run_pty(script, input)

self.assertIn(b'commands', output)
self.assertIn(b'condition', output)
self.assertIn(b'continue', output)
self.assertIn(b'hello!', output)
self.assertIn('commands', output)
self.assertIn('condition', output)
self.assertIn('continue', output)
self.assertIn('hello!', output)

def test_expression_completion(self):
script = textwrap.dedent("""
Expand All @@ -5039,11 +5054,11 @@ def test_expression_completion(self):
# Continue
input += b"c\n"

output = run_pty(script, input)
output = self._run_pty(script, input)

self.assertIn(b'special', output)
self.assertIn(b'species', output)
self.assertIn(b'$_frame', output)
self.assertIn('special', output)
self.assertIn('species', output)
self.assertIn('$_frame', output)

def test_builtin_completion(self):
script = textwrap.dedent("""
Expand All @@ -5057,9 +5072,9 @@ def test_builtin_completion(self):
# Continue
input += b"c\n"

output = run_pty(script, input)
output = self._run_pty(script, input)

self.assertIn(b'special', output)
self.assertIn('special', output)

def test_convvar_completion(self):
script = textwrap.dedent("""
Expand All @@ -5075,10 +5090,10 @@ def test_convvar_completion(self):
# Continue
input += b"c\n"

output = run_pty(script, input)
output = self._run_pty(script, input)

self.assertIn(b'<frame at 0x', output)
self.assertIn(b'102', output)
self.assertIn('<frame at 0x', output)
self.assertIn('102', output)

def test_local_namespace(self):
script = textwrap.dedent("""
Expand All @@ -5094,9 +5109,9 @@ def f():
# Continue
input += b"c\n"

output = run_pty(script, input)
output = self._run_pty(script, input)

self.assertIn(b'I love Python', output)
self.assertIn('I love Python', output)

@unittest.skipIf(sys.platform.startswith('freebsd'),
'\\x08 is not interpreted as backspace on FreeBSD')
Expand All @@ -5116,9 +5131,9 @@ def test_multiline_auto_indent(self):
input += b"f(-21-21)\n"
input += b"c\n"

output = run_pty(script, input)
output = self._run_pty(script, input)

self.assertIn(b'42', output)
self.assertIn('42', output)

def test_multiline_completion(self):
script = textwrap.dedent("""
Expand All @@ -5134,9 +5149,9 @@ def test_multiline_completion(self):
input += b"fun\t()\n"
input += b"c\n"

output = run_pty(script, input)
output = self._run_pty(script, input)

self.assertIn(b'42', output)
self.assertIn('42', output)

@unittest.skipIf(sys.platform.startswith('freebsd'),
'\\x08 is not interpreted as backspace on FreeBSD')
Expand All @@ -5162,10 +5177,10 @@ def func():
c
""").encode()

output = run_pty(script, input)
output = self._run_pty(script, input)

self.assertIn(b'5', output)
self.assertNotIn(b'Error', output)
self.assertIn('5', output)
self.assertNotIn('Error', output)

def test_interact_completion(self):
script = textwrap.dedent("""
Expand All @@ -5189,11 +5204,45 @@ def test_interact_completion(self):
# continue
input += b"c\n"

output = run_pty(script, input)
output = self._run_pty(script, input)

self.assertIn("'disp' is not defined", output)
self.assertIn('special', output)
self.assertIn('84', output)


@unittest.skipIf(not pdb._pyrepl_available(), "pyrepl is not available")
class PdbTestReadlinePyREPL(PdbTestReadline):
def _run_pty(self, script, input):
# Override the env to make sure pyrepl is used in this test class
return super()._run_pty(script, input, env={**os.environ})

def test_pyrepl_used(self):
script = textwrap.dedent("""
import pdb
db = pdb.Pdb()
print(db.pyrepl_input)
""")
input = b""
output = self._run_pty(script, input)
self.assertIn('PdbPyReplInput', output)

def test_pyrepl_multiline_change(self):
script = textwrap.dedent("""
import pdb; pdb.Pdb().set_trace()
""")

input = b"def f():\n"
# Auto-indent should work here
input += b"return x"
# The following command tries to add the argument x in f()
# up, left, left (in the parenthesis now), "x", down, down (at the end)
input += b"\x1bOA\x1bOD\x1bODx\x1bOB\x1bOB\n\n"
input += b"f(40 + 2)\n"
input += b"c\n"

self.assertIn(b"'disp' is not defined", output)
self.assertIn(b'special', output)
self.assertIn(b'84', output)
output = self._run_pty(script, input)
self.assertIn('42', output)


def load_tests(loader, tests, pattern):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Use ``PyREPL`` as the default input console for :mod:`pdb`
Loading