https://github.com/python/cpython/commit/a936af924efc6e2fb59e27990dcd905b7819470a
commit: a936af924efc6e2fb59e27990dcd905b7819470a
branch: main
author: Tian Gao <[email protected]>
committer: gaogaotiantian <[email protected]>
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 -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: [email protected]