https://github.com/python/cpython/commit/1ec56076642091c446ec95afcf166bb4f185f0b7
commit: 1ec56076642091c446ec95afcf166bb4f185f0b7
branch: main
author: Ɓukasz Langa <[email protected]>
committer: ambv <[email protected]>
date: 2026-07-27T13:45:40-07:00
summary:

gh-154467: Fix empty (Pdb) prompt when attaching to a process with pdb -p 
(#154469)

_PdbServer inherits _cmdloop, which wraps cmdloop() in
_maybe_use_pyrepl_as_stdin(). That context manager blanks self.prompt to ''
so that a local pyrepl draws the prompt itself. The remote server, however,
never reads from a local pyrepl -- it transmits self.prompt to the client
over the socket -- so the blanking made it send an empty prompt whenever the
target process had a pyrepl-capable terminal (pyrepl_input is set).

Override _maybe_use_pyrepl_as_stdin() in _PdbServer to a no-op, keeping the
real prompt. Add an integration test that attaches to a target running under
a pty, so PyREPL is genuinely enabled inside it, and asserts the transmitted
prompt is "(Pdb) ".

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-07-22-12-59-26.gh-issue-154467.AA9w1D.rst
M Lib/pdb.py
M Lib/test/test_remote_pdb.py

diff --git a/Lib/pdb.py b/Lib/pdb.py
index 01451f0229cacb..458eb835265236 100644
--- a/Lib/pdb.py
+++ b/Lib/pdb.py
@@ -3158,6 +3158,15 @@ def postloop(self):
         if self.quitting:
             self.detach()
 
+    @contextmanager
+    def _maybe_use_pyrepl_as_stdin(self):
+        # The server reads every command from the client over the socket, never
+        # from a local pyrepl. The base implementation swaps in pyrepl as stdin
+        # and blanks `self.prompt` to '' (pyrepl would draw the prompt itself),
+        # which here would transmit an empty prompt to the client whenever the
+        # target process happens to be pyrepl-capable. Keep the real prompt.
+        yield
+
     def detach(self):
         # Detach the debugger and close the socket without raising BdbQuit
         self.quitting = False
diff --git a/Lib/test/test_remote_pdb.py b/Lib/test/test_remote_pdb.py
index d26d63faa61ddb..5b23a194098b01 100644
--- a/Lib/test/test_remote_pdb.py
+++ b/Lib/test/test_remote_pdb.py
@@ -8,6 +8,7 @@
 import subprocess
 import sys
 import textwrap
+import threading
 import unittest
 import unittest.mock
 from contextlib import closing, contextmanager, redirect_stdout, 
redirect_stderr, ExitStack
@@ -18,6 +19,11 @@
 import pdb
 from pdb import _PdbServer, _PdbClient
 
+try:
+    import pty
+except ImportError:
+    pty = None
+
 
 if not sys.is_remote_debug_enabled():
     raise unittest.SkipTest('remote debugging is disabled')
@@ -1090,6 +1096,45 @@ def _connect_and_get_client_file(self):
 
         return process, client_file
 
+    def _connect_and_get_client_file_via_pty(self):
+        """Like _connect_and_get_client_file, but run the target under a pty.
+
+        With a real terminal on stdin/stdout, PyREPL is available *inside the
+        target process*, which is the condition that exercises pdb's PyREPL
+        input handling in the remote server.
+        """
+        controller, worker = pty.openpty()
+        self.addCleanup(os.close, controller)
+        env = dict(os.environ, TERM="xterm-256color")
+        env.pop("PYTHON_BASIC_REPL", None)  # don't opt out of PyREPL
+        process = subprocess.Popen(
+            [sys.executable, self.script_path],
+            stdin=worker, stdout=worker, stderr=worker, env=env, 
close_fds=True,
+        )
+        os.close(worker)  # only the child keeps the worker end open
+
+        # Continuously drain the terminal so the child never blocks on a write.
+        drainer = threading.Thread(
+            target=self._drain_until_eof, args=(controller,), daemon=True
+        )
+        drainer.start()
+        self.addCleanup(drainer.join, SHORT_TIMEOUT)
+
+        client_sock, _ = self.server_sock.accept()
+        client_file = client_sock.makefile('rwb')
+        self.addCleanup(client_file.close)
+        self.addCleanup(client_sock.close)
+
+        return process, client_file
+
+    @staticmethod
+    def _drain_until_eof(fd):
+        try:
+            while os.read(fd, 1024):
+                pass
+        except OSError:
+            pass  # controller closed, or the pty went away with the child
+
     def _read_until_prompt(self, client_file):
         """Helper to read messages until a prompt is received."""
         messages = []
@@ -1292,6 +1337,32 @@ def test_handle_eof(self):
             self.assertEqual(process.returncode, 0)
             self.assertEqual(stderr, "")
 
+    @unittest.skipUnless(pty, "requires pty")
+    def test_prompt_with_interactive_terminal(self):
+        """The server must send "(Pdb) " even when the target owns a terminal.
+
+        The remote server transmits its prompt string to the client, which
+        displays it. When the target process has an interactive terminal,
+        PyREPL is enabled inside it; the base pdb machinery then blanks
+        ``self.prompt`` (a local PyREPL would draw the prompt itself). The
+        remote server has no local PyREPL -- it reads input from the socket --
+        so it must keep the real prompt rather than transmit an empty one.
+        Regression test for gh-154467, where attaching with ``pdb -p`` to a
+        process running at an interactive prompt showed a blank prompt.
+        """
+        self._create_script()
+        process, client_file = self._connect_and_get_client_file_via_pty()
+
+        with kill_on_error(process):
+            messages = self._read_until_prompt(client_file)
+            # The message that ended the read is the prompt request.
+            self.assertEqual(messages[-1], {"prompt": "(Pdb) ", "state": 
"pdb"})
+
+            # Let the target run to completion so nothing is left attached.
+            self._send_command(client_file, "c")
+            process.wait(timeout=SHORT_TIMEOUT)
+            self.assertEqual(process.returncode, 0)
+
     def test_protocol_version(self):
         """Test that incompatible protocol versions are properly detected."""
         # Create a script using an incompatible protocol version
diff --git 
a/Misc/NEWS.d/next/Library/2026-07-22-12-59-26.gh-issue-154467.AA9w1D.rst 
b/Misc/NEWS.d/next/Library/2026-07-22-12-59-26.gh-issue-154467.AA9w1D.rst
new file mode 100644
index 00000000000000..9d7ac18a5e9db1
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-07-22-12-59-26.gh-issue-154467.AA9w1D.rst
@@ -0,0 +1,3 @@
+Fixed :mod:`pdb` remote attaching (``python -m pdb -p PID``) sending an
+empty prompt to the client instead of ``(Pdb)`` when the target process has
+an interactive terminal.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to