diff --git a/Doc/library/pdb.rst b/Doc/library/pdb.rst index 3c8c07074993f2..a0304edddf6478 100644 --- a/Doc/library/pdb.rst +++ b/Doc/library/pdb.rst @@ -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). diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 6eb12f1c6f00df..bc67ccd3ae37af 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -1275,6 +1275,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 ------ diff --git a/Lib/pdb.py b/Lib/pdb.py index 343cf4404d7f8c..7c628114f6a70f 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -93,6 +93,7 @@ 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 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None, 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 @@ def handle_command_def(self, line): 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 @@ def _print_lines(self, lines, start, breaks=(), frame=None): 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 @@ def print_stack_entry(self, frame_lineno, prompt_prefix=line_prefix): 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 @@ def __init__(self, sockfile, owns_sockfile=True, **kwargs): self._sockfile = sockfile self._command_name_cache = [] self._write_failed = False - super().__init__(**kwargs) + super().__init__(colorize=False, **kwargs) @staticmethod def protocol_version(): @@ -3338,7 +3355,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: diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index be365a5a3ddeec..9223a7130d4e0d 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -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 @@ def foo(): 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 index 00000000000000..43c2d9dfa39117 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-05-03-18-48-54.gh-issue-113081.JsLJ1X.rst @@ -0,0 +1 @@ +Highlight syntax on source code in :mod:`pdb`.