mirror of
https://github.com/python/cpython.git
synced 2026-05-06 04:37:33 -04:00
GH-145378: Use PyREPL as the default input console for pdb (#145379)
This commit is contained in:
@@ -1046,6 +1046,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
|
||||
------
|
||||
|
||||
|
||||
+162
-7
@@ -318,12 +318,34 @@ class _ZipTarget(_ExecutableTarget):
|
||||
|
||||
|
||||
class _PdbInteractiveConsole(code.InteractiveConsole):
|
||||
def __init__(self, ns, message):
|
||||
def __init__(self, ns=None, message=None):
|
||||
self._message = message
|
||||
super().__init__(locals=ns, local_exit=True)
|
||||
|
||||
def write(self, data):
|
||||
self._message(data, end='')
|
||||
if self._message is not None:
|
||||
self._message(data, end='')
|
||||
else:
|
||||
super().write(data)
|
||||
|
||||
def more_lines(self, text):
|
||||
# Generic Python multi-line completeness heuristic.
|
||||
# Strips pyrepl's trailing auto-indent before compiling.
|
||||
# This should be functionally identical to simple_interact._more_lines
|
||||
src = text.rstrip(" \t")
|
||||
n = len(src)
|
||||
if n > 0 and text[n-1] == '\n':
|
||||
text = src
|
||||
try:
|
||||
code_obj = self.compile(text, "<stdin>", "single")
|
||||
except (OverflowError, SyntaxError, ValueError):
|
||||
lines = text.splitlines(keepends=True)
|
||||
if len(lines) == 1:
|
||||
return False
|
||||
last = lines[-1]
|
||||
return ((last.startswith((" ", "\t")) or last.strip() != "")
|
||||
and not last.endswith("\n"))
|
||||
return code_obj is None
|
||||
|
||||
|
||||
# Interaction prompt line will separate file and call info from code
|
||||
@@ -352,6 +374,96 @@ 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"):
|
||||
CAN_USE_PYREPL = False
|
||||
else:
|
||||
try:
|
||||
from _pyrepl.main import CAN_USE_PYREPL
|
||||
except ModuleNotFoundError:
|
||||
CAN_USE_PYREPL = False
|
||||
return CAN_USE_PYREPL
|
||||
|
||||
|
||||
class PdbPyReplInput:
|
||||
def __init__(self, pdb_instance, stdin, stdout, prompt):
|
||||
import _pyrepl.readline
|
||||
|
||||
self.pdb_instance = pdb_instance
|
||||
self.prompt = prompt
|
||||
self.console = _PdbInteractiveConsole()
|
||||
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):
|
||||
|
||||
def more_lines(text):
|
||||
if text.strip() == "\x1a":
|
||||
# Ctrl + Z raises EOFError to quit pdb
|
||||
# This is similarly handled in simple_interact.py
|
||||
raise EOFError
|
||||
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 self.console.more_lines(text)
|
||||
|
||||
try:
|
||||
pyrepl_completer = self.readline_wrapper.get_completer()
|
||||
self.readline_wrapper.set_completer(self.complete)
|
||||
multiline = (
|
||||
self.readline_wrapper.multiline_input(
|
||||
more_lines,
|
||||
self.prompt,
|
||||
'... ' + ' ' * (len(self.prompt) - 4)
|
||||
) + '\n'
|
||||
)
|
||||
return multiline
|
||||
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
|
||||
|
||||
@@ -386,6 +498,12 @@ class Pdb(bdb.Bdb, cmd.Cmd):
|
||||
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
|
||||
@@ -624,6 +742,31 @@ class Pdb(bdb.Bdb, cmd.Cmd):
|
||||
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:
|
||||
@@ -631,7 +774,8 @@ class Pdb(bdb.Bdb, cmd.Cmd):
|
||||
# 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:
|
||||
@@ -2364,10 +2508,21 @@ class Pdb(bdb.Bdb, cmd.Cmd):
|
||||
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)
|
||||
banner = "*pdb interact start*"
|
||||
exitmsg = "*exit from pdb interact command*"
|
||||
if self.pyrepl_input is not None:
|
||||
from _pyrepl.simple_interact import run_multiline_interactive_console
|
||||
self.message(banner)
|
||||
try:
|
||||
run_multiline_interactive_console(console)
|
||||
except SystemExit:
|
||||
pass
|
||||
self.message(exitmsg)
|
||||
else:
|
||||
with self._enable_rlcompleter(ns):
|
||||
console.interact(banner=banner,
|
||||
exitmsg=exitmsg)
|
||||
|
||||
def do_alias(self, arg):
|
||||
"""alias [name [command]]
|
||||
|
||||
+76
-27
@@ -6,6 +6,7 @@ import gc
|
||||
import io
|
||||
import os
|
||||
import pdb
|
||||
import re
|
||||
import sys
|
||||
import types
|
||||
import codecs
|
||||
@@ -5006,6 +5007,20 @@ class PdbTestReadline(unittest.TestCase):
|
||||
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()
|
||||
@@ -5017,12 +5032,12 @@ class PdbTestReadline(unittest.TestCase):
|
||||
# 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("""
|
||||
@@ -5039,11 +5054,11 @@ class PdbTestReadline(unittest.TestCase):
|
||||
# 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("""
|
||||
@@ -5057,9 +5072,9 @@ class PdbTestReadline(unittest.TestCase):
|
||||
# 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("""
|
||||
@@ -5075,10 +5090,10 @@ class PdbTestReadline(unittest.TestCase):
|
||||
# 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("""
|
||||
@@ -5094,9 +5109,9 @@ class PdbTestReadline(unittest.TestCase):
|
||||
# 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')
|
||||
@@ -5116,9 +5131,9 @@ class PdbTestReadline(unittest.TestCase):
|
||||
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("""
|
||||
@@ -5134,9 +5149,9 @@ class PdbTestReadline(unittest.TestCase):
|
||||
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')
|
||||
@@ -5162,10 +5177,10 @@ class PdbTestReadline(unittest.TestCase):
|
||||
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("""
|
||||
@@ -5189,11 +5204,45 @@ class PdbTestReadline(unittest.TestCase):
|
||||
# continue
|
||||
input += b"c\n"
|
||||
|
||||
output = run_pty(script, input)
|
||||
output = self._run_pty(script, input)
|
||||
|
||||
self.assertIn(b"'disp' is not defined", output)
|
||||
self.assertIn(b'special', output)
|
||||
self.assertIn(b'84', output)
|
||||
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"
|
||||
|
||||
output = self._run_pty(script, input)
|
||||
self.assertIn('42', output)
|
||||
|
||||
|
||||
def load_tests(loader, tests, pattern):
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Use ``PyREPL`` as the default input console for :mod:`pdb`
|
||||
Reference in New Issue
Block a user