From c23190e8b44ef1400dbe1dd054f006bbc8f2d007 Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Tue, 24 Feb 2026 21:12:17 -0800 Subject: [PATCH 1/5] Use pyrepl for pdb --- Lib/pdb.py | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index b5d8f827827415..f56724f2c874d5 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -91,6 +91,7 @@ import tempfile import textwrap import tokenize +import functools import itertools import traceback import linecache @@ -348,6 +349,37 @@ def get_default_backend(): return _default_backend +class PdbPyReplInput: + def __init__(self, pdb_instance, prompt): + from _pyrepl.readline import _setup + self.pdb_instance = pdb_instance + self.prompt = prompt + self.console = code.InteractiveConsole() + _setup({}) + + def readline(self): + from _pyrepl.simple_interact import _more_lines + from _pyrepl.readline import get_completer, multiline_input, set_completer + + 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 = get_completer() + set_completer(self.pdb_instance.complete) + return multiline_input(more_lines, self.prompt, '... ') + '\n' + except EOFError: + return 'EOF' + finally: + set_completer(pyrepl_completer) + + class Pdb(bdb.Bdb, cmd.Cmd): _previous_sigint_handler = None @@ -382,6 +414,10 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None, except ImportError: pass + if self.use_rawinput and stdin is None: + self.pyrepl_input = PdbPyReplInput(self, self.prompt) + else: + self.pyrepl_input = None self.allow_kbdint = False self.nosigint = nosigint # Consider these characters as part of the command so when the users type @@ -620,6 +656,31 @@ 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: @@ -627,7 +688,8 @@ def _cmdloop(self): # 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: From 14ea66d61e4fa08b37d12045e053a967641f2ce8 Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Sat, 28 Feb 2026 17:46:23 -0800 Subject: [PATCH 2/5] Draft is done! --- Lib/pdb.py | 96 +++++++++++++++++++++++++++++++++------- Lib/test/test_pdb.py | 103 +++++++++++++++++++++++++++++++------------ 2 files changed, 156 insertions(+), 43 deletions(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index f56724f2c874d5..e1e59d13763260 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -349,17 +349,34 @@ 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, prompt): - from _pyrepl.readline import _setup + def __init__(self, pdb_instance, stdin, stdout, prompt): + import _pyrepl.readline + self.pdb_instance = pdb_instance self.prompt = prompt self.console = code.InteractiveConsole() - _setup({}) + 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 - from _pyrepl.readline import get_completer, multiline_input, set_completer def more_lines(text): cmd, _, line = self.pdb_instance.parseline(text) @@ -371,13 +388,48 @@ def more_lines(text): return _more_lines(self.console, text) try: - pyrepl_completer = get_completer() - set_completer(self.pdb_instance.complete) - return multiline_input(more_lines, self.prompt, '... ') + '\n' + 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: - set_completer(pyrepl_completer) + 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): @@ -414,10 +466,12 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None, except ImportError: pass - if self.use_rawinput and stdin is None: - self.pyrepl_input = PdbPyReplInput(self, self.prompt) - else: - self.pyrepl_input = None + 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 @@ -2422,10 +2476,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]] diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index 0e23cd6604379c..c5171f3388c965 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -6,6 +6,7 @@ import io import os import pdb +import re import sys import types import codecs @@ -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() @@ -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(""" @@ -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(""" @@ -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(""" @@ -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' Date: Sat, 28 Feb 2026 17:55:59 -0800 Subject: [PATCH 3/5] Remove unused import --- Lib/pdb.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index e1e59d13763260..0c3f4157052474 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -91,7 +91,6 @@ import tempfile import textwrap import tokenize -import functools import itertools import traceback import linecache From ce2fc9ad7e2d78d89f94d9d82964dbed096edb55 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 01:58:17 +0000 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2026-03-01-01-58-10.gh-issue-145378.oy6rb9.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2026-03-01-01-58-10.gh-issue-145378.oy6rb9.rst diff --git a/Misc/NEWS.d/next/Library/2026-03-01-01-58-10.gh-issue-145378.oy6rb9.rst b/Misc/NEWS.d/next/Library/2026-03-01-01-58-10.gh-issue-145378.oy6rb9.rst new file mode 100644 index 00000000000000..b6a6273d882d79 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-01-01-58-10.gh-issue-145378.oy6rb9.rst @@ -0,0 +1 @@ +Use ``PyREPL`` as the default input console for :mod:`pdb` From 4cc58015b0c47621a85deee2a8ceb237c45f3718 Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Sat, 28 Feb 2026 18:03:21 -0800 Subject: [PATCH 5/5] Add what's new entry --- Doc/whatsnew/3.15.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 816d45e0756824..3e30a10047b07f 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -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 ------