diff --git a/Doc/library/pdb.rst b/Doc/library/pdb.rst index d80c5eebbf27a7..00caa97d212a2c 100644 --- a/Doc/library/pdb.rst +++ b/Doc/library/pdb.rst @@ -379,6 +379,14 @@ can be overridden by the local file. Execute the current line, stop at the first possible occasion (either in a function that is called or on the next line in the current function). +.. pdbcommand:: si | stepi + + Execute the current bytecode instruction, stop at the first possible occasion + (either in a function that is called or on the next instruction in the + current function). + + .. versionadded:: 3.12 + .. pdbcommand:: n(ext) Continue execution until the next line in the current function is reached or @@ -387,6 +395,13 @@ can be overridden by the local file. executes called functions at (nearly) full speed, only stopping at the next line in the current function.) +.. pdbcommand:: ni | nexti + + Continue execution until the next bytecode instruction in the current function + is reached or it returns. + + .. versionadded:: 3.12 + .. pdbcommand:: unt(il) [lineno] Without argument, continue execution until the line with a number greater @@ -433,6 +448,13 @@ can be overridden by the local file. .. versionadded:: 3.2 The ``>>`` marker. +.. pdbcommand:: li | listi [first[, last]] + + Similar to :pdbcmd:`list`, but also display bytecode instructions with + the source code + + .. versionadded:: 3.12 + .. pdbcommand:: ll | longlist List all source code for the current function or frame. Interesting lines @@ -440,6 +462,13 @@ can be overridden by the local file. .. versionadded:: 3.2 +.. pdbcommand:: lli | longlisti + + Similar to :pdbcmd:`ll`, but also display bytecode instructions with + the source code + + .. versionadded:: 3.12 + .. pdbcommand:: a(rgs) Print the argument list of the current function. diff --git a/Lib/bdb.py b/Lib/bdb.py index 7f9b09514ffd00..429771ded4c281 100644 --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -32,6 +32,9 @@ def __init__(self, skip=None): self.skip = set(skip) if skip else None self.breaks = {} self.fncache = {} + self._curframe = None + self.lasti = -1 + self.trace_opcodes = False self.frame_returning = None self._load_breaks() @@ -75,6 +78,7 @@ def trace_dispatch(self, frame, event, arg): is entered. return: A function or other code block is about to return. exception: An exception has occurred. + opcode: An opcode is going to be executed. c_call: A C function is about to be called. c_return: A C function has returned. c_exception: A C function has raised an exception. @@ -84,6 +88,8 @@ def trace_dispatch(self, frame, event, arg): The arg parameter depends on the previous event. """ + self._curframe = frame + if self.quitting: return # None if event == 'line': @@ -94,6 +100,8 @@ def trace_dispatch(self, frame, event, arg): return self.dispatch_return(frame, arg) if event == 'exception': return self.dispatch_exception(frame, arg) + if event == 'opcode': + return self.dispatch_opcode(frame) if event == 'c_call': return self.trace_dispatch if event == 'c_exception': @@ -115,6 +123,18 @@ def dispatch_line(self, frame): if self.quitting: raise BdbQuit return self.trace_dispatch + def dispatch_opcode(self, frame): + """Invoke user function and return trace function for opcode event. + + If the debugger stops on the current opcode, invoke + self.user_opcode(). Raise BdbQuit if self.quitting is set. + Return self.trace_dispatch to continue tracing in this scope. + """ + if self.stop_here(frame) or self.break_here(frame): + self.user_opcode(frame) + if self.quitting: raise BdbQuit + return self.trace_dispatch + def dispatch_call(self, frame, arg): """Invoke user function and return trace function for call event. @@ -122,6 +142,11 @@ def dispatch_call(self, frame, arg): self.user_call(). Raise BdbQuit if self.quitting is set. Return self.trace_dispatch to continue tracing in this scope. """ + if self.trace_opcodes: + frame.f_trace_opcodes = True + else: + frame.f_trace_opcodes = False + # XXX 'arg' is no longer used if self.botframe is None: # First call of dispatch since reset() @@ -209,9 +234,15 @@ def stop_here(self, frame): if frame is self.stopframe: if self.stoplineno == -1: return False - return frame.f_lineno >= self.stoplineno + if self.trace_opcodes: + return self.lasti != frame.f_lasti + else: + return frame.f_lineno >= self.stoplineno if not self.stopframe: - return True + if self.trace_opcodes: + return self.lasti != frame.f_lasti + else: + return True return False def break_here(self, frame): @@ -272,7 +303,21 @@ def user_exception(self, frame, exc_info): """Called when we stop on an exception.""" pass - def _set_stopinfo(self, stopframe, returnframe, stoplineno=0): + def user_opcode(self, frame): + """Called when we are about to execute an opcode.""" + pass + + def _set_trace_opcodes(self, trace_opcodes): + if trace_opcodes != self.trace_opcodes: + self.trace_opcodes = trace_opcodes + frame = self._curframe + while frame is not None: + frame.f_trace_opcodes = trace_opcodes + if frame is self.botframe: + break + frame = frame.f_back + + def _set_stopinfo(self, stopframe, returnframe, stoplineno=0, lasti=None): """Set the attributes for stopping. If stoplineno is greater than or equal to 0, then stop at line @@ -285,6 +330,22 @@ def _set_stopinfo(self, stopframe, returnframe, stoplineno=0): # stoplineno >= 0 means: stop at line >= the stoplineno # stoplineno -1 means: don't stop at all self.stoplineno = stoplineno + if lasti: + # We are stopping at opcode level + self._set_trace_opcodes(True) + self.lasti = lasti + else: + self._set_trace_opcodes(False) + + def _set_caller_tracefunc(self): + # Issue #13183: pdb skips frames after hitting a breakpoint and running + # step commands. + # Restore the trace function in the caller (that may not have been set + # for performance reasons) when returning from the current frame. + if self.frame_returning: + caller_frame = self.frame_returning.f_back + if caller_frame and not caller_frame.f_trace: + caller_frame.f_trace = self.trace_dispatch # Derived classes and clients can call the following methods # to affect the stepping state. @@ -299,20 +360,22 @@ def set_until(self, frame, lineno=None): def set_step(self): """Stop after one line of code.""" - # Issue #13183: pdb skips frames after hitting a breakpoint and running - # step commands. - # Restore the trace function in the caller (that may not have been set - # for performance reasons) when returning from the current frame. - if self.frame_returning: - caller_frame = self.frame_returning.f_back - if caller_frame and not caller_frame.f_trace: - caller_frame.f_trace = self.trace_dispatch + self._set_caller_tracefunc() self._set_stopinfo(None, None) + def set_stepi(self, frame): + """Stop after one opcode.""" + self._set_caller_tracefunc() + self._set_stopinfo(None, None, lasti=frame.f_lasti) + def set_next(self, frame): """Stop on the next line in or below the given frame.""" self._set_stopinfo(frame, None) + def set_nexti(self, frame): + """Stop on the next line in or below the given frame.""" + self._set_stopinfo(frame, None, lasti=frame.f_lasti) + def set_return(self, frame): """Stop when returning from the given frame.""" if frame.f_code.co_flags & GENERATOR_AND_COROUTINE_FLAGS: diff --git a/Lib/pdb.py b/Lib/pdb.py index 3a06cd00ad2bf1..3ff9eb1dc9adb7 100755 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -203,6 +203,7 @@ def namespace(self): # command "pdb.line_prefix = '\n% '". # line_prefix = ': ' # Use this to get the old situation back line_prefix = '\n-> ' # Probably a better default +inst_prefix = '\n--> ' # Probably a better default class Pdb(bdb.Bdb, cmd.Cmd): @@ -330,6 +331,15 @@ def user_line(self, frame): if self.bp_commands(frame): self.interaction(frame, None) + def user_opcode(self, frame): + """This function is called when we are about to execute an opcode.""" + if self._wait_for_mainpyfile: + if (self.mainpyfile != self.canonic(frame.f_code.co_filename) + or frame.f_lineno <= 0): + return + self._wait_for_mainpyfile = False + self.interaction(frame, None) + def bp_commands(self, frame): """Call every command that was set for the current active breakpoint (if there is one). @@ -422,6 +432,8 @@ def interaction(self, frame, traceback): self.forget() return self.print_stack_entry(self.stack[self.curindex]) + if self.trace_opcodes: + self.print_current_inst(frame) self._cmdloop() self.forget() @@ -1087,15 +1099,34 @@ def do_step(self, arg): return 1 do_s = do_step + def do_stepi(self, arg): + """s(tep) + Execute the current bytecode instruction, stop at the first + possible occasion (either in a function that is called or in + the current function). + """ + self.set_stepi(self.curframe) + return 1 + do_si = do_stepi + def do_next(self, arg): """n(ext) Continue execution until the next line in the current function - is reached or it returns. + is reached or the current function returns. """ self.set_next(self.curframe) return 1 do_n = do_next + def do_nexti(self, arg): + """n(ext) + Continue execution until the next bytecode instruction in the + current function is reached or the current function returns. + """ + self.set_nexti(self.curframe) + return 1 + do_ni = do_nexti + def do_run(self, arg): """run [args...] Restart the debugged python program. If a string is supplied @@ -1287,21 +1318,7 @@ def do_pp(self, arg): complete_p = _complete_expression complete_pp = _complete_expression - def do_list(self, arg): - """l(ist) [first [,last] | .] - - List source code for the current file. Without arguments, - list 11 lines around the current line or continue the previous - listing. With . as argument, list 11 lines around the current - line. With one argument, list 11 lines starting at that line. - With two arguments, list the given range; if the second - argument is less than the first, it is a count. - - The current line in the current frame is indicated by "->". - If an exception is being debugged, the line where the - exception was originally raised or propagated is indicated by - ">>", if it differs from the current line. - """ + def _do_list(self, arg, show_instructions=False): self.lastcmd = 'list' last = None if arg and arg != '.': @@ -1335,19 +1352,60 @@ def do_list(self, arg): breaklist = self.get_file_breaks(filename) try: lines = linecache.getlines(filename, self.curframe.f_globals) + instructions = dis.get_instructions(self.curframe.f_code, adaptive=True) \ + if show_instructions else None self._print_lines(lines[first-1:last], first, breaklist, - self.curframe) + self.curframe, + instructions) self.lineno = min(last, len(lines)) if len(lines) < last: self.message('[EOF]') except KeyboardInterrupt: pass + + def do_list(self, arg): + """l(ist) [first [,last] | .] + + List source code for the current file. Without arguments, + list 11 lines around the current line or continue the previous + listing. With . as argument, list 11 lines around the current + line. With one argument, list 11 lines starting at that line. + With two arguments, list the given range; if the second + argument is less than the first, it is a count. + + The current line in the current frame is indicated by "->". + If an exception is being debugged, the line where the + exception was originally raised or propagated is indicated by + ">>", if it differs from the current line. + """ + self._do_list(arg, False) do_l = do_list - def do_longlist(self, arg): - """longlist | ll - List the whole source code for the current function or frame. + def do_listi(self, arg): + """listi | li [first[, last] | .] + + List source code for the current file with bytecode + instructions. + + Without arguments, list 11 lines with their corresponding + instructions around the current line or continue the + previous listing. With . as argument, list 11 lines with + their corresponding instructions around the current line. + With one argument, list 11 lines with their corresponding + instructions starting at that line. With two arguments, + list the given range; if the second argument is less than + the first, it is a count. + + The current line in the current frame is indicated by "->". + The current instruction is indicated by "-->" + If an exception is being debugged, the line where the + exception was originally raised or propagated is indicated by + ">>", if it differs from the current line. """ + self._do_list(arg, True) + do_li = do_listi + + def _do_longlist(self, arg, show_instructions=False): filename = self.curframe.f_code.co_filename breaklist = self.get_file_breaks(filename) try: @@ -1355,9 +1413,27 @@ def do_longlist(self, arg): except OSError as err: self.error(err) return - self._print_lines(lines, lineno, breaklist, self.curframe) + instructions = dis.get_instructions(self.curframe.f_code, adaptive=True) \ + if show_instructions else None + self._print_lines(lines, lineno, breaklist, self.curframe, + instructions) + + def do_longlist(self, arg): + """longlist | ll + List the whole source code for the current function or frame. + """ + self._do_longlist(arg, False) do_ll = do_longlist + def do_longlisti(self, arg): + """longlisti | lli + + List the whole source code with bytecode instructions for + the current function or frame. + """ + self._do_longlist(arg, True) + do_lli = do_longlisti + def do_source(self, arg): """source expression Try to get source code for the given object and display it. @@ -1375,13 +1451,15 @@ def do_source(self, arg): complete_source = _complete_expression - def _print_lines(self, lines, start, breaks=(), frame=None): + def _print_lines(self, lines, start, breaks=(), frame=None, instructions=None): """Print a range of lines.""" if frame: current_lineno = frame.f_lineno exc_lineno = self.tb_lineno.get(frame, -1) else: current_lineno = exc_lineno = -1 + if instructions: + inst = next(instructions) for lineno, line in enumerate(lines, start): s = str(lineno).rjust(3) if len(s) < 4: @@ -1395,6 +1473,24 @@ def _print_lines(self, lines, start, breaks=(), frame=None): elif lineno == exc_lineno: s += '>>' self.message(s + '\t' + line.rstrip()) + if instructions: + # For the current line of the source code, get all the + # instructions belong to it. We keep a single iterator + # `instructions` for all the instructions compiled from + # the source and try to only go through the iterator once + while True: + if inst.positions.lineno == lineno: + current_inst = frame and frame.f_lasti == inst.offset + disassem = inst._disassemble(lineno_width=None, + mark_as_current=current_inst) + self.message(f" {disassem}") + elif inst.positions.lineno is not None and \ + inst.positions.lineno > lineno: + break + try: + inst = next(instructions) + except StopIteration: + break def do_whatis(self, arg): """whatis arg @@ -1559,6 +1655,13 @@ def print_stack_entry(self, frame_lineno, prompt_prefix=line_prefix): self.message(prefix + self.format_stack_entry(frame_lineno, prompt_prefix)) + def print_current_inst(self, frame): + for inst in dis.get_instructions(frame.f_code): + if inst.offset == frame.f_lasti: + self.message(inst._disassemble(lineno_width=None, + mark_as_current=True)) + return + # Provide help def do_help(self, arg): @@ -1668,10 +1771,11 @@ def _compile_error_message(self, expr): # unfortunately we can't guess this order from the class definition _help_order = [ 'help', 'where', 'down', 'up', 'break', 'tbreak', 'clear', 'disable', - 'enable', 'ignore', 'condition', 'commands', 'step', 'next', 'until', - 'jump', 'return', 'retval', 'run', 'continue', 'list', 'longlist', - 'args', 'p', 'pp', 'whatis', 'source', 'display', 'undisplay', - 'interact', 'alias', 'unalias', 'debug', 'quit', + 'enable', 'ignore', 'condition', 'commands', 'step', 'stepi', + 'next', 'nexti', 'until', 'jump', 'return', 'retval', 'run', + 'continue', 'list', 'listi', 'longlist', 'longlisti', 'args', 'p', + 'pp', 'whatis', 'source', 'display', 'undisplay', 'interact', 'alias', + 'unalias', 'debug', 'quit', ] for _command in _help_order: diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index de2bab46495729..735092f6d7ab22 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -1,5 +1,6 @@ # A test suite for pdb; not very comprehensive at the moment. +import dis import doctest import os import pdb @@ -2351,6 +2352,101 @@ def _create_fake_frozen_module(): # verify that pdb found the source of the "frozen" function self.assertIn('x = "Sentinel string for gh-93696"', stdout, "Sentinel statement not found") + def get_func_opnames(self, func_def, func_name): + extract_code = f""" + import dis + for inst in dis.get_instructions({func_name}): + print(inst.opname) + """ + + with redirect_stdout(StringIO()) as s: + exec(textwrap.dedent(func_def) + textwrap.dedent(extract_code)) + + return s.getvalue().splitlines() + + def test_list_instruction(self): + func_def = """ + def f(): + a = [1, 2, 3] + return a[0] + """ + func_exec = """ + f() + """ + script = func_def + func_exec + + commands_li = """ + break f + c + li + """ + + commands_lli = """ + break f + c + lli + """ + + # Make sure all the opcodes are listed + stdout, stderr = self.run_pdb_module(script, commands_li) + for opname in self.get_func_opnames(func_def, "f"): + self.assertIn(opname, stdout) + + stdout, stderr = self.run_pdb_module(script, commands_lli) + for opname in self.get_func_opnames(func_def, "f"): + self.assertIn(opname, stdout) + + def test_instruction_level_control(self): + func_def = """ + def f(): + a = [1, 2, 3] + return a[0] + """ + func_exec = """ + f() + """ + script = func_def + func_exec + + commands = """ + ni + li + """ + + # Check that after ni, current instruction is displayed + stdout, stderr = self.run_pdb_module(script, commands) + lines = [line.strip() for line in stdout.splitlines()] + for idx, line in enumerate(lines): + if "-->" in line: + # Found the current instruction indicator after ni + # Make sure that is listed in li + self.assertIn(line, lines[idx+1:]) + break + + commands = """ + ni + ni + ni + c + """ + + stdout, stderr = self.run_pdb_module(script, commands) + curr_instr_lines = [line.strip() for line in stdout.splitlines() if "-->" in line] + self.assertEqual(len(curr_instr_lines), 3) + for line in curr_instr_lines: + # Make sure ni is moving forward, not stopping at the same instruction + self.assertEqual(curr_instr_lines.count(line), 1) + + # this test is under the assumption that within 10 instructions the function + # f should be called + commands = "si\n" * 10 + "c\n" + + stdout, stderr = self.run_pdb_module(script, commands) + curr_instr_lines = [line.strip() for line in stdout.splitlines()] + # Make sure si stepped into the function so the users can see the source + # code of the function + self.assertTrue(any("-> a = [1, 2, 3]" in line for line in curr_instr_lines)) + + class ChecklineTests(unittest.TestCase): def setUp(self): linecache.clearcache() # Pdb.checkline() uses linecache.getline() diff --git a/Misc/NEWS.d/next/Library/2023-04-08-23-20-34.gh-issue-103049.PY_2cD.rst b/Misc/NEWS.d/next/Library/2023-04-08-23-20-34.gh-issue-103049.PY_2cD.rst new file mode 100644 index 00000000000000..f20de87713ba6f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-04-08-23-20-34.gh-issue-103049.PY_2cD.rst @@ -0,0 +1 @@ +Add instruction commands support for :mod:`pdb` pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy