]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-113081: Highlight source code in pdb (#133355)
authorTian Gao <gaogaotiantian@hotmail.com>
Mon, 5 May 2025 07:49:52 +0000 (00:49 -0700)
committerGitHub <noreply@github.com>
Mon, 5 May 2025 07:49:52 +0000 (09:49 +0200)
Doc/library/pdb.rst
Doc/whatsnew/3.14.rst
Lib/pdb.py
Lib/test/test_pdb.py
Misc/NEWS.d/next/Library/2025-05-03-18-48-54.gh-issue-113081.JsLJ1X.rst [new file with mode: 0644]

index 3c8c07074993f2740476bcdf7f7132f0456f8e86..a0304edddf6478949825bc04e873ddfa80585bd8 100644 (file)
@@ -243,7 +243,7 @@ The ``run*`` functions and :func:`set_trace` are aliases for instantiating the
 access further features, you have to do this yourself:
 
 .. class:: Pdb(completekey='tab', stdin=None, stdout=None, skip=None, \
-               nosigint=False, readrc=True, mode=None, backend=None)
+               nosigint=False, readrc=True, mode=None, backend=None, colorize=False)
 
    :class:`Pdb` is the debugger class.
 
@@ -273,6 +273,9 @@ access further features, you have to do this yourself:
    is passed, the default backend will be used. See :func:`set_default_backend`.
    Otherwise the supported backends are ``'settrace'`` and ``'monitoring'``.
 
+   The *colorize* argument, if set to ``True``, will enable colorized output in the
+   debugger, if color is supported. This will highlight source code displayed in pdb.
+
    Example call to enable tracing with *skip*::
 
       import pdb; pdb.Pdb(skip=['django.*']).set_trace()
@@ -295,6 +298,9 @@ access further features, you have to do this yourself:
    .. versionadded:: 3.14
       Added the *backend* argument.
 
+   .. versionadded:: 3.14
+      Added the *colorize* argument.
+
    .. versionchanged:: 3.14
       Inline breakpoints like :func:`breakpoint` or :func:`pdb.set_trace` will
       always stop the program at calling frame, ignoring the *skip* pattern (if any).
index 4b5515900cad41d56e313b78a6fa6876fc3c58b9..330140010160f6960dd282c7c8594697c9bb588f 100644 (file)
@@ -1436,6 +1436,11 @@ pdb
   function.
   (Contributed by Tian Gao in :gh:`132576`.)
 
+* Source code displayed in :mod:`pdb` will be syntax-highlighted. This feature
+  can be controlled using the same methods as PyREPL, in addition to the newly
+  added ``colorize`` argument of :class:`pdb.Pdb`.
+  (Contributed by Tian Gao in :gh:`133355`.)
+
 
 pickle
 ------
index 2aa60c75396085d1786e2357a350170ceb7e4fc4..195bfa557ef1e47be795625d010395bc7e1ddc2a 100644 (file)
@@ -93,6 +93,7 @@ import itertools
 import traceback
 import linecache
 import _colorize
+import _pyrepl.utils
 
 from contextlib import closing
 from contextlib import contextmanager
@@ -339,7 +340,7 @@ class Pdb(bdb.Bdb, cmd.Cmd):
     _last_pdb_instance = None
 
     def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None,
-                 nosigint=False, readrc=True, mode=None, backend=None):
+                 nosigint=False, readrc=True, mode=None, backend=None, colorize=False):
         bdb.Bdb.__init__(self, skip=skip, backend=backend if backend else get_default_backend())
         cmd.Cmd.__init__(self, completekey, stdin, stdout)
         sys.audit("pdb.Pdb")
@@ -352,6 +353,7 @@ class Pdb(bdb.Bdb, cmd.Cmd):
         self._wait_for_mainpyfile = False
         self.tb_lineno = {}
         self.mode = mode
+        self.colorize = _colorize.can_colorize(file=stdout or sys.stdout) and colorize
         # Try to load readline if it exists
         try:
             import readline
@@ -1036,6 +1038,13 @@ class Pdb(bdb.Bdb, cmd.Cmd):
             return True
         return False
 
+    def _colorize_code(self, code):
+        if self.colorize:
+            colors = list(_pyrepl.utils.gen_colors(code))
+            chars, _ = _pyrepl.utils.disp_str(code, colors=colors)
+            code = "".join(chars)
+        return code
+
     # interface abstraction functions
 
     def message(self, msg, end='\n'):
@@ -2166,6 +2175,8 @@ class Pdb(bdb.Bdb, cmd.Cmd):
                 s += '->'
             elif lineno == exc_lineno:
                 s += '>>'
+            if self.colorize:
+                line = self._colorize_code(line)
             self.message(s + '\t' + line.rstrip())
 
     def do_whatis(self, arg):
@@ -2365,8 +2376,14 @@ class Pdb(bdb.Bdb, cmd.Cmd):
             prefix = '> '
         else:
             prefix = '  '
