diff --git a/Doc/howto/gdb_helpers.rst b/Doc/howto/gdb_helpers.rst index 98ce813ca4ab02..866f73505bd15a 100644 --- a/Doc/howto/gdb_helpers.rst +++ b/Doc/howto/gdb_helpers.rst @@ -352,9 +352,21 @@ Again, the implementation details can be revealed with a cast to builtin 'len' = (gdb) py-print scarlet_pimpernel 'scarlet_pimpernel' not found + (gdb) py-print nested_dict + local 'nested_dict' = {'a': {'b': {'c': {'d': 1, 'e': 2, 'f': 3}, 'g': {'h': 4, 'i': 5}}, 'j': 6, 'k': {'l': 7, 'm': {'n': 8, 'o': 9}}}, 'p': {'q': {'r': 10, 's': {'t': 11, 'u': {'v': 12, 'w': 13, 'x': 14}}}}, 'y': {'z': 15}} + (gdb) set py-verbose-print on + (gdb) py-print nested_dict + local 'nested_dict' = \ + {'a': {'b': {'c': {'d': 1, 'e': 2, 'f': 3}, 'g': {'h': 4, 'i': 5}}, + 'j': 6, + 'k': {'l': 7, 'm': {'n': 8, 'o': 9}}}, + 'p': {'q': {'r': 10, 's': {'t': 11, 'u': {'v': 12, 'w': 13, 'x': 14}}}}, + 'y': {'z': 15}} If the current C frame corresponds to multiple Python frames, ``py-print`` only considers the first one. + Setting the parameter ``py-verbose-print`` to ``on`` enables Python object pretty printing + and allow Python objects to be printed completely instead of being truncated at some limit. ``py-locals`` ------------- diff --git a/Lib/test/test_gdb/test_misc.py b/Lib/test/test_gdb/test_misc.py index 1047f4867c1d03..b6f3a88b9fd4e5 100644 --- a/Lib/test/test_gdb/test_misc.py +++ b/Lib/test/test_gdb/test_misc.py @@ -71,6 +71,12 @@ def test_two_abs_args(self): from _testcapi import pyobject_vectorcall def foo(a, b, c): + nested_dict = \ + {'a': {'b': {'c': {'d': 1, 'e': 2, 'f': 3}, 'g': {'h': 4, 'i': 5}}, + 'j': 6, + 'k': {'l': 7, 'm': {'n': 8, 'o': 9}}}, + 'p': {'q': {'r': 10, 's': {'t': 11, 'u': {'v': 12, 'w': 13, 'x': 14}}}}, + 'y': {'z': 15}} bar(a, b, c) def bar(a, b, c): @@ -94,7 +100,7 @@ def test_pyup_command(self): cmds_after_breakpoint=['py-up', 'py-up']) self.assertMultilineMatches(bt, r'''^.* -#[0-9]+ Frame 0x-?[0-9a-f]+, for file , line 12, in baz \(args=\(1, 2, 3\)\) +#[0-9]+ Frame 0x-?[0-9a-f]+, for file , line 17, in baz \(args=\(1, 2, 3\)\) #[0-9]+ $''') @@ -123,9 +129,9 @@ def test_up_then_down(self): cmds_after_breakpoint=['py-up', 'py-up', 'py-down']) self.assertMultilineMatches(bt, r'''^.* -#[0-9]+ Frame 0x-?[0-9a-f]+, for file , line 12, in baz \(args=\(1, 2, 3\)\) +#[0-9]+ Frame 0x-?[0-9a-f]+, for file , line 17, in baz \(args=\(1, 2, 3\)\) #[0-9]+ -#[0-9]+ Frame 0x-?[0-9a-f]+, for file , line 12, in baz \(args=\(1, 2, 3\)\) +#[0-9]+ Frame 0x-?[0-9a-f]+, for file , line 17, in baz \(args=\(1, 2, 3\)\) $''') class PyPrintTests(DebuggerTests): @@ -163,6 +169,22 @@ def test_printing_builtin(self): self.assertMultilineMatches(bt, r".*\nbuiltin 'len' = \n.*") + @unittest.skipIf(python_is_optimized(), + "Python was compiled with optimizations") + def test_pretty_printing(self): + 'Verify that the "py-print" with pretty printing command works' + bt = self.get_stack_trace(source=SAMPLE_WITH_C_CALL, + cmds_after_breakpoint=['py-up', 'py-up', 'set py-verbose-print on', 'py-print nested_dict']) + self.assertMultilineMatches(bt, + r'''.*^ +local 'nested_dict' = \\ + {'a': {'b': {'c': {'d': 1, 'e': 2, 'f': 3}, 'g': {'h': 4, 'i': 5}}, + 'j': 6, + 'k': {'l': 7, 'm': {'n': 8, 'o': 9}}}, + 'p': {'q': {'r': 10, 's': {'t': 11, 'u': {'v': 12, 'w': 13, 'x': 14}}}}, + 'y': {'z': 15}}.*$ +''') + class PyLocalsTests(DebuggerTests): @unittest.skipIf(python_is_optimized(), "Python was compiled with optimizations") diff --git a/Lib/test/test_gdb/test_pretty_print.py b/Lib/test/test_gdb/test_pretty_print.py index dfc77d65ab16a4..d5692cdc12a51e 100644 --- a/Lib/test/test_gdb/test_pretty_print.py +++ b/Lib/test/test_gdb/test_pretty_print.py @@ -14,7 +14,8 @@ def setUpModule(): class PrettyPrintTests(DebuggerTests): def get_gdb_repr(self, source, cmds_after_breakpoint=None, - import_site=False): + import_site=False, + verbose_output=False): # Given an input python source representation of data, # run "python -c'id(DATA)'" under gdb with a breakpoint on # builtin_id and scrape out gdb's representation of the "op" @@ -31,7 +32,8 @@ def get_gdb_repr(self, source, cmds_after_breakpoint = cmds_after_breakpoint or ["backtrace 1"] gdb_output = self.get_stack_trace(source, breakpoint=BREAKPOINT_FN, cmds_after_breakpoint=cmds_after_breakpoint, - import_site=import_site) + import_site=import_site, + verbose_output=verbose_output) # gdb can insert additional '\n' and space characters in various places # in its output, depending on the width of the terminal it's connected # to (using its "wrap_here" function) @@ -52,10 +54,10 @@ def test_getting_backtrace(self): gdb_output = self.get_stack_trace('id(42)') self.assertTrue(BREAKPOINT_FN in gdb_output) - def assertGdbRepr(self, val, exp_repr=None): + def assertGdbRepr(self, val, exp_repr=None, verbose_output=False): # Ensure that gdb's rendering of the value in a debugged process # matches repr(value) in this process: - gdb_repr, gdb_output = self.get_gdb_repr('id(' + ascii(val) + ')') + gdb_repr, gdb_output = self.get_gdb_repr('id(' + ascii(val) + ')', verbose_output=verbose_output) if not exp_repr: exp_repr = repr(val) self.assertEqual(gdb_repr, exp_repr, @@ -414,6 +416,11 @@ def test_truncation(self): self.assertEqual(len(gdb_repr), 1024 + len('...(truncated)')) + def test_verbose_not_truncated(self): + 'Verify that very long output is not truncated when verbose output is requested' + long_list = [42] * 500 + self.assertGdbRepr(long_list, verbose_output=True) + def test_builtin_method(self): gdb_repr, gdb_output = self.get_gdb_repr('import sys; id(sys.stdout.readlines)') self.assertTrue(re.match(r'', diff --git a/Lib/test/test_gdb/util.py b/Lib/test/test_gdb/util.py index 8097fd52ababe6..d69e5617f6ecbb 100644 --- a/Lib/test/test_gdb/util.py +++ b/Lib/test/test_gdb/util.py @@ -160,7 +160,8 @@ def get_stack_trace(self, source=None, script=None, breakpoint=BREAKPOINT_FN, cmds_after_breakpoint=None, import_site=False, - ignore_stderr=False): + ignore_stderr=False, + verbose_output=False): ''' Run 'python -c SOURCE' under gdb with a breakpoint. @@ -210,6 +211,9 @@ def get_stack_trace(self, source=None, script=None, if GDB_VERSION >= (7, 4): commands += ['set print entry-values no'] + if verbose_output: + commands += ['set py-verbose-print on'] + if cmds_after_breakpoint: if CET_PROTECTION: # bpo-32962: When Python is compiled with -mcet diff --git a/Misc/NEWS.d/next/Tools-Demos/2025-08-26-20-30-35.gh-issue-129351.No_Kdl.rst b/Misc/NEWS.d/next/Tools-Demos/2025-08-26-20-30-35.gh-issue-129351.No_Kdl.rst new file mode 100644 index 00000000000000..201d7453c968c7 --- /dev/null +++ b/Misc/NEWS.d/next/Tools-Demos/2025-08-26-20-30-35.gh-issue-129351.No_Kdl.rst @@ -0,0 +1 @@ +Allow switching to pretty-printer when using gdb via ``set py-verbose-print on`` diff --git a/Tools/gdb/libpython.py b/Tools/gdb/libpython.py index 27aa6b0cc266d3..981ea22d7736e6 100755 --- a/Tools/gdb/libpython.py +++ b/Tools/gdb/libpython.py @@ -1580,31 +1580,30 @@ def int_from_int(gdbval): return int(gdbval) -def stringify(val): - # TODO: repr() puts everything on one line; pformat can be nicer, but - # can lead to v.long results; this function isolates the choice - if True: - return repr(val) +_verbose_stringify = False + +def stringify(pyop, multiline=False): + if _verbose_stringify: + # Generate full proxy value then stringify it. + # Doing so could be expensive + proxyval = pyop.proxyval(set()) + if multiline: + from pprint import pformat + return pformat(proxyval) + else: + return repr(proxyval) else: - from pprint import pformat - return pformat(val) + return pyop.get_truncated_repr(MAX_OUTPUT_LEN) class PyObjectPtrPrinter: "Prints a (PyObject*)" - def __init__ (self, gdbval): + def __init__(self, gdbval): self.gdbval = gdbval - def to_string (self): - pyop = PyObjectPtr.from_pyobject_ptr(self.gdbval) - if True: - return pyop.get_truncated_repr(MAX_OUTPUT_LEN) - else: - # Generate full proxy value then stringify it. - # Doing so could be expensive - proxyval = pyop.proxyval(set()) - return stringify(proxyval) + def to_string(self): + return stringify(PyObjectPtr.from_pyobject_ptr(self.gdbval)) def pretty_printer_lookup(gdbval): type = gdbval.type.strip_typedefs().unqualified() @@ -1624,8 +1623,9 @@ def pretty_printer_lookup(gdbval): (gdb) python import sys -sys.path.append('/home/david/coding/python-gdb') +sys.path.append('/home/david/coding/cpython/Tools/gdb') import libpython +from importlib import reload end then reloading it after each edit like this: @@ -2087,6 +2087,23 @@ def invoke(self, args, from_tty): PyBacktrace() +class PyParameterVerbosePrint(gdb.Parameter): + set_doc ="Enable Python object pretty printing and allow Python objects to be printed completely instead of being truncated at some limit" + + def __init__(self): + gdb.Parameter.__init__ (self, + "py-verbose-print", + gdb.COMMAND_DATA, + gdb.PARAM_BOOLEAN) + self.value = False + + def get_set_string (self): + global _verbose_stringify + _verbose_stringify = self.value + return "" + +PyParameterVerbosePrint() + class PyPrint(gdb.Command): 'Look up the given python variable name, and print it' def __init__(self): @@ -2111,11 +2128,16 @@ def invoke(self, args, from_tty): pyop_var, scope = pyop_frame.get_var_by_name(name) + var_stringifed = stringify(pyop_var, multiline=True) + multiline = "\n" in var_stringifed + if multiline: + # Prefix with a line continuation and indent by single space + var_stringifed = "\\\n %s" % var_stringifed.replace("\n", "\n ") if pyop_var: print('%s %r = %s' % (scope, name, - pyop_var.get_truncated_repr(MAX_OUTPUT_LEN))) + var_stringifed)) else: print('%r not found' % name) @@ -2151,7 +2173,7 @@ def invoke(self, args, from_tty): for pyop_name, pyop_value in pyop_frame.iter_locals(): print('%s = %s' % (pyop_name.proxyval(set()), - pyop_value.get_truncated_repr(MAX_OUTPUT_LEN))) + stringify(pyop_value))) pyop_frame = pyop_frame.previous()