diff --git a/Lib/pdb.py b/Lib/pdb.py index c3440a5d11a588..1ef877cce4dd96 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -2440,9 +2440,13 @@ def _print_lines(self, lines, start, breaks=(), frame=None): s += '->' elif lineno == exc_lineno: s += '>>' + # Strip the trailing newline before colorizing: the colorizer + # renders control characters (like '\n') in caret notation, so a + # later rstrip() could not remove the resulting '^J'. + line = line.rstrip() if self.colorize: line = self._colorize_code(line) - self.message(s + '\t' + line.rstrip()) + self.message(s + '\t' + line) def do_whatis(self, arg): """whatis expression diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index e7b66031a86a6c..0aa4ceb71c866f 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -4996,6 +4996,16 @@ def test_code_display(self): p.set_trace(commands=['ll', 'c']) self.assertNotIn("\x1b", output.getvalue()) + def test_list_does_not_colorize_trailing_newlines(self): + # Keep the marker split so it is not present in the listed source. + caret_newline = "^" + "J" + output = io.StringIO() + p = pdb.Pdb(stdout=output, colorize=True) + p.set_trace(commands=['list', 'continue']) + result = output.getvalue() + self.assertIn("\x1b", result) + self.assertNotIn(caret_newline, result) + def test_stack_entry(self): output = io.StringIO() p = pdb.Pdb(stdout=output, colorize=True) diff --git a/Lib/test/test_remote_pdb.py b/Lib/test/test_remote_pdb.py index 5b23a194098b01..e5387526090536 100644 --- a/Lib/test/test_remote_pdb.py +++ b/Lib/test/test_remote_pdb.py @@ -1337,6 +1337,55 @@ def test_handle_eof(self): self.assertEqual(process.returncode, 0) self.assertEqual(stderr, "") + def test_colorized_list_has_no_caret_encoded_newlines(self): + """A colorized ``list`` must not append "^J" to each source line. + + The remote server colorizes the source it sends to the client. The + colorizer renders control characters in caret notation, so a source + line's trailing newline has to be stripped *before* it is colorized; + otherwise every listed line ends with a spurious "^J". ``where`` was + unaffected because it strips the line before colorizing. See + gh-154470. + """ + # colorize=True is what attaching from a color-capable terminal passes + # to the server, and it is what makes the server colorize ``list``. + script = textwrap.dedent(f""" + import pdb, sys + def helper(): + x = 42 + return x + def connect(): + frame = sys._getframe() + pdb._connect( + host='127.0.0.1', + port={self.port}, + frame=frame, + commands="", + version=pdb._PdbServer.protocol_version(), + signal_raising_thread=False, + colorize=True, + ) + return helper() + connect() + """) + self._create_script(script=script) + process, client_file = self._connect_and_get_client_file() + + with kill_on_error(process): + self._read_until_prompt(client_file) + self._send_command(client_file, "l 1, 15") + messages = self._read_until_prompt(client_file) + source = "".join(m["message"] for m in messages if "message" in m) + + # Sanity: we really did receive colorized source ... + self.assertIn("helper", source) + self.assertIn("\x1b[", source) # ANSI color escapes are present + # ... and no trailing newline leaked through as caret notation. + self.assertNotIn("^J", source) + self._send_command(client_file, "c") + process.wait(timeout=SHORT_TIMEOUT) + self.assertEqual(process.returncode, 0) + @unittest.skipUnless(pty, "requires pty") def test_prompt_with_interactive_terminal(self): """The server must send "(Pdb) " even when the target owns a terminal. diff --git a/Misc/NEWS.d/next/Library/2026-07-22-13-39-58.gh-issue-154470.zGcz6K.rst b/Misc/NEWS.d/next/Library/2026-07-22-13-39-58.gh-issue-154470.zGcz6K.rst new file mode 100644 index 00000000000000..e3d8e3c27cf638 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-22-13-39-58.gh-issue-154470.zGcz6K.rst @@ -0,0 +1,4 @@ +Fixed a spurious ``^J`` printed at the end of every source line by the +``list`` command of :mod:`pdb` when the output is colorized (for example +when attaching to a running process). The source line is now stripped before +it is colorized, like the ``where`` command already did.