-        self.message(prefix +
-                     self.format_stack_entry(frame_lineno, prompt_prefix))
+        stack_entry = self.format_stack_entry(frame_lineno, prompt_prefix)
+        if self.colorize:
+            lines = stack_entry.split(prompt_prefix, 1)
+            if len(lines) > 1:
+                # We have some code to display
+                lines[1] = self._colorize_code(lines[1])
+                stack_entry = prompt_prefix.join(lines)
+        self.message(prefix + stack_entry)
 
     # Provide help
 
@@ -2604,7 +2621,7 @@ def set_trace(*, header=None, commands=None):
     if Pdb._last_pdb_instance is not None:
         pdb = Pdb._last_pdb_instance
     else:
-        pdb = Pdb(mode='inline', backend='monitoring')
+        pdb = Pdb(mode='inline', backend='monitoring', colorize=True)
     if header is not None:
         pdb.message(header)
     pdb.set_trace(sys._getframe().f_back, commands=commands)
@@ -2619,7 +2636,7 @@ async def set_trace_async(*, header=None, commands=None):
     if Pdb._last_pdb_instance is not None:
         pdb = Pdb._last_pdb_instance
     else:
-        pdb = Pdb(mode='inline', backend='monitoring')
+        pdb = Pdb(mode='inline', backend='monitoring', colorize=True)
     if header is not None:
         pdb.message(header)
     await pdb.set_trace_async(sys._getframe().f_back, commands=commands)
@@ -2633,7 +2650,7 @@ class _PdbServer(Pdb):
         self._sockfile = sockfile
         self._command_name_cache = []
         self._write_failed = False
-        super().__init__(**kwargs)
+        super().__init__(colorize=False, **kwargs)
 
     @staticmethod
     def protocol_version():
@@ -3345,7 +3362,7 @@ def main():
     # modified by the script being debugged. It's a bad idea when it was
     # changed by the user from the command line. There is a "restart" command
     # which allows explicit specification of command line arguments.
-    pdb = Pdb(mode='cli', backend='monitoring')
+    pdb = Pdb(mode='cli', backend='monitoring', colorize=True)
     pdb.rcLines.extend(opts.commands)
     while True:
         try:
index be365a5a3ddeecc7baa3174886fe24b631b80c15..9223a7130d4e0d653502f2f21925244ee4b86464 100644 (file)
@@ -1,7 +1,9 @@
 # A test suite for pdb; not very comprehensive at the moment.
 
+import _colorize
 import doctest
 import gc
+import io
 import os
 import pdb
 import sys
@@ -3446,6 +3448,7 @@ def test_pdb_issue_gh_65052():
     """
 
 
+@support.force_not_colorized_test_class
 @support.requires_subprocess()
 class PdbTestCase(unittest.TestCase):
     def tearDown(self):
@@ -4688,6 +4691,40 @@ class PdbTestInline(unittest.TestCase):
         self.assertIn("42", stdout)
 
 
+@unittest.skipUnless(_colorize.can_colorize(), "Test requires colorize")
+class PdbTestColorize(unittest.TestCase):
+    def setUp(self):
+        self._original_can_colorize = _colorize.can_colorize
+        # Force colorize to be enabled because we are sending data
+        # to a StringIO
+        _colorize.can_colorize = lambda *args, **kwargs: True
+
+    def tearDown(self):
+        _colorize.can_colorize = self._original_can_colorize
+
+    def test_code_display(self):
+        output = io.StringIO()
+        p = pdb.Pdb(stdout=output, colorize=True)
+        p.set_trace(commands=['ll', 'c'])
+        self.assertIn("\x1b", output.getvalue())
+
+        output = io.StringIO()
+        p = pdb.Pdb(stdout=output, colorize=False)
+        p.set_trace(commands=['ll', 'c'])
+        self.assertNotIn("\x1b", output.getvalue())
+
+        output = io.StringIO()
+        p = pdb.Pdb(stdout=output)
+        p.set_trace(commands=['ll', 'c'])
+        self.assertNotIn("\x1b", output.getvalue())
+
+    def test_stack_entry(self):
+        output = io.StringIO()
+        p = pdb.Pdb(stdout=output, colorize=True)
+        p.set_trace(commands=['w', 'c'])
+        self.assertIn("\x1b", output.getvalue())
+
+
 @support.force_not_colorized_test_class
 @support.requires_subprocess()
 class TestREPLSession(unittest.TestCase):
diff --git a/Misc/NEWS.d/next/Library/2025-05-03-18-48-54.gh-issue-113081.JsLJ1X.rst b/Misc/NEWS.d/next/Library/2025-05-03-18-48-54.gh-issue-113081.JsLJ1X.rst
new file mode 100644 (file)
index 0000000..43c2d9d
--- /dev/null
@@ -0,0 +1 @@
+Highlight syntax on source code in :mod:`pdb`.