https://github.com/python/cpython/commit/a936af924efc6e2fb59e27990dcd905b7819470a
commit: a936af924efc6e2fb59e27990dcd905b7819470a
branch: main
author: Tian Gao <gaogaotiant...@hotmail.com>
committer: gaogaotiantian <gaogaotiant...@hotmail.com>
date: 2025-03-17T18:34:37-04:00
summary:

gh-120144: Make it possible to use `sys.monitoring` for bdb and make it default 
for pdb (#124533)

files:
A Misc/NEWS.d/next/Library/2024-09-25-18-45-03.gh-issue-120144.JUcjLG.rst
M Doc/library/bdb.rst
M Doc/library/pdb.rst
M Doc/whatsnew/3.14.rst
M Lib/bdb.py
M Lib/pdb.py
M Lib/test/test_pdb.py

diff --git a/Doc/library/bdb.rst b/Doc/library/bdb.rst
index 85df7914a9a014..90f042aa377711 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,20 @@ The :mod:`bdb` module also defines two classes:
       <os.path.abspath>`. A *filename* with angle brackets, such as 
``"<stdin>"``
       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)``
+
+      .. 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
@@ -364,6 +391,28 @@ 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.
+
+      .. versionadded:: 3.14
+
+   .. 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.
+
+      .. versionadded:: 3.14
+
+
    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 c4ddff378d9c83..75f4b370795282 100644
--- a/Doc/library/pdb.rst
+++ b/Doc/library/pdb.rst
@@ -203,13 +203,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.
 
@@ -235,6 +254,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()
@@ -254,6 +277,9 @@ access further features, you have to do this yourself:
    .. versionadded:: 3.14
       Added the *mode* argument.
 
+   .. versionadded:: 3.14
+      Added the *backend* 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 789156974cb0d1..9a25f27ca57257 100644
--- a/Doc/whatsnew/3.14.rst
+++ b/Doc/whatsnew/3.14.rst
@@ -423,6 +423,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
 --------
@@ -843,6 +848,13 @@ 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
+  :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
 ------
diff --git a/Lib/bdb.py b/Lib/bdb.py
index 2463cc217c6d75..d32a05f59ad692 100644
--- a/Lib/bdb.py
+++ b/Lib/bdb.py
@@ -2,6 +2,7 @@
 
 import fnmatch
 import sys
+import threading
 import os
 import weakref
 from contextlib import contextmanager
@@ -16,6 +17,181 @@ class BdbQuit(Exception):
     """Exception to give up completely."""
 
 
+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',
+    }
+
+    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
+        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)
+        elif curr_tool == self._name:
+            sys.monitoring.clear_tool_id(self._tool_id)
+        else:
+            raise ValueError('Another debugger is using the monitoring tool')
+        E = sys.monitoring.events
+        all_events = 0
+        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_func()
+        self.check_trace_opcodes()
+        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
+        sys.monitoring.clear_tool_id(self._tool_id)
+        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):
+            if self._tracing_thread != threading.current_thread():
+                return
+            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:
+                    return ret
+            except BaseException:
+                self.stop_trace()
+                sys._getframe().f_back.f_trace = None
+                raise
+            finally:
+                self._disable_current_event = False
+
+        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
+            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):
+        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 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
+        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.
 
@@ -30,7 +206,7 @@ class Bdb:
     is determined by the __name__ in the frame globals.
     """
 
-    def __init__(self, skip=None):
+    def __init__(self, skip=None, backend='settrace'):
         self.skip = set(skip) if skip else None
         self.breaks = {}
         self.fncache = {}
@@ -39,6 +215,13 @@ 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()
+        elif backend == 'settrace':
+            self.monitoring_tracer = None
+        else:
+            raise ValueError(f"Invalid backend '{backend}'")
 
         self._load_breaks()
 
@@ -59,6 +242,18 @@ def canonic(self, filename):
             self.fncache[filename] = canonic
         return canonic
 
+    def start_trace(self):
+        if self.monitoring_tracer:
+            self.monitoring_tracer.start_trace(self.trace_dispatch)
+        else:
+            sys.settrace(self.trace_dispatch)
+
+    def stop_trace(self):
+        if self.monitoring_tracer:
+            self.monitoring_tracer.stop_trace()
+        else:
+            sys.settrace(None)
+
     def reset(self):
         """Set values of attributes as ready to start debugging."""
         import linecache
@@ -128,7 +323,10 @@ 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()
         return self.trace_dispatch
 
     def dispatch_call(self, frame, arg):
@@ -150,6 +348,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
 
@@ -170,6 +369,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
@@ -197,6 +397,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
@@ -206,6 +407,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
@@ -221,6 +423,7 @@ def dispatch_opcode(self, frame, arg):
         unconditionally.
         """
         self.user_opcode(frame)
+        self.restart_events()
         if self.quitting: raise BdbQuit
         return self.trace_dispatch
 
@@ -336,6 +539,8 @@ def _set_trace_opcodes(self, trace_opcodes):
             frame = self.enterframe
             while frame is not None:
                 frame.f_trace_opcodes = trace_opcodes
+                if self.monitoring_tracer:
+                    self.monitoring_tracer.set_trace_opcodes(frame, 
trace_opcodes)
                 if frame is self.botframe:
                     break
                 frame = frame.f_back
@@ -400,7 +605,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()
@@ -413,7 +618,8 @@ def set_trace(self, frame=None):
                 frame.f_trace_lines = True
                 frame = frame.f_back
             self.set_stepinstr()
-        sys.settrace(self.trace_dispatch)
+            self.enterframe = None
+        self.start_trace()
 
     def set_continue(self):
         """Stop only at breakpoints or when finished.
@@ -424,13 +630,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 = {}
 
     def set_quit(self):
@@ -441,7 +649,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
@@ -669,6 +877,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.
@@ -686,14 +904,14 @@ def run(self, cmd, globals=None, locals=None):
         self.reset()
         if isinstance(cmd, str):
             cmd = compile(cmd, "<string>", "exec")
-        sys.settrace(self.trace_dispatch)
+        self.start_trace()
         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.
@@ -706,14 +924,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()
         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()."""
@@ -728,7 +946,7 @@ def runcall(self, func, /, *args, **kwds):
         Return the result of the function call.
         """
         self.reset()
-        sys.settrace(self.trace_dispatch)
+        self.start_trace()
         res = None
         try:
             res = func(*args, **kwds)
@@ -736,7 +954,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 0357e46ead3ec8..160a7043a30c55 100644
--- a/Lib/pdb.py
+++ b/Lib/pdb.py
@@ -99,7 +99,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):
@@ -302,6 +302,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
@@ -315,8 +332,8 @@ 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):
-        bdb.Bdb.__init__(self, skip=skip)
+                 nosigint=False, readrc=True, mode=None, backend=None):
+        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:
@@ -1768,7 +1785,7 @@ def do_debug(self, arg):
         if not arg:
             self._print_invalid_arg(arg)
             return
-        sys.settrace(None)
+        self.stop_trace()
         globals = self.curframe.f_globals
         locals = self.curframe.f_locals
         p = Pdb(self.completekey, self.stdin, self.stdout)
@@ -1779,7 +1796,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.lastcmd = p.lastcmd
 
     complete_debug = _complete_expression
@@ -2469,7 +2486,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)
@@ -2600,7 +2617,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 2c85c63bea0915..8cd634426bd88b 100644
--- a/Lib/test/test_pdb.py
+++ b/Lib/test/test_pdb.py
@@ -364,6 +364,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()
+    > <doctest 
test.test_pdb.test_pdb_breakpoint_ignore_and_condition[1]>(2)test_function()
+    -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace()
+    (Pdb) break 4
+    Breakpoint 1 at <doctest 
test.test_pdb.test_pdb_breakpoint_ignore_and_condition[1]>:4
+    (Pdb) ignore 1 2
+    Will ignore next 2 crossings of breakpoint 1.
+    (Pdb) continue
+    0
+    1
+    > <doctest 
test.test_pdb.test_pdb_breakpoint_ignore_and_condition[1]>(4)test_function()
+    -> print(i)
+    (Pdb) condition 1 i == 4
+    New condition set for breakpoint 1.
+    (Pdb) continue
+    2
+    3
+    > <doctest 
test.test_pdb.test_pdb_breakpoint_ignore_and_condition[1]>(4)test_function()
+    -> print(i)
+    (Pdb) clear 1
+    Deleted breakpoint 1 at <doctest 
test.test_pdb.test_pdb_breakpoint_ignore_and_condition[1]>:4
+    (Pdb) continue
+    4
+    """
+
 def test_pdb_breakpoint_on_annotated_function_def():
     """Test breakpoints on function definitions with annotation.
 
@@ -488,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()
+    > <doctest 
test.test_pdb.test_pdb_breakpoint_on_disabled_line[1]>(2)test_function()
+    -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace()
+    (Pdb) break 5
+    Breakpoint 1 at <doctest 
test.test_pdb.test_pdb_breakpoint_on_disabled_line[1]>:5
+    (Pdb) c
+    > <doctest 
test.test_pdb.test_pdb_breakpoint_on_disabled_line[1]>(5)test_function()
+    -> print(j)
+    (Pdb) clear 1
+    Deleted breakpoint 1 at <doctest 
test.test_pdb.test_pdb_breakpoint_on_disabled_line[1]>:5
+    (Pdb) break 4
+    Breakpoint 2 at <doctest 
test.test_pdb.test_pdb_breakpoint_on_disabled_line[1]>:4
+    (Pdb) c
+    0
+    > <doctest 
test.test_pdb.test_pdb_breakpoint_on_disabled_line[1]>(4)test_function()
+    -> j = i * 2
+    (Pdb) clear 2
+    Deleted breakpoint 2 at <doctest 
test.test_pdb.test_pdb_breakpoint_on_disabled_line[1]>:4
+    (Pdb) c
+    2
+    4
+    """
+
 def test_pdb_breakpoints_preserved_across_interactive_sessions():
     """Breakpoints are remembered between interactive sessions
 
@@ -4585,7 +4670,13 @@ def func():
 
 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.set_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
 
 
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`.

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: arch...@mail-archive.com

Reply via email to