https://github.com/python/cpython/commit/5f6fc462db73ed18eaa9520cbc275a3c5bbad746
commit: 5f6fc462db73ed18eaa9520cbc275a3c5bbad746
branch: 3.15
author: Miss Islington (bot) <[email protected]>
committer: hugovk <[email protected]>
date: 2026-09-15T09:45:59+03:00
summary:

[3.15] gh-154470: Fix spurious ^J in pdb's colorized list command (GH-154473) 
(#157536)

gh-154470: Fix spurious ^J in pdb's colorized list command (GH-154473)
(cherry picked from commit c68ce16cee99df38405da127eefe1eeb40d37e99)

Co-authored-by: Ɓukasz Langa <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-07-22-13-39-58.gh-issue-154470.zGcz6K.rst
M Lib/pdb.py
M Lib/test/test_pdb.py
M Lib/test/test_remote_pdb.py

diff --git a/Lib/pdb.py b/Lib/pdb.py
index 458eb8352652366..e0c9b4d2c241a74 100644
--- a/Lib/pdb.py
+++ b/Lib/pdb.py
@@ -2435,9 +2435,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 771abb8f871bab9..cfddd96a24fd7d9 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 5b23a194098b01c..e5387526090536d 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 000000000000000..e3d8e3c27cf6389
--- /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.

_______________________________________________
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