Skip to content

gh-113081: Highlight source code in pdb #133355

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Doc/library/pdb.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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()
Expand All @@ -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).
Expand Down
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
------
Expand Down
31 changes: 24 additions & 7 deletions Lib/pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
import traceback
import linecache
import _colorize
import _pyrepl.utils

from contextlib import closing
from contextlib import contextmanager
Expand Down Expand Up @@ -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")
Expand All @@ -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
Expand Down Expand Up @@ -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'):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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():
Expand Down Expand Up @@ -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:
Expand Down
37 changes: 37 additions & 0 deletions Lib/test/test_pdb.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Highlight syntax on source code in :mod:`pdb`.
Loading