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

This commit is contained in:
Tian Gao 2025-03-17 18:34:37 -04:00 committed by GitHub
parent f48887fb97
commit a936af924e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 435 additions and 21 deletions

View file

@ -118,7 +118,7 @@ The :mod:`bdb` module also defines two classes:
Count of the number of times a :class:`Breakpoint` has been hit. 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. 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 frame is considered to originate in a certain module is determined
by the ``__name__`` in the frame globals. 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 .. versionchanged:: 3.1
Added the *skip* parameter. Added the *skip* parameter.
.. versionchanged:: 3.14
Added the *backend* parameter.
The following methods of :class:`Bdb` normally don't need to be overridden. The following methods of :class:`Bdb` normally don't need to be overridden.
.. method:: canonic(filename) .. 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>"`` <os.path.abspath>`. A *filename* with angle brackets, such as ``"<stdin>"``
generated in interactive mode, is returned unchanged. 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() .. method:: reset()
Set the :attr:`!botframe`, :attr:`!stopframe`, :attr:`!returnframe` and 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. 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 Derived classes and clients can call the following methods to get a data
structure representing a stack trace. structure representing a stack trace.

View file

@ -203,13 +203,32 @@ slightly different way:
Enter post-mortem debugging of the exception found in Enter post-mortem debugging of the exception found in
:data:`sys.last_exc`. :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 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 :class:`Pdb` class and calling the method of the same name. If you want to
access further features, you have to do this yourself: access further features, you have to do this yourself:
.. class:: Pdb(completekey='tab', stdin=None, stdout=None, skip=None, \ .. 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. :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* or ``None`` (for backwards compatible behaviour, as before the *mode*
argument was added). 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*:: Example call to enable tracing with *skip*::
import pdb; pdb.Pdb(skip=['django.*']).set_trace() import pdb; pdb.Pdb(skip=['django.*']).set_trace()
@ -254,6 +277,9 @@ access further features, you have to do this yourself:
.. versionadded:: 3.14 .. versionadded:: 3.14
Added the *mode* argument. Added the *mode* argument.
.. versionadded:: 3.14
Added the *backend* argument.
.. versionchanged:: 3.14 .. versionchanged:: 3.14
Inline breakpoints like :func:`breakpoint` or :func:`pdb.set_trace` will Inline breakpoints like :func:`breakpoint` or :func:`pdb.set_trace` will
always stop the program at calling frame, ignoring the *skip* pattern (if any). always stop the program at calling frame, ignoring the *skip* pattern (if any).

View file

@ -423,6 +423,11 @@ ast
that the root node type is appropriate. that the root node type is appropriate.
(Contributed by Irit Katriel in :gh:`130139`.) (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 calendar
-------- --------
@ -843,6 +848,13 @@ pdb
* ``$_asynctask`` is added to access the current asyncio task if applicable. * ``$_asynctask`` is added to access the current asyncio task if applicable.
(Contributed by Tian Gao in :gh:`124367`.) (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 pickle
------ ------

View file

@ -2,6 +2,7 @@
import fnmatch import fnmatch
import sys import sys
import threading
import os import os
import weakref import weakref
from contextlib import contextmanager from contextlib import contextmanager
@ -16,6 +17,181 @@ class BdbQuit(Exception):
"""Exception to give up completely.""" """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: class Bdb:
"""Generic Python debugger base class. """Generic Python debugger base class.
@ -30,7 +206,7 @@ class Bdb:
is determined by the __name__ in the frame globals. 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.skip = set(skip) if skip else None
self.breaks = {} self.breaks = {}
self.fncache = {} self.fncache = {}
@ -39,6 +215,13 @@ class Bdb:
self.trace_opcodes = False self.trace_opcodes = False
self.enterframe = None self.enterframe = None
self.code_linenos = weakref.WeakKeyDictionary() 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() self._load_breaks()
@ -59,6 +242,18 @@ class Bdb:
self.fncache[filename] = canonic self.fncache[filename] = canonic
return 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): def reset(self):
"""Set values of attributes as ready to start debugging.""" """Set values of attributes as ready to start debugging."""
import linecache import linecache
@ -128,7 +323,10 @@ class Bdb:
""" """
if self.stop_here(frame) or self.break_here(frame): if self.stop_here(frame) or self.break_here(frame):
self.user_line(frame) self.user_line(frame)
self.restart_events()
if self.quitting: raise BdbQuit 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 return self.trace_dispatch
def dispatch_call(self, frame, arg): def dispatch_call(self, frame, arg):
@ -150,6 +348,7 @@ class Bdb:
if self.stopframe and frame.f_code.co_flags & GENERATOR_AND_COROUTINE_FLAGS: if self.stopframe and frame.f_code.co_flags & GENERATOR_AND_COROUTINE_FLAGS:
return self.trace_dispatch return self.trace_dispatch
self.user_call(frame, arg) self.user_call(frame, arg)
self.restart_events()
if self.quitting: raise BdbQuit if self.quitting: raise BdbQuit
return self.trace_dispatch return self.trace_dispatch
@ -170,6 +369,7 @@ class Bdb:
try: try:
self.frame_returning = frame self.frame_returning = frame
self.user_return(frame, arg) self.user_return(frame, arg)
self.restart_events()
finally: finally:
self.frame_returning = None self.frame_returning = None
if self.quitting: raise BdbQuit if self.quitting: raise BdbQuit
@ -197,6 +397,7 @@ class Bdb:
if not (frame.f_code.co_flags & GENERATOR_AND_COROUTINE_FLAGS if not (frame.f_code.co_flags & GENERATOR_AND_COROUTINE_FLAGS
and arg[0] is StopIteration and arg[2] is None): and arg[0] is StopIteration and arg[2] is None):
self.user_exception(frame, arg) self.user_exception(frame, arg)
self.restart_events()
if self.quitting: raise BdbQuit if self.quitting: raise BdbQuit
# Stop at the StopIteration or GeneratorExit exception when the user # Stop at the StopIteration or GeneratorExit exception when the user
# has set stopframe in a generator by issuing a return command, or a # has set stopframe in a generator by issuing a return command, or a
@ -206,6 +407,7 @@ class Bdb:
and self.stopframe.f_code.co_flags & GENERATOR_AND_COROUTINE_FLAGS and self.stopframe.f_code.co_flags & GENERATOR_AND_COROUTINE_FLAGS
and arg[0] in (StopIteration, GeneratorExit)): and arg[0] in (StopIteration, GeneratorExit)):
self.user_exception(frame, arg) self.user_exception(frame, arg)
self.restart_events()
if self.quitting: raise BdbQuit if self.quitting: raise BdbQuit
return self.trace_dispatch return self.trace_dispatch
@ -221,6 +423,7 @@ class Bdb:
unconditionally. unconditionally.
""" """
self.user_opcode(frame) self.user_opcode(frame)
self.restart_events()
if self.quitting: raise BdbQuit if self.quitting: raise BdbQuit
return self.trace_dispatch return self.trace_dispatch
@ -336,6 +539,8 @@ class Bdb:
frame = self.enterframe frame = self.enterframe
while frame is not None: while frame is not None:
frame.f_trace_opcodes = trace_opcodes frame.f_trace_opcodes = trace_opcodes
if self.monitoring_tracer:
self.monitoring_tracer.set_trace_opcodes(frame, trace_opcodes)
if frame is self.botframe: if frame is self.botframe:
break break
frame = frame.f_back frame = frame.f_back
@ -400,7 +605,7 @@ class Bdb:
If frame is not specified, debugging starts from caller's frame. If frame is not specified, debugging starts from caller's frame.
""" """
sys.settrace(None) self.stop_trace()
if frame is None: if frame is None:
frame = sys._getframe().f_back frame = sys._getframe().f_back
self.reset() self.reset()
@ -413,7 +618,8 @@ class Bdb:
frame.f_trace_lines = True frame.f_trace_lines = True
frame = frame.f_back frame = frame.f_back
self.set_stepinstr() self.set_stepinstr()
sys.settrace(self.trace_dispatch) self.enterframe = None
self.start_trace()
def set_continue(self): def set_continue(self):
"""Stop only at breakpoints or when finished. """Stop only at breakpoints or when finished.
@ -424,13 +630,15 @@ class Bdb:
self._set_stopinfo(self.botframe, None, -1) self._set_stopinfo(self.botframe, None, -1)
if not self.breaks: if not self.breaks:
# no breakpoints; run without debugger overhead # no breakpoints; run without debugger overhead
sys.settrace(None) self.stop_trace()
frame = sys._getframe().f_back frame = sys._getframe().f_back
while frame and frame is not self.botframe: while frame and frame is not self.botframe:
del frame.f_trace del frame.f_trace
frame = frame.f_back frame = frame.f_back
for frame, (trace_lines, trace_opcodes) in self.frame_trace_lines_opcodes.items(): 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 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.frame_trace_lines_opcodes = {}
def set_quit(self): def set_quit(self):
@ -441,7 +649,7 @@ class Bdb:
self.stopframe = self.botframe self.stopframe = self.botframe
self.returnframe = None self.returnframe = None
self.quitting = True self.quitting = True
sys.settrace(None) self.stop_trace()
# Derived classes and clients can call the following methods # Derived classes and clients can call the following methods
# to manipulate breakpoints. These methods return an # to manipulate breakpoints. These methods return an
@ -669,6 +877,16 @@ class Bdb:
s += f'{lprefix}Warning: lineno is None' s += f'{lprefix}Warning: lineno is None'
return s 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 # The following methods can be called by clients to use
# a debugger to debug a statement or an expression. # a debugger to debug a statement or an expression.
# Both can be given as a string, or a code object. # Both can be given as a string, or a code object.
@ -686,14 +904,14 @@ class Bdb:
self.reset() self.reset()
if isinstance(cmd, str): if isinstance(cmd, str):
cmd = compile(cmd, "<string>", "exec") cmd = compile(cmd, "<string>", "exec")
sys.settrace(self.trace_dispatch) self.start_trace()
try: try:
exec(cmd, globals, locals) exec(cmd, globals, locals)
except BdbQuit: except BdbQuit:
pass pass
finally: finally:
self.quitting = True self.quitting = True
sys.settrace(None) self.stop_trace()
def runeval(self, expr, globals=None, locals=None): def runeval(self, expr, globals=None, locals=None):
"""Debug an expression executed via the eval() function. """Debug an expression executed via the eval() function.
@ -706,14 +924,14 @@ class Bdb:
if locals is None: if locals is None:
locals = globals locals = globals
self.reset() self.reset()
sys.settrace(self.trace_dispatch) self.start_trace()
try: try:
return eval(expr, globals, locals) return eval(expr, globals, locals)
except BdbQuit: except BdbQuit:
pass pass
finally: finally:
self.quitting = True self.quitting = True
sys.settrace(None) self.stop_trace()
def runctx(self, cmd, globals, locals): def runctx(self, cmd, globals, locals):
"""For backwards-compatibility. Defers to run().""" """For backwards-compatibility. Defers to run()."""
@ -728,7 +946,7 @@ class Bdb:
Return the result of the function call. Return the result of the function call.
""" """
self.reset() self.reset()
sys.settrace(self.trace_dispatch) self.start_trace()
res = None res = None
try: try:
res = func(*args, **kwds) res = func(*args, **kwds)
@ -736,7 +954,7 @@ class Bdb:
pass pass
finally: finally:
self.quitting = True self.quitting = True
sys.settrace(None) self.stop_trace()
return res return res

View file

@ -99,7 +99,7 @@ class Restart(Exception):
pass pass
__all__ = ["run", "pm", "Pdb", "runeval", "runctx", "runcall", "set_trace", __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): def find_first_executable_line(code):
@ -302,6 +302,23 @@ class _PdbInteractiveConsole(code.InteractiveConsole):
line_prefix = '\n-> ' # Probably a better default 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): class Pdb(bdb.Bdb, cmd.Cmd):
_previous_sigint_handler = None _previous_sigint_handler = None
@ -315,8 +332,8 @@ class Pdb(bdb.Bdb, cmd.Cmd):
_last_pdb_instance = None _last_pdb_instance = None
def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None, def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None,
nosigint=False, readrc=True, mode=None): nosigint=False, readrc=True, mode=None, backend=None):
bdb.Bdb.__init__(self, skip=skip) bdb.Bdb.__init__(self, skip=skip, backend=backend if backend else get_default_backend())
cmd.Cmd.__init__(self, completekey, stdin, stdout) cmd.Cmd.__init__(self, completekey, stdin, stdout)
sys.audit("pdb.Pdb") sys.audit("pdb.Pdb")
if stdout: if stdout:
@ -1768,7 +1785,7 @@ class Pdb(bdb.Bdb, cmd.Cmd):
if not arg: if not arg:
self._print_invalid_arg(arg) self._print_invalid_arg(arg)
return return
sys.settrace(None) self.stop_trace()
globals = self.curframe.f_globals globals = self.curframe.f_globals
locals = self.curframe.f_locals locals = self.curframe.f_locals
p = Pdb(self.completekey, self.stdin, self.stdout) p = Pdb(self.completekey, self.stdin, self.stdout)
@ -1779,7 +1796,7 @@ class Pdb(bdb.Bdb, cmd.Cmd):
except Exception: except Exception:
self._error_exc() self._error_exc()
self.message("LEAVING RECURSIVE DEBUGGER") self.message("LEAVING RECURSIVE DEBUGGER")
sys.settrace(self.trace_dispatch) self.start_trace()
self.lastcmd = p.lastcmd self.lastcmd = p.lastcmd
complete_debug = _complete_expression complete_debug = _complete_expression
@ -2469,7 +2486,7 @@ def set_trace(*, header=None, commands=None):
if Pdb._last_pdb_instance is not None: if Pdb._last_pdb_instance is not None:
pdb = Pdb._last_pdb_instance pdb = Pdb._last_pdb_instance
else: else:
pdb = Pdb(mode='inline') pdb = Pdb(mode='inline', backend='monitoring')
if header is not None: if header is not None:
pdb.message(header) pdb.message(header)
pdb.set_trace(sys._getframe().f_back, commands=commands) 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 # 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 # changed by the user from the command line. There is a "restart" command
# which allows explicit specification of command line arguments. # which allows explicit specification of command line arguments.
pdb = Pdb(mode='cli') pdb = Pdb(mode='cli', backend='monitoring')
pdb.rcLines.extend(opts.commands) pdb.rcLines.extend(opts.commands)
while True: while True:
try: try:

View file

@ -364,6 +364,49 @@ def test_pdb_breakpoint_commands():
4 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(): def test_pdb_breakpoint_on_annotated_function_def():
"""Test breakpoints on function definitions with annotation. """Test breakpoints on function definitions with annotation.
@ -488,6 +531,48 @@ def test_pdb_breakpoint_with_filename():
(Pdb) continue (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(): def test_pdb_breakpoints_preserved_across_interactive_sessions():
"""Breakpoints are remembered between interactive sessions """Breakpoints are remembered between interactive sessions
@ -4585,7 +4670,13 @@ class PdbTestReadline(unittest.TestCase):
def load_tests(loader, tests, pattern): def load_tests(loader, tests, pattern):
from test import test_pdb 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 return tests

View file

@ -0,0 +1 @@
Add the optional backend of ``sys.monitoring`` to :mod:`bdb` and use it for :mod:`pdb`.