From 18e6e9ceca88f6f3946ab06fe12b8562246562af Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Wed, 25 Sep 2024 10:00:49 -0700 Subject: [PATCH 01/17] Allow using sys.monitoring for bdb --- Lib/bdb.py | 180 +++++++++++++++++++++++++++++++++++++++++++++++++---- Lib/pdb.py | 4 +- 2 files changed, 171 insertions(+), 13 deletions(-) diff --git a/Lib/bdb.py b/Lib/bdb.py index 73e249621a053b..a72e985abac32c 100644 --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -5,6 +5,7 @@ import os import weakref from inspect import CO_GENERATOR, CO_COROUTINE, CO_ASYNC_GENERATOR +from functools import partial __all__ = ["BdbQuit", "Bdb", "Breakpoint"] @@ -15,6 +16,142 @@ class BdbQuit(Exception): """Exception to give up completely.""" +E = sys.monitoring.events + +class _MonitoringTracer: + def __init__(self): + self._tool_id = sys.monitoring.DEBUGGER_ID + self._name = 'bdbtracer' + self._tracefunc = None + + def start_trace(self, tracefunc): + self._tracefunc = tracefunc + curr_tool = sys.monitoring.get_tool(self._tool_id) + if curr_tool is None: + sys.monitoring.use_tool_id(self._tool_id, self._name) + elif curr_tool == self._name: + sys.monitoring.set_events(self._tool_id, 0) + else: + raise ValueError('Another debugger is using the monitoring tool') + E = sys.monitoring.events + all_events = 0 + for event in (E.PY_START, E.PY_RESUME, E.PY_THROW): + sys.monitoring.register_callback(self._tool_id, event, self.call_callback) + all_events |= event + for event in (E.LINE, ): + sys.monitoring.register_callback(self._tool_id, event, self.line_callback) + all_events |= event + for event in (E.JUMP, ): + sys.monitoring.register_callback(self._tool_id, event, self.jump_callback) + all_events |= event + for event in (E.PY_RETURN, E.PY_YIELD): + sys.monitoring.register_callback(self._tool_id, event, self.return_callback) + all_events |= event + for event in (E.PY_UNWIND, ): + sys.monitoring.register_callback(self._tool_id, event, self.unwind_callback) + all_events |= event + for event in (E.RAISE, E.STOP_ITERATION): + sys.monitoring.register_callback(self._tool_id, event, self.exception_callback) + all_events |= event + for event in (E.INSTRUCTION, ): + sys.monitoring.register_callback(self._tool_id, event, self.opcode_callback) + self.check_trace_opcodes() + sys.monitoring.set_events(self._tool_id, all_events) + + def stop_trace(self): + curr_tool = sys.monitoring.get_tool(self._tool_id) + if curr_tool != self._name: + return + for event in (E.PY_START, E.PY_RESUME, E.PY_RETURN, E.PY_YIELD, E.RAISE, E.LINE, + E.JUMP, E.PY_UNWIND, E.PY_THROW, E.STOP_ITERATION): + sys.monitoring.register_callback(self._tool_id, event, None) + sys.monitoring.set_events(self._tool_id, 0) + self.check_trace_opcodes() + sys.monitoring.free_tool_id(self._tool_id) + + def callback_wrapper(func): + def wrapper(self, *args): + try: + frame = sys._getframe().f_back + return func(self, frame, *args) + except Exception: + self.stop_trace() + raise + return wrapper + + @callback_wrapper + def call_callback(self, frame, code, *args): + local_tracefunc = self._tracefunc(frame, 'call', None) + if local_tracefunc is not None: + frame.f_trace = local_tracefunc + + @callback_wrapper + def return_callback(self, frame, code, offset, retval): + if frame.f_trace: + frame.f_trace(frame, 'return', retval) + + @callback_wrapper + def unwind_callback(self, frame, code, *args): + if frame.f_trace: + frame.f_trace(frame, 'return', None) + + @callback_wrapper + def line_callback(self, frame, code, *args): + if frame.f_trace and frame.f_trace_lines: + frame.f_trace(frame, 'line', None) + + @callback_wrapper + def jump_callback(self, frame, code, inst_offset, dest_offset): + if dest_offset > inst_offset: + return sys.monitoring.DISABLE + inst_lineno = self._get_lineno(code, inst_offset) + dest_lineno = self._get_lineno(code, dest_offset) + if inst_lineno != dest_lineno: + return sys.monitoring.DISABLE + if frame.f_trace and frame.f_trace_lines: + frame.f_trace(frame, 'line', None) + + @callback_wrapper + def exception_callback(self, frame, code, offset, exc): + if frame.f_trace: + if exc.__traceback__ and hasattr(exc.__traceback__, 'tb_frame'): + tb = exc.__traceback__ + while tb: + if tb.tb_frame.f_locals.get('self') is self: + return + tb = tb.tb_next + frame.f_trace(frame, 'exception', (type(exc), exc, exc.__traceback__)) + + @callback_wrapper + def opcode_callback(self, frame, code, offset): + if frame.f_trace and frame.f_trace_opcodes: + frame.f_trace(frame, 'opcode', None) + + def check_trace_opcodes(self, frame=None): + if frame is None: + frame = sys._getframe().f_back + while frame is not None: + self.set_trace_opcodes(frame, frame.f_trace_opcodes) + frame = frame.f_back + + def set_trace_opcodes(self, frame, trace_opcodes): + if sys.monitoring.get_tool(self._tool_id) != self._name: + return + if trace_opcodes: + sys.monitoring.set_local_events(self._tool_id, frame.f_code, E.INSTRUCTION) + else: + sys.monitoring.set_local_events(self._tool_id, frame.f_code, 0) + + def _get_lineno(self, code, offset): + import dis + last_lineno = None + for start, lineno in dis.findlinestarts(code): + if offset < start: + return last_lineno + last_lineno = lineno + return last_lineno + + class Bdb: """Generic Python debugger base class. @@ -29,7 +166,7 @@ class Bdb: is determined by the __name__ in the frame globals. """ - def __init__(self, skip=None): + def __init__(self, skip=None, backend='monitoring'): self.skip = set(skip) if skip else None self.breaks = {} self.fncache = {} @@ -38,6 +175,11 @@ def __init__(self, skip=None): self.trace_opcodes = False self.enterframe = None self.code_linenos = weakref.WeakKeyDictionary() + self.backend = backend + if backend == 'monitoring': + self.monitoring_tracer = _MonitoringTracer() + else: + self.monitoring_tracer = None self._load_breaks() @@ -58,6 +200,18 @@ def canonic(self, filename): self.fncache[filename] = canonic return canonic + def start_trace(self, trace_dispatch): + if self.backend == 'monitoring': + self.monitoring_tracer.start_trace(trace_dispatch) + else: + sys.settrace(self.trace_dispatch) + + def stop_trace(self): + if self.backend == 'monitoring': + self.monitoring_tracer.stop_trace() + else: + sys.settrace(None) + def reset(self): """Set values of attributes as ready to start debugging.""" import linecache @@ -327,6 +481,8 @@ def _set_trace_opcodes(self, trace_opcodes): frame = self.enterframe while frame is not None: frame.f_trace_opcodes = trace_opcodes + if self.backend == 'monitoring': + self.monitoring_tracer.set_trace_opcodes(frame, trace_opcodes) if frame is self.botframe: break frame = frame.f_back @@ -391,7 +547,7 @@ def set_trace(self, frame=None): If frame is not specified, debugging starts from caller's frame. """ - sys.settrace(None) + self.stop_trace() if frame is None: frame = sys._getframe().f_back self.reset() @@ -405,7 +561,7 @@ def set_trace(self, frame=None): frame = frame.f_back self.set_stepinstr() self.enterframe = None - sys.settrace(self.trace_dispatch) + self.start_trace(self.trace_dispatch) def set_continue(self): """Stop only at breakpoints or when finished. @@ -416,13 +572,15 @@ def set_continue(self): self._set_stopinfo(self.botframe, None, -1) if not self.breaks: # no breakpoints; run without debugger overhead - sys.settrace(None) + self.stop_trace() frame = sys._getframe().f_back while frame and frame is not self.botframe: del frame.f_trace frame = frame.f_back for frame, (trace_lines, trace_opcodes) in self.frame_trace_lines_opcodes.items(): frame.f_trace_lines, frame.f_trace_opcodes = trace_lines, trace_opcodes + if self.backend == 'monitoring': + self.monitoring_tracer.set_trace_opcodes(frame, trace_opcodes) self.frame_trace_lines_opcodes = {} self.enterframe = None @@ -434,7 +592,7 @@ def set_quit(self): self.stopframe = self.botframe self.returnframe = None self.quitting = True - sys.settrace(None) + self.stop_trace() # Derived classes and clients can call the following methods # to manipulate breakpoints. These methods return an @@ -679,14 +837,14 @@ def run(self, cmd, globals=None, locals=None): self.reset() if isinstance(cmd, str): cmd = compile(cmd, "", "exec") - sys.settrace(self.trace_dispatch) + self.start_trace(self.trace_dispatch) try: exec(cmd, globals, locals) except BdbQuit: pass finally: self.quitting = True - sys.settrace(None) + self.stop_trace() def runeval(self, expr, globals=None, locals=None): """Debug an expression executed via the eval() function. @@ -699,14 +857,14 @@ def runeval(self, expr, globals=None, locals=None): if locals is None: locals = globals self.reset() - sys.settrace(self.trace_dispatch) + self.start_trace(self.trace_dispatch) try: return eval(expr, globals, locals) except BdbQuit: pass finally: self.quitting = True - sys.settrace(None) + self.stop_trace() def runctx(self, cmd, globals, locals): """For backwards-compatibility. Defers to run().""" @@ -721,7 +879,7 @@ def runcall(self, func, /, *args, **kwds): Return the result of the function call. """ self.reset() - sys.settrace(self.trace_dispatch) + self.start_trace(self.trace_dispatch) res = None try: res = func(*args, **kwds) @@ -729,7 +887,7 @@ def runcall(self, func, /, *args, **kwds): pass finally: self.quitting = True - sys.settrace(None) + self.stop_trace() return res diff --git a/Lib/pdb.py b/Lib/pdb.py index 10d1923cdad2d6..4507975791d8bd 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -1704,7 +1704,7 @@ def do_debug(self, arg): argument (which is an arbitrary expression or statement to be executed in the current environment). """ - sys.settrace(None) + self.stop_trace() globals = self.curframe.f_globals locals = self.curframe.f_locals p = Pdb(self.completekey, self.stdin, self.stdout) @@ -1715,7 +1715,7 @@ def do_debug(self, arg): except Exception: self._error_exc() self.message("LEAVING RECURSIVE DEBUGGER") - sys.settrace(self.trace_dispatch) + self.start_trace(self.trace_dispatch) self.lastcmd = p.lastcmd complete_debug = _complete_expression From b29aff5a2df49c3a816fead9d323f555bb01e476 Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Wed, 25 Sep 2024 11:16:45 -0700 Subject: [PATCH 02/17] Add basic line ignore --- Lib/bdb.py | 80 ++++++++++++++++++++++++++++++++++++++---------------- Lib/pdb.py | 2 +- 2 files changed, 57 insertions(+), 25 deletions(-) diff --git a/Lib/bdb.py b/Lib/bdb.py index a72e985abac32c..b37faeba8493e6 100644 --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -5,7 +5,6 @@ import os import weakref from inspect import CO_GENERATOR, CO_COROUTINE, CO_ASYNC_GENERATOR -from functools import partial __all__ = ["BdbQuit", "Bdb", "Breakpoint"] @@ -19,10 +18,25 @@ class BdbQuit(Exception): E = sys.monitoring.events class _MonitoringTracer: + EVENT_CALLBACK_MAP = { + E.PY_START: 'call', + E.PY_RESUME: 'call', + E.PY_THROW: 'call', + E.LINE: 'line', + E.JUMP: 'jump', + E.PY_RETURN: 'return', + E.PY_YIELD: 'return', + E.PY_UNWIND: 'unwind', + E.RAISE: 'exception', + E.STOP_ITERATION: 'exception', + E.INSTRUCTION: 'opcode', + } + def __init__(self): self._tool_id = sys.monitoring.DEBUGGER_ID self._name = 'bdbtracer' self._tracefunc = None + self._disable_current_event = False def start_trace(self, tracefunc): self._tracefunc = tracefunc @@ -35,26 +49,11 @@ def start_trace(self, tracefunc): raise ValueError('Another debugger is using the monitoring tool') E = sys.monitoring.events all_events = 0 - for event in (E.PY_START, E.PY_RESUME, E.PY_THROW): - sys.monitoring.register_callback(self._tool_id, event, self.call_callback) - all_events |= event - for event in (E.LINE, ): - sys.monitoring.register_callback(self._tool_id, event, self.line_callback) - all_events |= event - for event in (E.JUMP, ): - sys.monitoring.register_callback(self._tool_id, event, self.jump_callback) - all_events |= event - for event in (E.PY_RETURN, E.PY_YIELD): - sys.monitoring.register_callback(self._tool_id, event, self.return_callback) - all_events |= event - for event in (E.PY_UNWIND, ): - sys.monitoring.register_callback(self._tool_id, event, self.unwind_callback) - all_events |= event - for event in (E.RAISE, E.STOP_ITERATION): - sys.monitoring.register_callback(self._tool_id, event, self.exception_callback) - all_events |= event - for event in (E.INSTRUCTION, ): - sys.monitoring.register_callback(self._tool_id, event, self.opcode_callback) + for event, cb_name in self.EVENT_CALLBACK_MAP.items(): + callback = getattr(self, f'{cb_name}_callback') + sys.monitoring.register_callback(self._tool_id, event, callback) + if event != E.INSTRUCTION: + all_events |= event self.check_trace_opcodes() sys.monitoring.set_events(self._tool_id, all_events) @@ -62,21 +61,37 @@ def stop_trace(self): curr_tool = sys.monitoring.get_tool(self._tool_id) if curr_tool != self._name: return - for event in (E.PY_START, E.PY_RESUME, E.PY_RETURN, E.PY_YIELD, E.RAISE, E.LINE, - E.JUMP, E.PY_UNWIND, E.PY_THROW, E.STOP_ITERATION): + for event in self.EVENT_CALLBACK_MAP.keys(): sys.monitoring.register_callback(self._tool_id, event, None) sys.monitoring.set_events(self._tool_id, 0) self.check_trace_opcodes() sys.monitoring.free_tool_id(self._tool_id) + def disable_current_event(self): + self._disable_current_event = True + + def restart_events(self): + if sys.monitoring.get_tool(self._tool_id) == self._name: + sys.monitoring.restart_events() + def callback_wrapper(func): + import functools + + @functools.wraps(func) def wrapper(self, *args): try: frame = sys._getframe().f_back - return func(self, frame, *args) + ret = func(self, frame, *args) + if self._disable_current_event: + return sys.monitoring.DISABLE + else: + return ret except Exception: self.stop_trace() raise + finally: + self._disable_current_event = False + return wrapper @callback_wrapper @@ -277,6 +292,8 @@ def dispatch_line(self, frame): if self.stop_here(frame) or self.break_here(frame): self.user_line(frame) if self.quitting: raise BdbQuit + else: + self.disable_current_event() return self.trace_dispatch def dispatch_call(self, frame, arg): @@ -298,6 +315,7 @@ def dispatch_call(self, frame, arg): if self.stopframe and frame.f_code.co_flags & GENERATOR_AND_COROUTINE_FLAGS: return self.trace_dispatch self.user_call(frame, arg) + self.restart_events() if self.quitting: raise BdbQuit return self.trace_dispatch @@ -318,6 +336,7 @@ def dispatch_return(self, frame, arg): try: self.frame_returning = frame self.user_return(frame, arg) + self.restart_events() finally: self.frame_returning = None if self.quitting: raise BdbQuit @@ -345,6 +364,7 @@ def dispatch_exception(self, frame, arg): if not (frame.f_code.co_flags & GENERATOR_AND_COROUTINE_FLAGS and arg[0] is StopIteration and arg[2] is None): self.user_exception(frame, arg) + self.restart_events() if self.quitting: raise BdbQuit # Stop at the StopIteration or GeneratorExit exception when the user # has set stopframe in a generator by issuing a return command, or a @@ -354,6 +374,7 @@ def dispatch_exception(self, frame, arg): and self.stopframe.f_code.co_flags & GENERATOR_AND_COROUTINE_FLAGS and arg[0] in (StopIteration, GeneratorExit)): self.user_exception(frame, arg) + self.restart_events() if self.quitting: raise BdbQuit return self.trace_dispatch @@ -366,6 +387,7 @@ def dispatch_opcode(self, frame, arg): """ if self.stop_here(frame) or self.break_here(frame): self.user_opcode(frame) + self.restart_events() if self.quitting: raise BdbQuit return self.trace_dispatch @@ -820,6 +842,16 @@ def format_stack_entry(self, frame_lineno, lprefix=': '): s += f'{lprefix}Warning: lineno is None' return s + def disable_current_event(self): + """Disable the current event.""" + if self.backend == 'monitoring': + self.monitoring_tracer.disable_current_event() + + def restart_events(self): + """Restart all events.""" + if self.backend == 'monitoring': + self.monitoring_tracer.restart_events() + # The following methods can be called by clients to use # a debugger to debug a statement or an expression. # Both can be given as a string, or a code object. diff --git a/Lib/pdb.py b/Lib/pdb.py index 4507975791d8bd..3b3ac1e1698c18 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -314,7 +314,7 @@ class Pdb(bdb.Bdb, cmd.Cmd): def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None, nosigint=False, readrc=True, mode=None): - bdb.Bdb.__init__(self, skip=skip) + bdb.Bdb.__init__(self, skip=skip, backend='monitoring') cmd.Cmd.__init__(self, completekey, stdin, stdout) sys.audit("pdb.Pdb") if stdout: From 23601f37f3150694238bf1140822e9bc4b5b1ab1 Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Wed, 25 Sep 2024 11:24:35 -0700 Subject: [PATCH 03/17] Use setttrace as default backend for bdb --- Lib/bdb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/bdb.py b/Lib/bdb.py index b37faeba8493e6..cd8c45482ad53f 100644 --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -181,7 +181,7 @@ class Bdb: is determined by the __name__ in the frame globals. """ - def __init__(self, skip=None, backend='monitoring'): + def __init__(self, skip=None, backend='settrace'): self.skip = set(skip) if skip else None self.breaks = {} self.fncache = {} From c9a92f601b4c8388d166eb940a8e43d724fce5dc Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Wed, 25 Sep 2024 18:45:07 +0000 Subject: [PATCH 04/17] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2024-09-25-18-45-03.gh-issue-120144.JUcjLG.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2024-09-25-18-45-03.gh-issue-120144.JUcjLG.rst diff --git a/Misc/NEWS.d/next/Library/2024-09-25-18-45-03.gh-issue-120144.JUcjLG.rst b/Misc/NEWS.d/next/Library/2024-09-25-18-45-03.gh-issue-120144.JUcjLG.rst new file mode 100644 index 00000000000000..fceda9a3e1800b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-09-25-18-45-03.gh-issue-120144.JUcjLG.rst @@ -0,0 +1 @@ +Add the optional backend of ``sys.monitoring`` to :mod:`bdb` and use it for :mod:`pdb`. From 8955d78969ceacddfbb76ea8046d7f2b9fc8da50 Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Thu, 26 Sep 2024 12:16:39 -0700 Subject: [PATCH 05/17] Use settrace by default for pdb.Pdb, but use monitoring for all direct pdb usage --- Lib/pdb.py | 9 +++++---- Lib/test/test_pdb.py | 8 +++++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index 3b3ac1e1698c18..c23050838f720d 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -307,14 +307,15 @@ class Pdb(bdb.Bdb, cmd.Cmd): # Limit the maximum depth of chained exceptions, we should be handling cycles, # but in case there are recursions, we stop at 999. MAX_CHAINED_EXCEPTION_DEPTH = 999 + DEFAULT_BACKEND = 'settrace' _file_mtime_table = {} _last_pdb_instance = None def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None, - nosigint=False, readrc=True, mode=None): - bdb.Bdb.__init__(self, skip=skip, backend='monitoring') + nosigint=False, readrc=True, mode=None, backend=None): + bdb.Bdb.__init__(self, skip=skip, backend=backend if backend else self.DEFAULT_BACKEND) cmd.Cmd.__init__(self, completekey, stdin, stdout) sys.audit("pdb.Pdb") if stdout: @@ -2376,7 +2377,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') + pdb = Pdb(mode='inline', backend='monitoring') if header is not None: pdb.message(header) pdb.set_trace(sys._getframe().f_back, commands=commands) @@ -2507,7 +2508,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') + pdb = Pdb(mode='cli', backend='monitoring') pdb.rcLines.extend(opts.commands) while True: try: diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index 09601623b29ac1..048fb6be33613d 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -4339,7 +4339,13 @@ def test_multiline_completion(self): def load_tests(loader, tests, pattern): from test import test_pdb - tests.addTest(doctest.DocTestSuite(test_pdb)) + def setUpPdbBackend(backend): + def setUp(test): + import pdb + pdb.Pdb.DEFAULT_BACKEND = backend + return setUp + tests.addTest(doctest.DocTestSuite(test_pdb, setUp=setUpPdbBackend('monitoring'))) + tests.addTest(doctest.DocTestSuite(test_pdb, setUp=setUpPdbBackend('settrace'))) return tests From 6afc2e7af15d8cfbc9de859ae35b789f7d6d1d2b Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Wed, 16 Oct 2024 19:28:13 -0400 Subject: [PATCH 06/17] Only trigger events on thread calling start_trace --- Lib/bdb.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Lib/bdb.py b/Lib/bdb.py index cd8c45482ad53f..81aa692676e65f 100644 --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -2,6 +2,7 @@ import fnmatch import sys +import threading import os import weakref from inspect import CO_GENERATOR, CO_COROUTINE, CO_ASYNC_GENERATOR @@ -37,9 +38,11 @@ def __init__(self): self._name = 'bdbtracer' self._tracefunc = None self._disable_current_event = False + self._tracing_thread = None def start_trace(self, tracefunc): self._tracefunc = tracefunc + self._tracing_thread = threading.current_thread() curr_tool = sys.monitoring.get_tool(self._tool_id) if curr_tool is None: sys.monitoring.use_tool_id(self._tool_id, self._name) @@ -58,6 +61,7 @@ def start_trace(self, tracefunc): sys.monitoring.set_events(self._tool_id, all_events) def stop_trace(self): + self._tracing_thread = None curr_tool = sys.monitoring.get_tool(self._tool_id) if curr_tool != self._name: return @@ -79,6 +83,8 @@ def callback_wrapper(func): @functools.wraps(func) def wrapper(self, *args): + if self._tracing_thread != threading.current_thread(): + return try: frame = sys._getframe().f_back ret = func(self, frame, *args) From b59568262f593730f3e445015f244e931dc57d54 Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Wed, 16 Oct 2024 19:58:42 -0400 Subject: [PATCH 07/17] Use local events when possible --- Lib/bdb.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/Lib/bdb.py b/Lib/bdb.py index 81aa692676e65f..4757c34b31e605 100644 --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -33,12 +33,16 @@ class _MonitoringTracer: E.INSTRUCTION: 'opcode', } + GLOBAL_EVENTS = E.PY_START | E.PY_RESUME | E.PY_THROW | E.PY_UNWIND | E.RAISE + LOCAL_EVENTS = E.LINE | E.JUMP | E.PY_RETURN | E.PY_YIELD | E.STOP_ITERATION + def __init__(self): self._tool_id = sys.monitoring.DEBUGGER_ID self._name = 'bdbtracer' self._tracefunc = None self._disable_current_event = False self._tracing_thread = None + self._enabled = False def start_trace(self, tracefunc): self._tracefunc = tracefunc @@ -47,7 +51,7 @@ def start_trace(self, tracefunc): if curr_tool is None: sys.monitoring.use_tool_id(self._tool_id, self._name) elif curr_tool == self._name: - sys.monitoring.set_events(self._tool_id, 0) + sys.monitoring.clear_tool_id(self._tool_id) else: raise ValueError('Another debugger is using the monitoring tool') E = sys.monitoring.events @@ -57,17 +61,18 @@ def start_trace(self, tracefunc): sys.monitoring.register_callback(self._tool_id, event, callback) if event != E.INSTRUCTION: all_events |= event + self.check_trace_func() self.check_trace_opcodes() - sys.monitoring.set_events(self._tool_id, all_events) + sys.monitoring.set_events(self._tool_id, self.GLOBAL_EVENTS) + self._enabled = True def stop_trace(self): + self._enabled = False self._tracing_thread = None curr_tool = sys.monitoring.get_tool(self._tool_id) if curr_tool != self._name: return - for event in self.EVENT_CALLBACK_MAP.keys(): - sys.monitoring.register_callback(self._tool_id, event, None) - sys.monitoring.set_events(self._tool_id, 0) + sys.monitoring.clear_tool_id(self._tool_id) self.check_trace_opcodes() sys.monitoring.free_tool_id(self._tool_id) @@ -88,6 +93,8 @@ def wrapper(self, *args): try: frame = sys._getframe().f_back ret = func(self, frame, *args) + if self._enabled and frame.f_trace: + self.check_trace_func() if self._disable_current_event: return sys.monitoring.DISABLE else: @@ -105,6 +112,8 @@ def call_callback(self, frame, code, *args): local_tracefunc = self._tracefunc(frame, 'call', None) if local_tracefunc is not None: frame.f_trace = local_tracefunc + if self._enabled: + sys.monitoring.set_local_events(self._tool_id, code, self.LOCAL_EVENTS) @callback_wrapper def return_callback(self, frame, code, offset, retval): @@ -163,6 +172,14 @@ def set_trace_opcodes(self, frame, trace_opcodes): else: sys.monitoring.set_local_events(self._tool_id, frame.f_code, 0) + def check_trace_func(self, frame=None): + if frame is None: + frame = sys._getframe().f_back + while frame is not None: + if frame.f_trace is not None: + sys.monitoring.set_local_events(self._tool_id, frame.f_code, self.LOCAL_EVENTS) + frame = frame.f_back + def _get_lineno(self, code, offset): import dis last_lineno = None From 70d51386ff394fd060f97b8cd0df97d9fbfa83b5 Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Sat, 8 Feb 2025 18:39:13 -0500 Subject: [PATCH 08/17] Fix ignore and condition of breakpoints --- Lib/bdb.py | 2 +- Lib/test/test_pdb.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/Lib/bdb.py b/Lib/bdb.py index 1d4663843f58b6..b1f85452e4506c 100644 --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -321,7 +321,7 @@ def dispatch_line(self, frame): if self.stop_here(frame) or self.break_here(frame): self.user_line(frame) if self.quitting: raise BdbQuit - else: + elif not self.get_break(frame.f_code.co_filename, frame.f_lineno): self.disable_current_event() return self.trace_dispatch diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index 048fb6be33613d..a0b44803a709d1 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -362,6 +362,49 @@ def test_pdb_breakpoint_commands(): 4 """ +def test_pdb_breakpoint_ignore_and_condition(): + """ + >>> reset_Breakpoint() + + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... for i in range(5): + ... print(i) + + >>> with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE + ... 'break 4', + ... 'ignore 1 2', # ignore once + ... 'continue', + ... 'condition 1 i == 4', + ... 'continue', + ... 'clear 1', + ... 'continue', + ... ]): + ... test_function() + > (2)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) break 4 + Breakpoint 1 at :4 + (Pdb) ignore 1 2 + Will ignore next 2 crossings of breakpoint 1. + (Pdb) continue + 0 + 1 + > (4)test_function() + -> print(i) + (Pdb) condition 1 i == 4 + New condition set for breakpoint 1. + (Pdb) continue + 2 + 3 + > (4)test_function() + -> print(i) + (Pdb) clear 1 + Deleted breakpoint 1 at :4 + (Pdb) continue + 4 + """ + def test_pdb_breakpoint_on_annotated_function_def(): """Test breakpoints on function definitions with annotation. From fe9971c5f4aaf6993e612d8c8c281e7c22a694f6 Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Tue, 18 Feb 2025 12:25:01 -0500 Subject: [PATCH 09/17] Address comments about backend --- Lib/bdb.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Lib/bdb.py b/Lib/bdb.py index b1f85452e4506c..21f4d8046f2222 100644 --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -217,8 +217,10 @@ def __init__(self, skip=None, backend='settrace'): self.backend = backend if backend == 'monitoring': self.monitoring_tracer = _MonitoringTracer() - else: + elif backend == 'settrace': self.monitoring_tracer = None + else: + raise ValueError(f"Invalid backend '{backend}'") self._load_breaks() @@ -240,13 +242,13 @@ def canonic(self, filename): return canonic def start_trace(self, trace_dispatch): - if self.backend == 'monitoring': + if self.monitoring_tracer: self.monitoring_tracer.start_trace(trace_dispatch) else: sys.settrace(self.trace_dispatch) def stop_trace(self): - if self.backend == 'monitoring': + if self.monitoring_tracer: self.monitoring_tracer.stop_trace() else: sys.settrace(None) @@ -532,7 +534,7 @@ def _set_trace_opcodes(self, trace_opcodes): frame = self.enterframe while frame is not None: frame.f_trace_opcodes = trace_opcodes - if self.backend == 'monitoring': + if self.monitoring_tracer: self.monitoring_tracer.set_trace_opcodes(frame, trace_opcodes) if frame is self.botframe: break From 05cc3b0b674240eb6d0bb85c925b63838f551294 Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Tue, 18 Feb 2025 15:49:59 -0500 Subject: [PATCH 10/17] We need BaseException to handle SystemExit case --- Lib/bdb.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/bdb.py b/Lib/bdb.py index 21f4d8046f2222..7a304bd7e4e878 100644 --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -100,8 +100,9 @@ def wrapper(self, *args): return sys.monitoring.DISABLE else: return ret - except Exception: + except BaseException: self.stop_trace() + sys._getframe().f_back.f_trace = None raise finally: self._disable_current_event = False From e6bc28774f31c39d5141f901ed97c787bd1cf867 Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Wed, 19 Feb 2025 16:42:04 -0500 Subject: [PATCH 11/17] Move default_backend to module level and provide utils --- Lib/pdb.py | 20 ++++++++++++++++++-- Lib/test/test_pdb.py | 2 +- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index f37133f38fa80a..fc8fe15ae7325f 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -301,6 +301,23 @@ def write(self, data): line_prefix = '\n-> ' # Probably a better default +# The default backend to use for Pdb instances if not specified +# Should be either 'settrace' or 'monitoring' +_default_backend = 'settrace' + + +def set_default_backend(backend): + """Set the default backend to use for Pdb instances.""" + global _default_backend + if backend not in ('settrace', 'monitoring'): + raise ValueError("Invalid backend: %s" % backend) + _default_backend = backend + + +def get_default_backend(): + """Get the default backend to use for Pdb instances.""" + return _default_backend + class Pdb(bdb.Bdb, cmd.Cmd): _previous_sigint_handler = None @@ -308,7 +325,6 @@ class Pdb(bdb.Bdb, cmd.Cmd): # Limit the maximum depth of chained exceptions, we should be handling cycles, # but in case there are recursions, we stop at 999. MAX_CHAINED_EXCEPTION_DEPTH = 999 - DEFAULT_BACKEND = 'settrace' _file_mtime_table = {} @@ -316,7 +332,7 @@ class Pdb(bdb.Bdb, cmd.Cmd): def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None, nosigint=False, readrc=True, mode=None, backend=None): - bdb.Bdb.__init__(self, skip=skip, backend=backend if backend else self.DEFAULT_BACKEND) + 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") if stdout: diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index d952c83eb123fd..c873e22441120d 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -4449,7 +4449,7 @@ def load_tests(loader, tests, pattern): def setUpPdbBackend(backend): def setUp(test): import pdb - pdb.Pdb.DEFAULT_BACKEND = backend + pdb.set_default_backend(backend) return setUp tests.addTest(doctest.DocTestSuite(test_pdb, setUp=setUpPdbBackend('monitoring'))) tests.addTest(doctest.DocTestSuite(test_pdb, setUp=setUpPdbBackend('settrace'))) From f0c1306461a4ad8ec7b914e5129593ccbbc2b339 Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Wed, 19 Feb 2025 17:06:35 -0500 Subject: [PATCH 12/17] Do not need to pass trace_dispatch explicitly --- Lib/bdb.py | 12 ++++++------ Lib/pdb.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Lib/bdb.py b/Lib/bdb.py index 7a304bd7e4e878..a35ca87609ee9c 100644 --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -242,9 +242,9 @@ def canonic(self, filename): self.fncache[filename] = canonic return canonic - def start_trace(self, trace_dispatch): + def start_trace(self): if self.monitoring_tracer: - self.monitoring_tracer.start_trace(trace_dispatch) + self.monitoring_tracer.start_trace(self.trace_dispatch) else: sys.settrace(self.trace_dispatch) @@ -615,7 +615,7 @@ def set_trace(self, frame=None): frame = frame.f_back self.set_stepinstr() self.enterframe = None - self.start_trace(self.trace_dispatch) + self.start_trace() def set_continue(self): """Stop only at breakpoints or when finished. @@ -900,7 +900,7 @@ def run(self, cmd, globals=None, locals=None): self.reset() if isinstance(cmd, str): cmd = compile(cmd, "", "exec") - self.start_trace(self.trace_dispatch) + self.start_trace() try: exec(cmd, globals, locals) except BdbQuit: @@ -920,7 +920,7 @@ def runeval(self, expr, globals=None, locals=None): if locals is None: locals = globals self.reset() - self.start_trace(self.trace_dispatch) + self.start_trace() try: return eval(expr, globals, locals) except BdbQuit: @@ -942,7 +942,7 @@ def runcall(self, func, /, *args, **kwds): Return the result of the function call. """ self.reset() - self.start_trace(self.trace_dispatch) + self.start_trace() res = None try: res = func(*args, **kwds) diff --git a/Lib/pdb.py b/Lib/pdb.py index fc8fe15ae7325f..d2923e24ea7d6a 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -1761,7 +1761,7 @@ def do_debug(self, arg): except Exception: self._error_exc() self.message("LEAVING RECURSIVE DEBUGGER") - self.start_trace(self.trace_dispatch) + self.start_trace() self.lastcmd = p.lastcmd complete_debug = _complete_expression From a9b53ed07fef9af658ba1b26928b4d5b40fd5ebd Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Wed, 19 Feb 2025 17:32:40 -0500 Subject: [PATCH 13/17] Add docs --- Doc/library/bdb.rst | 43 ++++++++++++++++++++++++++++++++++++++++++- Doc/library/pdb.rst | 28 +++++++++++++++++++++++++++- Doc/whatsnew/3.14.rst | 11 +++++++++++ 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/Doc/library/bdb.rst b/Doc/library/bdb.rst index 85df7914a9a014..ed4afaf3c3c085 100644 --- a/Doc/library/bdb.rst +++ b/Doc/library/bdb.rst @@ -118,7 +118,7 @@ The :mod:`bdb` module also defines two classes: Count of the number of times a :class:`Breakpoint` has been hit. -.. class:: Bdb(skip=None) +.. class:: Bdb(skip=None, backend='settrace') The :class:`Bdb` class acts as a generic Python debugger base class. @@ -132,9 +132,22 @@ The :mod:`bdb` module also defines two classes: frame is considered to originate in a certain module is determined by the ``__name__`` in the frame globals. + The *backend* argument specifies the backend to use for :class:`Bdb`. It + can be either ``'settrace'`` or ``'monitoring'``. ``'settrace'`` uses + :func:`sys.settrace` which has the best backward compatibility. The + ``'monitoring'`` backend uses the new :mod:`sys.monitoring` that was + introduced in Python 3.12, which can be much more efficient because it + can disable unused events. We are trying to keep the exact interfaces + for both backends, but there are some differences. The debugger developers + are encouraged to use the ``'monitoring'`` backend to achieve better + performance. + .. versionchanged:: 3.1 Added the *skip* parameter. + .. versionchanged:: 3.14 + Added the *backend* parameter. + The following methods of :class:`Bdb` normally don't need to be overridden. .. method:: canonic(filename) @@ -146,6 +159,16 @@ The :mod:`bdb` module also defines two classes: `. A *filename* with angle brackets, such as ``""`` generated in interactive mode, is returned unchanged. + .. method:: start_trace(self) + + Start tracing. For ``'settrace'`` backend, this method is equivalent to + ``sys.settrace(self.trace_dispatch)`` + + .. method:: stop_trace(self) + + Stop tracing. For ``'settrace'`` backend, this method is equivalent to + ``sys.settrace(None)`` + .. method:: reset() Set the :attr:`!botframe`, :attr:`!stopframe`, :attr:`!returnframe` and @@ -364,6 +387,24 @@ The :mod:`bdb` module also defines two classes: Return all breakpoints that are set. + Derived classes and clients can call the following methods to disable and + restart events to achieve better performance. These methods only work + when using the ``'monitoring'`` backend. + + .. method:: disable_current_event() + + Disable the current event until the next time :func:`restart_events` is + called. This is helpful when the debugger is not interested in the current + line. + + .. method:: restart_events() + + Restart all the disabled events. This function is automatically called in + ``dispatch_*`` methods after ``user_*`` methods are called. If the + ``dispatch_*`` methods are not overridden, the disabled events will be + restarted after each user interaction. + + Derived classes and clients can call the following methods to get a data structure representing a stack trace. diff --git a/Doc/library/pdb.rst b/Doc/library/pdb.rst index bdd89d127491a5..c46bca9748e421 100644 --- a/Doc/library/pdb.rst +++ b/Doc/library/pdb.rst @@ -194,13 +194,32 @@ slightly different way: Enter post-mortem debugging of the exception found in :data:`sys.last_exc`. +.. function:: set_default_backend(backend) + + There are two supported backends for pdb: ``'settrace'`` and ``'monitoring'``. + See :class:`bdb.Bdb` for details. The user can set the default backend to + use if none is specified when instantiating :class:`Pdb`. If no backend is + specified, the default is ``'settrace'``. + + .. note:: + + :func:`breakpoint` and :func:`set_trace` will not be affected by this + function. They always use ``'monitoring'`` backend. + + .. versionadded:: 3.14 + +.. function:: get_default_backend() + + Returns the default backend for pdb. + + .. versionadded:: 3.14 The ``run*`` functions and :func:`set_trace` are aliases for instantiating the :class:`Pdb` class and calling the method of the same name. If you want to 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) + nosigint=False, readrc=True, mode=None, backend=None) :class:`Pdb` is the debugger class. @@ -226,6 +245,10 @@ access further features, you have to do this yourself: or ``None`` (for backwards compatible behaviour, as before the *mode* argument was added). + The *backend* argument specifies the backend to use for the debugger. If ``None`` + is passed, the default backend will be used. See :func:`set_default_backend`. + Otherwise the supported backends are ``'settrace'`` and ``'monitoring'``. + Example call to enable tracing with *skip*:: import pdb; pdb.Pdb(skip=['django.*']).set_trace() @@ -245,6 +268,9 @@ access further features, you have to do this yourself: .. versionadded:: 3.14 Added the *mode* argument. + .. versionadded:: 3.14 + Added the *backend* argument. + .. method:: run(statement, globals=None, locals=None) runeval(expression, globals=None, locals=None) runcall(function, *args, **kwds) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 556af51756a521..b7882c1ac35e68 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -350,6 +350,11 @@ ast that the root node type is appropriate. (Contributed by Irit Katriel in :gh:`130139`.) +bdb +--- + +* The :mod:`bdb` module now supports the :mod:`sys.monitoring` backend. + (Contributed by Tian Gao in :gh:`124533`.) calendar -------- @@ -684,6 +689,12 @@ pdb the quit and call :func:`sys.exit`, instead of raising :exc:`bdb.BdbQuit`. (Contributed by Tian Gao in :gh:`124704`.) +* :mod:`pdb` now supports two backends: :func:`sys.settrace` and + :mod:`sys.monitoring`. Using :mod:`pdb` CLI or :func:`breakpoint` will + always use the :mod:`sys.monitoring` backend. Explicitly instantiating + :class:`pdb.Pdb` and its derived classes will use the :func:`sys.settrace` + backend by default, which is configurable. + (Contributed by Tian Gao in :gh:`124533`.) pickle ------ From 97900852e11f65043a8c0d57fa57c5d7d83a575f Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Wed, 19 Feb 2025 17:34:38 -0500 Subject: [PATCH 14/17] Add version added --- Doc/library/bdb.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Doc/library/bdb.rst b/Doc/library/bdb.rst index ed4afaf3c3c085..90f042aa377711 100644 --- a/Doc/library/bdb.rst +++ b/Doc/library/bdb.rst @@ -164,11 +164,15 @@ The :mod:`bdb` module also defines two classes: Start tracing. For ``'settrace'`` backend, this method is equivalent to ``sys.settrace(self.trace_dispatch)`` + .. versionadded:: 3.14 + .. method:: stop_trace(self) Stop tracing. For ``'settrace'`` backend, this method is equivalent to ``sys.settrace(None)`` + .. versionadded:: 3.14 + .. method:: reset() Set the :attr:`!botframe`, :attr:`!stopframe`, :attr:`!returnframe` and @@ -397,6 +401,8 @@ The :mod:`bdb` module also defines two classes: called. This is helpful when the debugger is not interested in the current line. + .. versionadded:: 3.14 + .. method:: restart_events() Restart all the disabled events. This function is automatically called in @@ -404,6 +410,8 @@ The :mod:`bdb` module also defines two classes: ``dispatch_*`` methods are not overridden, the disabled events will be restarted after each user interaction. + .. versionadded:: 3.14 + Derived classes and clients can call the following methods to get a data structure representing a stack trace. From ea811f22427fdef48741d2ef63c094719788f971 Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Wed, 19 Feb 2025 17:35:21 -0500 Subject: [PATCH 15/17] Add new functions to __all__ --- Lib/pdb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index d2923e24ea7d6a..8faa3d33ff6f3d 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -98,7 +98,7 @@ class Restart(Exception): pass __all__ = ["run", "pm", "Pdb", "runeval", "runctx", "runcall", "set_trace", - "post_mortem", "help"] + "post_mortem", "set_default_backend", "get_default_backend", "help"] def find_first_executable_line(code): From e4ccd8a6bc324fda246cc5c99a447bf8f449a20f Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Thu, 6 Mar 2025 21:04:51 -0500 Subject: [PATCH 16/17] Restart events after user line --- Lib/bdb.py | 1 + Lib/test/test_pdb.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/Lib/bdb.py b/Lib/bdb.py index 95569f0c1df9e8..d32a05f59ad692 100644 --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -323,6 +323,7 @@ def dispatch_line(self, frame): """ if self.stop_here(frame) or self.break_here(frame): self.user_line(frame) + self.restart_events() if self.quitting: raise BdbQuit elif not self.get_break(frame.f_code.co_filename, frame.f_lineno): self.disable_current_event() diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index 5a188cd2005496..aa519f912cd9cf 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -531,6 +531,48 @@ def test_pdb_breakpoint_with_filename(): (Pdb) continue """ +def test_pdb_breakpoint_on_disabled_line(): + """New breakpoint on once disabled line should work + + >>> reset_Breakpoint() + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... for i in range(3): + ... j = i * 2 + ... print(j) + + >>> with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE + ... 'break 5', + ... 'c', + ... 'clear 1', + ... 'break 4', + ... 'c', + ... 'clear 2', + ... 'c' + ... ]): + ... test_function() + > (2)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) break 5 + Breakpoint 1 at :5 + (Pdb) c + > (5)test_function() + -> print(j) + (Pdb) clear 1 + Deleted breakpoint 1 at :5 + (Pdb) break 4 + Breakpoint 2 at :4 + (Pdb) c + 0 + > (4)test_function() + -> j = i * 2 + (Pdb) clear 2 + Deleted breakpoint 2 at :4 + (Pdb) c + 2 + 4 + """ + def test_pdb_breakpoints_preserved_across_interactive_sessions(): """Breakpoints are remembered between interactive sessions From e9252ec8ddf08c354ad0300f583703fe3e293d3b Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Sun, 16 Mar 2025 20:39:37 -0400 Subject: [PATCH 17/17] Remove blank line --- Doc/whatsnew/3.14.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 39809c80edaa5f..cad8bd6a73037f 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -837,7 +837,7 @@ pdb * ``$_asynctask`` is added to access the current asyncio task if applicable. (Contributed by Tian Gao in :gh:`124367`.) - + * :mod:`pdb` now supports two backends: :func:`sys.settrace` and :mod:`sys.monitoring`. Using :mod:`pdb` CLI or :func:`breakpoint` will always use the :mod:`sys.monitoring` backend. Explicitly instantiating