1"""
2Pdb debugger class.
3
4
5This is an extension to PDB which adds a number of new features.
6Note that there is also the `IPython.terminal.debugger` class which provides UI
7improvements.
8
9We also strongly recommend to use this via the `ipdb` package, which provides
10extra configuration options.
11
12Among other things, this subclass of PDB:
13 - supports many IPython magics like pdef/psource
14 - hide frames in tracebacks based on `__tracebackhide__`
15 - allows to skip frames based on `__debuggerskip__`
16
17
18Global Configuration
19--------------------
20
21The IPython debugger will by read the global ``~/.pdbrc`` file.
22That is to say you can list all commands supported by ipdb in your `~/.pdbrc`
23configuration file, to globally configure pdb.
24
25Example::
26
27 # ~/.pdbrc
28 skip_predicates debuggerskip false
29 skip_hidden false
30 context 25
31
32Features
33--------
34
35The IPython debugger can hide and skip frames when printing or moving through
36the stack. This can have a performance impact, so can be configures.
37
38The skipping and hiding frames are configurable via the `skip_predicates`
39command.
40
41By default, frames from readonly files will be hidden, frames containing
42``__tracebackhide__ = True`` will be hidden.
43
44Frames containing ``__debuggerskip__`` will be stepped over, frames whose parent
45frames value of ``__debuggerskip__`` is ``True`` will also be skipped.
46
47 >>> def helpers_helper():
48 ... pass
49 ...
50 ... def helper_1():
51 ... print("don't step in me")
52 ... helpers_helpers() # will be stepped over unless breakpoint set.
53 ...
54 ...
55 ... def helper_2():
56 ... print("in me neither")
57 ...
58
59One can define a decorator that wraps a function between the two helpers:
60
61 >>> def pdb_skipped_decorator(function):
62 ...
63 ...
64 ... def wrapped_fn(*args, **kwargs):
65 ... __debuggerskip__ = True
66 ... helper_1()
67 ... __debuggerskip__ = False
68 ... result = function(*args, **kwargs)
69 ... __debuggerskip__ = True
70 ... helper_2()
71 ... # setting __debuggerskip__ to False again is not necessary
72 ... return result
73 ...
74 ... return wrapped_fn
75
76When decorating a function, ipdb will directly step into ``bar()`` by
77default:
78
79 >>> @foo_decorator
80 ... def bar(x, y):
81 ... return x * y
82
83
84You can toggle the behavior with
85
86 ipdb> skip_predicates debuggerskip false
87
88or configure it in your ``.pdbrc``
89
90
91
92License
93-------
94
95Modified from the standard pdb.Pdb class to avoid including readline, so that
96the command line completion of other programs which include this isn't
97damaged.
98
99In the future, this class will be expanded with improvements over the standard
100pdb.
101
102The original code in this file is mainly lifted out of cmd.py in Python 2.2,
103with minor changes. Licensing should therefore be under the standard Python
104terms. For details on the PSF (Python Software Foundation) standard license,
105see:
106
107https://docs.python.org/2/license.html
108
109
110All the changes since then are under the same license as IPython.
111
112"""
113
114# *****************************************************************************
115#
116# This file is licensed under the PSF license.
117#
118# Copyright (C) 2001 Python Software Foundation, www.python.org
119# Copyright (C) 2005-2006 Fernando Perez. <fperez@colorado.edu>
120#
121#
122# *****************************************************************************
123
124from __future__ import annotations
125
126import inspect
127import linecache
128import os
129import re
130import sys
131import warnings
132from contextlib import contextmanager
133from functools import lru_cache
134
135from IPython import get_ipython
136from IPython.core.debugger_backport import PdbClosureBackport
137from IPython.utils import PyColorize
138from IPython.utils.PyColorize import TokenStream
139
140from typing import TYPE_CHECKING
141from types import FrameType
142
143# We have to check this directly from sys.argv, config struct not yet available
144from pdb import Pdb as _OldPdb
145from pygments.token import Token
146
147
148if sys.version_info < (3, 13):
149
150 class OldPdb(PdbClosureBackport, _OldPdb):
151 pass
152
153else:
154 OldPdb = _OldPdb
155
156if TYPE_CHECKING:
157 # otherwise circular import
158 from IPython.core.interactiveshell import InteractiveShell
159
160# skip module docstests
161__skip_doctest__ = True
162
163prompt = "ipdb> "
164
165
166# Allow the set_trace code to operate outside of an ipython instance, even if
167# it does so with some limitations. The rest of this support is implemented in
168# the Tracer constructor.
169
170DEBUGGERSKIP = "__debuggerskip__"
171
172
173# this has been implemented in Pdb in Python 3.13 (https://github.com/python/cpython/pull/106676
174# on lower python versions, we backported the feature.
175CHAIN_EXCEPTIONS = sys.version_info < (3, 13)
176
177
178def BdbQuit_excepthook(et, ev, tb, excepthook=None):
179 """Exception hook which handles `BdbQuit` exceptions.
180
181 All other exceptions are processed using the `excepthook`
182 parameter.
183 """
184 raise ValueError(
185 "`BdbQuit_excepthook` is deprecated since version 5.1. It is still around only because it is still imported by ipdb.",
186 )
187
188
189RGX_EXTRA_INDENT = re.compile(r"(?<=\n)\s+")
190
191
192def strip_indentation(multiline_string):
193 return RGX_EXTRA_INDENT.sub("", multiline_string)
194
195
196def decorate_fn_with_doc(new_fn, old_fn, additional_text=""):
197 """Make new_fn have old_fn's doc string. This is particularly useful
198 for the ``do_...`` commands that hook into the help system.
199 Adapted from from a comp.lang.python posting
200 by Duncan Booth."""
201
202 def wrapper(*args, **kw):
203 return new_fn(*args, **kw)
204
205 if old_fn.__doc__:
206 wrapper.__doc__ = strip_indentation(old_fn.__doc__) + additional_text
207 return wrapper
208
209
210class Pdb(OldPdb):
211 """Modified Pdb class, does not load readline.
212
213 for a standalone version that uses prompt_toolkit, see
214 `IPython.terminal.debugger.TerminalPdb` and
215 `IPython.terminal.debugger.set_trace()`
216
217
218 This debugger can hide and skip frames that are tagged according to some predicates.
219 See the `skip_predicates` commands.
220
221 """
222
223 shell: InteractiveShell
224 _theme_name: str
225 _context: int
226
227 _chained_exceptions: tuple[Exception, ...]
228 _chained_exception_index: int
229
230 if CHAIN_EXCEPTIONS:
231 MAX_CHAINED_EXCEPTION_DEPTH = 999
232
233 default_predicates = {
234 "tbhide": True,
235 "readonly": False,
236 "ipython_internal": True,
237 "debuggerskip": True,
238 }
239
240 def __init__(
241 self,
242 completekey=None,
243 stdin=None,
244 stdout=None,
245 context: int | None | str = 5,
246 *,
247 mode: str | None = None,
248 **kwargs,
249 ):
250 """Create a new IPython debugger.
251
252 Parameters
253 ----------
254 completekey : default None
255 Passed to pdb.Pdb.
256 stdin : default None
257 Passed to pdb.Pdb.
258 stdout : default None
259 Passed to pdb.Pdb.
260 context : int
261 Number of lines of source code context to show when
262 displaying stacktrace information.
263 mode : str, optional
264 How the debugger was invoked, one of ``'inline'`` (used by the
265 ``breakpoint()`` builtin), ``'cli'`` (used by the command line
266 invocation) or ``None`` (backwards compatible behaviour). This
267 argument was added to stdlib's ``pdb.Pdb`` in Python 3.14; it is
268 accepted on every supported Python version here but only forwarded
269 to the underlying ``pdb.Pdb`` when it is actually supported.
270 **kwargs
271 Passed to pdb.Pdb.
272
273 Notes
274 -----
275 The possibilities are python version dependent, see the python
276 docs for more info.
277 """
278 # ipdb issue, see https://github.com/ipython/ipython/issues/14811
279 if context is None:
280 context = 5
281 if isinstance(context, str):
282 context = int(context)
283 self.context = context
284
285 # The `mode` argument was added to `pdb.Pdb` in Python 3.14. We accept
286 # it on every supported Python version so that callers written against
287 # 3.14+ keep working, but only forward it to the underlying `pdb.Pdb`
288 # when it understands it.
289 if sys.version_info >= (3, 14):
290 kwargs["mode"] = mode
291 else:
292 self.mode = mode
293
294 # `kwargs` ensures full compatibility with stdlib's `pdb.Pdb`.
295 OldPdb.__init__(self, completekey, stdin, stdout, **kwargs)
296 # Python 3.15+ should define this, so no need to initialize
297 # this avoids some getattr(self, 'curframe')
298 if sys.version_info < (3, 15):
299 self.curframe = None
300
301 # IPython changes...
302 self.shell = get_ipython()
303
304 if self.shell is None:
305 save_main = sys.modules["__main__"]
306 # No IPython instance running, we must create one
307 from IPython.terminal.interactiveshell import TerminalInteractiveShell
308
309 self.shell = TerminalInteractiveShell.instance()
310 # needed by any code which calls __import__("__main__") after
311 # the debugger was entered. See also #9941.
312 sys.modules["__main__"] = save_main
313
314 self.aliases = {}
315
316 theme_name = self.shell.colors
317 assert isinstance(theme_name, str)
318 assert theme_name.lower() == theme_name
319
320 # Add a python parser so we can syntax highlight source while
321 # debugging.
322 self.parser = PyColorize.Parser(theme_name=theme_name)
323 self.set_theme_name(theme_name)
324
325 # Set the prompt - the default prompt is '(Pdb)'
326 self.prompt = prompt
327 self.skip_hidden = True
328 self.report_skipped = True
329
330 # list of predicates we use to skip frames
331 self._predicates = self.default_predicates
332
333 if CHAIN_EXCEPTIONS:
334 self._chained_exceptions = tuple()
335 self._chained_exception_index = 0
336
337 @property
338 def context(self) -> int:
339 return self._context
340
341 @context.setter
342 def context(self, value: int | str) -> None:
343 # ipdb issue see https://github.com/ipython/ipython/issues/14811
344 if not isinstance(value, int):
345 value = int(value)
346 assert isinstance(value, int)
347 assert value >= 0
348 self._context = value
349
350 def set_theme_name(self, name):
351 assert name.lower() == name
352 assert isinstance(name, str)
353 self._theme_name = name
354 self.parser.theme_name = name
355
356 @property
357 def theme(self):
358 return PyColorize.theme_table[self._theme_name]
359
360 #
361 def set_colors(self, scheme):
362 """Shorthand access to the color table scheme selector method."""
363 warnings.warn(
364 "set_colors is deprecated since IPython 9.0, use set_theme_name instead",
365 DeprecationWarning,
366 stacklevel=2,
367 )
368 assert scheme == scheme.lower()
369 self._theme_name = scheme.lower()
370 self.parser.theme_name = scheme.lower()
371
372 def set_trace(self, frame=None, **kwargs):
373 if frame is None:
374 frame = sys._getframe().f_back
375 self.initial_frame = frame
376 return super().set_trace(frame, **kwargs)
377
378 def get_stack(self, *args, **kwargs):
379 stack, pos = super().get_stack(*args, **kwargs)
380 if len(stack) >= 0 and self._is_internal_frame(stack[0][0]):
381 stack.pop(0)
382 pos -= 1
383 return stack, pos
384
385 def _is_internal_frame(self, frame):
386 """Determine if this frame should be skipped as internal"""
387 filename = frame.f_code.co_filename
388
389 # Skip bdb.py runcall and internal operations
390 if filename.endswith("bdb.py"):
391 func_name = frame.f_code.co_name
392 # Skip internal bdb operations but allow breakpoint hits
393 if func_name in ("runcall", "run", "runeval"):
394 return True
395
396 return False
397
398 def _hidden_predicate(self, frame):
399 """
400 Given a frame return whether it it should be hidden or not by IPython.
401 """
402
403 if self._predicates["readonly"]:
404 fname = frame.f_code.co_filename
405 # we need to check for file existence and interactively define
406 # function would otherwise appear as RO.
407 if os.path.isfile(fname) and not os.access(fname, os.W_OK):
408 return True
409
410 if self._predicates["tbhide"]:
411 if frame in (self.curframe, getattr(self, "initial_frame", None)):
412 return False
413 frame_locals = self._get_frame_locals(frame)
414 if "__tracebackhide__" not in frame_locals:
415 return False
416 return frame_locals["__tracebackhide__"]
417 return False
418
419 def hidden_frames(self, stack):
420 """
421 Given an index in the stack return whether it should be skipped.
422
423 This is used in up/down and where to skip frames.
424 """
425 # The f_locals dictionary is updated from the actual frame
426 # locals whenever the .f_locals accessor is called, so we
427 # avoid calling it here to preserve self.curframe_locals.
428 # Furthermore, there is no good reason to hide the current frame.
429 ip_hide = [self._hidden_predicate(s[0]) for s in stack]
430 ip_start = [i for i, s in enumerate(ip_hide) if s == "__ipython_bottom__"]
431 if ip_start and self._predicates["ipython_internal"]:
432 ip_hide = [h if i > ip_start[0] else True for (i, h) in enumerate(ip_hide)]
433 return ip_hide
434
435 if CHAIN_EXCEPTIONS:
436
437 def _get_tb_and_exceptions(self, tb_or_exc):
438 """
439 Given a tracecack or an exception, return a tuple of chained exceptions
440 and current traceback to inspect.
441 This will deal with selecting the right ``__cause__`` or ``__context__``
442 as well as handling cycles, and return a flattened list of exceptions we
443 can jump to with do_exceptions.
444 """
445 _exceptions = []
446 if isinstance(tb_or_exc, BaseException):
447 traceback, current = tb_or_exc.__traceback__, tb_or_exc
448
449 while current is not None:
450 if current in _exceptions:
451 break
452 _exceptions.append(current)
453 if current.__cause__ is not None:
454 current = current.__cause__
455 elif (
456 current.__context__ is not None
457 and not current.__suppress_context__
458 ):
459 current = current.__context__
460
461 if len(_exceptions) >= self.MAX_CHAINED_EXCEPTION_DEPTH:
462 self.message(
463 f"More than {self.MAX_CHAINED_EXCEPTION_DEPTH}"
464 " chained exceptions found, not all exceptions"
465 " will be browsable with `exceptions`."
466 )
467 break
468 else:
469 traceback = tb_or_exc
470 return tuple(reversed(_exceptions)), traceback
471
472 @contextmanager
473 def _hold_exceptions(self, exceptions):
474 """
475 Context manager to ensure proper cleaning of exceptions references
476 When given a chained exception instead of a traceback,
477 pdb may hold references to many objects which may leak memory.
478 We use this context manager to make sure everything is properly cleaned
479 """
480 try:
481 self._chained_exceptions = exceptions
482 self._chained_exception_index = len(exceptions) - 1
483 yield
484 finally:
485 # we can't put those in forget as otherwise they would
486 # be cleared on exception change
487 self._chained_exceptions = tuple()
488 self._chained_exception_index = 0
489
490 def do_exceptions(self, arg):
491 """exceptions [number]
492 List or change current exception in an exception chain.
493 Without arguments, list all the current exception in the exception
494 chain. Exceptions will be numbered, with the current exception indicated
495 with an arrow.
496 If given an integer as argument, switch to the exception at that index.
497 ``exception`` can be used as an alias for this command.
498 """
499 if not self._chained_exceptions:
500 self.message(
501 "Did not find chained exceptions. To move between"
502 " exceptions, pdb/post_mortem must be given an exception"
503 " object rather than a traceback."
504 )
505 return
506 if not arg:
507 for ix, exc in enumerate(self._chained_exceptions):
508 prompt = ">" if ix == self._chained_exception_index else " "
509 rep = repr(exc)
510 if len(rep) > 80:
511 rep = rep[:77] + "..."
512 indicator = (
513 " -"
514 if self._chained_exceptions[ix].__traceback__ is None
515 else f"{ix:>3}"
516 )
517 self.message(f"{prompt} {indicator} {rep}")
518 else:
519 try:
520 number = int(arg)
521 except ValueError:
522 self.error("Argument must be an integer")
523 return
524 if 0 <= number < len(self._chained_exceptions):
525 if self._chained_exceptions[number].__traceback__ is None:
526 self.error(
527 "This exception does not have a traceback, cannot jump to it"
528 )
529 return
530
531 self._chained_exception_index = number
532 self.setup(None, self._chained_exceptions[number].__traceback__)
533 self.print_stack_entry(self.stack[self.curindex])
534 else:
535 self.error("No exception with that number")
536
537 def do_exception(self, arg):
538 """exception [number]
539 Alias for the ``exceptions`` command.
540 """
541 return self.do_exceptions(arg)
542
543 def _cmdloop(self):
544 # Override to bypass Python 3.15's _maybe_use_pyrepl_as_stdin(), which
545 # sets use_rawinput=False and conflicts with IPython's own input handling.
546 while True:
547 try:
548 self.allow_kbdint = True
549 self.cmdloop()
550 self.allow_kbdint = False
551 break
552 except KeyboardInterrupt:
553 self.message("--KeyboardInterrupt--")
554
555 def interaction(self, frame, tb_or_exc):
556 try:
557 if CHAIN_EXCEPTIONS:
558 # this context manager is part of interaction in 3.13
559 _chained_exceptions, tb = self._get_tb_and_exceptions(tb_or_exc)
560 if isinstance(tb_or_exc, BaseException):
561 assert tb is not None, "main exception must have a traceback"
562 with self._hold_exceptions(_chained_exceptions):
563 OldPdb.interaction(self, frame, tb)
564 else:
565 OldPdb.interaction(self, frame, tb_or_exc)
566
567 except KeyboardInterrupt:
568 self.stdout.write("\n" + self.shell.get_exception_only())
569
570 def precmd(self, line):
571 """Perform useful escapes on the command before it is executed."""
572
573 if line.endswith("??"):
574 line = "pinfo2 " + line[:-2]
575 elif line.endswith("?"):
576 line = "pinfo " + line[:-1]
577
578 line = super().precmd(line)
579
580 return line
581
582 def new_do_quit(self, arg):
583 return OldPdb.do_quit(self, arg)
584
585 do_q = do_quit = decorate_fn_with_doc(new_do_quit, OldPdb.do_quit)
586
587 def print_stack_trace(self, context: int | None = None):
588 if context is None:
589 context = self.context
590 try:
591 skipped = 0
592 to_print = ""
593 for hidden, frame_lineno in zip(self.hidden_frames(self.stack), self.stack):
594 if hidden and self.skip_hidden:
595 skipped += 1
596 continue
597 if skipped:
598 to_print += self.theme.format(
599 [
600 (
601 Token.ExcName,
602 f" [... skipping {skipped} hidden frame(s)]",
603 ),
604 (Token, "\n"),
605 ]
606 )
607
608 skipped = 0
609 to_print += self.format_stack_entry(frame_lineno)
610 if skipped:
611 to_print += self.theme.format(
612 [
613 (
614 Token.ExcName,
615 f" [... skipping {skipped} hidden frame(s)]",
616 ),
617 (Token, "\n"),
618 ]
619 )
620 print(to_print, file=self.stdout)
621 except KeyboardInterrupt:
622 pass
623
624 def print_stack_entry(
625 self, frame_lineno: tuple[FrameType, int], prompt_prefix: str = "\n-> "
626 ) -> None:
627 """
628 Overwrite print_stack_entry from superclass (PDB)
629 """
630 print(self.format_stack_entry(frame_lineno, ""), file=self.stdout)
631
632 frame, lineno = frame_lineno
633 filename = frame.f_code.co_filename
634 self.shell.hooks.synchronize_with_editor(filename, lineno, 0)
635
636 def _pdbcmd_print_frame_status(self, arg):
637 """Use print_stack_entry to print frames in Python 3.14+."""
638 if sys.version_info[:2] >= (3, 14):
639 # This is the only line changed from the base class.
640 self.print_stack_entry(self.stack[self.curindex])
641
642 # Same as in 3.14
643 self._validate_file_mtime()
644 self._show_display()
645 else:
646 # 3.13 and 3.12 don't need any changes.
647 super()._pdbcmd_print_frame_status(arg) # type: ignore[misc]
648
649 def _get_frame_locals(self, frame):
650 """ "
651 Accessing f_local of current frame reset the namespace, so we want to avoid
652 that or the following can happen
653
654 ipdb> foo
655 "old"
656 ipdb> foo = "new"
657 ipdb> foo
658 "new"
659 ipdb> where
660 ipdb> foo
661 "old"
662
663 So if frame is self.current_frame we instead return self.curframe_locals
664
665 """
666 if frame is self.curframe:
667 return self.curframe_locals
668 else:
669 return frame.f_locals
670
671 def format_stack_entry(
672 self,
673 frame_lineno: tuple[FrameType, int], # type: ignore[override] # stubs are wrong
674 lprefix: str = ": ",
675 ) -> str:
676 """
677 overwrite from super class so must -> str
678 """
679 context = self.context
680 try:
681 context = int(context)
682 if context <= 0:
683 print("Context must be a positive integer", file=self.stdout)
684 except (TypeError, ValueError):
685 print("Context must be a positive integer", file=self.stdout)
686
687 import reprlib
688
689 ret_tok = []
690
691 frame, lineno = frame_lineno
692
693 return_value = ""
694 loc_frame = self._get_frame_locals(frame)
695 if "__return__" in loc_frame:
696 rv = loc_frame["__return__"]
697 # return_value += '->'
698 return_value += reprlib.repr(rv) + "\n"
699 ret_tok.extend([(Token, return_value)])
700
701 # s = filename + '(' + `lineno` + ')'
702 filename = self.canonic(frame.f_code.co_filename)
703 link_tok = (Token.FilenameEm, filename)
704
705 if frame.f_code.co_name:
706 func = frame.f_code.co_name
707 else:
708 func = "<lambda>"
709
710 call_toks = []
711 if func != "?":
712 if "__args__" in loc_frame:
713 args = reprlib.repr(loc_frame["__args__"])
714 else:
715 args = "()"
716 call_toks = [(Token.VName, func), (Token.ValEm, args)]
717
718 # The level info should be generated in the same format pdb uses, to
719 # avoid breaking the pdbtrack functionality of python-mode in *emacs.
720 if frame is self.curframe:
721 ret_tok.append((Token.CurrentFrame, self.theme.make_arrow(2)))
722 else:
723 ret_tok.append((Token, " "))
724
725 ret_tok.extend(
726 [
727 link_tok,
728 (Token, "("),
729 (Token.Lineno, str(lineno)),
730 (Token, ")"),
731 *call_toks,
732 (Token, "\n"),
733 ]
734 )
735
736 start = lineno - 1 - context // 2
737 lines = linecache.getlines(filename)
738 start = min(start, len(lines) - context)
739 start = max(start, 0)
740 lines = lines[start : start + context]
741
742 for i, line in enumerate(lines):
743 show_arrow = start + 1 + i == lineno
744
745 bp, num, colored_line = self.__line_content(
746 filename,
747 start + 1 + i,
748 line,
749 arrow=show_arrow,
750 )
751 if frame is self.curframe or show_arrow:
752 rlt = [
753 bp,
754 (Token.LinenoEm, num),
755 (Token, " "),
756 # TODO: investigate Toke.Line here, likely LineEm,
757 # Token is problematic here as line is already colored, a
758 # and this changes the full style of the colored line.
759 # ideally, __line_content returns the token and we modify the style.
760 (Token, colored_line),
761 ]
762 else:
763 rlt = [
764 bp,
765 (Token.Lineno, num),
766 (Token, " "),
767 # TODO: investigate Toke.Line here, likely Line
768 # Token is problematic here as line is already colored, a
769 # and this changes the full style of the colored line.
770 # ideally, __line_content returns the token and we modify the style.
771 (Token.Line, colored_line),
772 ]
773 ret_tok.extend(rlt)
774
775 return self.theme.format(ret_tok)
776
777 def __line_content(
778 self, filename: str, lineno: int, line: str, arrow: bool = False
779 ):
780 bp_mark = ""
781 BreakpointToken = Token.Breakpoint
782
783 new_line, err = self.parser.format2(line, "str")
784 if not err:
785 assert new_line is not None
786 line = new_line
787
788 bp = None
789 if lineno in self.get_file_breaks(filename):
790 bps = self.get_breaks(filename, lineno)
791 bp = bps[-1]
792
793 if bp:
794 bp_mark = str(bp.number)
795 BreakpointToken = Token.Breakpoint.Enabled
796 if not bp.enabled:
797 BreakpointToken = Token.Breakpoint.Disabled
798 numbers_width = 7
799 if arrow:
800 # This is the line with the error
801 pad = numbers_width - len(str(lineno)) - len(bp_mark)
802 num = "%s%s" % (self.theme.make_arrow(pad), str(lineno))
803 else:
804 num = "%*s" % (numbers_width - len(bp_mark), str(lineno))
805 bp_str = (BreakpointToken, bp_mark)
806 return (bp_str, num, line)
807
808 def print_list_lines(self, filename: str, first: int, last: int) -> None:
809 """The printing (as opposed to the parsing part of a 'list'
810 command."""
811 toks: TokenStream = []
812 try:
813 if filename == "<string>" and hasattr(self, "_exec_filename"):
814 filename = self._exec_filename
815
816 for lineno in range(first, last + 1):
817 line = linecache.getline(filename, lineno)
818 if not line:
819 break
820
821 assert self.curframe is not None
822
823 if lineno == self.curframe.f_lineno:
824 bp, num, colored_line = self.__line_content(
825 filename, lineno, line, arrow=True
826 )
827 toks.extend(
828 [
829 bp,
830 (Token.LinenoEm, num),
831 (Token, " "),
832 # TODO: investigate Token.Line here
833 (Token, colored_line),
834 ]
835 )
836 else:
837 bp, num, colored_line = self.__line_content(
838 filename, lineno, line, arrow=False
839 )
840 toks.extend(
841 [
842 bp,
843 (Token.Lineno, num),
844 (Token, " "),
845 (Token, colored_line),
846 ]
847 )
848
849 self.lineno = lineno
850
851 print(self.theme.format(toks), file=self.stdout)
852
853 except KeyboardInterrupt:
854 pass
855
856 def do_skip_predicates(self, args):
857 """
858 Turn on/off individual predicates as to whether a frame should be hidden/skip.
859
860 The global option to skip (or not) hidden frames is set with skip_hidden
861
862 To change the value of a predicate
863
864 skip_predicates key [true|false]
865
866 Call without arguments to see the current values.
867
868 To permanently change the value of an option add the corresponding
869 command to your ``~/.pdbrc`` file. If you are programmatically using the
870 Pdb instance you can also change the ``default_predicates`` class
871 attribute.
872 """
873 if not args.strip():
874 print("current predicates:")
875 for p, v in self._predicates.items():
876 print(" ", p, ":", v)
877 return
878 type_value = args.strip().split(" ")
879 if len(type_value) != 2:
880 print(
881 f"Usage: skip_predicates <type> <value>, with <type> one of {set(self._predicates.keys())}"
882 )
883 return
884
885 type_, value = type_value
886 if type_ not in self._predicates:
887 print(f"{type_!r} not in {set(self._predicates.keys())}")
888 return
889 if value.lower() not in ("true", "yes", "1", "no", "false", "0"):
890 print(
891 f"{value!r} is invalid - use one of ('true', 'yes', '1', 'no', 'false', '0')"
892 )
893 return
894
895 self._predicates[type_] = value.lower() in ("true", "yes", "1")
896 if not any(self._predicates.values()):
897 print(
898 "Warning, all predicates set to False, skip_hidden may not have any effects."
899 )
900
901 def do_skip_hidden(self, arg):
902 """
903 Change whether or not we should skip frames with the
904 __tracebackhide__ attribute.
905 """
906 if not arg.strip():
907 print(
908 f"skip_hidden = {self.skip_hidden}, use 'yes','no', 'true', or 'false' to change."
909 )
910 elif arg.strip().lower() in ("true", "yes"):
911 self.skip_hidden = True
912 elif arg.strip().lower() in ("false", "no"):
913 self.skip_hidden = False
914 if not any(self._predicates.values()):
915 print(
916 "Warning, all predicates set to False, skip_hidden may not have any effects."
917 )
918
919 def do_list(self, arg):
920 """Print lines of code from the current stack frame"""
921 self.lastcmd = "list"
922 last = None
923 if arg and arg != ".":
924 try:
925 x = eval(arg, {}, {})
926 if type(x) == type(()):
927 first, last = x # type: ignore[misc]
928 first = int(first) # type: ignore[call-overload]
929 last = int(last) # type: ignore[call-overload]
930 if last < first:
931 # Assume it's a count
932 last = first + last
933 else:
934 first = max(1, int(x) - 5)
935 except:
936 print("*** Error in argument:", repr(arg), file=self.stdout)
937 return
938 elif self.lineno is None or arg == ".":
939 assert self.curframe is not None
940 first = max(1, self.curframe.f_lineno - 5)
941 else:
942 first = self.lineno + 1
943 if last is None:
944 last = first + 10
945 assert self.curframe is not None
946 self.print_list_lines(self.curframe.f_code.co_filename, first, last)
947
948 lineno = first
949 filename = self.curframe.f_code.co_filename
950 self.shell.hooks.synchronize_with_editor(filename, lineno, 0)
951
952 do_l = do_list
953
954 def getsourcelines(self, obj):
955 lines, lineno = inspect.findsource(obj)
956 if inspect.isframe(obj) and obj.f_globals is self._get_frame_locals(obj):
957 # must be a module frame: do not try to cut a block out of it
958 return lines, 1
959 elif inspect.ismodule(obj):
960 return lines, 1
961 return inspect.getblock(lines[lineno:]), lineno + 1
962
963 def do_longlist(self, arg):
964 """Print lines of code from the current stack frame.
965
966 Shows more lines than 'list' does.
967 """
968 self.lastcmd = "longlist"
969 try:
970 lines, lineno = self.getsourcelines(self.curframe)
971 except OSError as err:
972 self.error(str(err))
973 return
974 last = lineno + len(lines)
975 assert self.curframe is not None
976 self.print_list_lines(self.curframe.f_code.co_filename, lineno, last)
977
978 do_ll = do_longlist
979
980 def do_debug(self, arg):
981 """debug code
982 Enter a recursive debugger that steps through the code
983 argument (which is an arbitrary expression or statement to be
984 executed in the current environment).
985 """
986 trace_function = sys.gettrace()
987 sys.settrace(None)
988 assert self.curframe is not None
989 globals = self.curframe.f_globals
990 locals = self.curframe_locals
991 p = self.__class__(
992 completekey=self.completekey, stdin=self.stdin, stdout=self.stdout
993 )
994 p.use_rawinput = self.use_rawinput
995 p.prompt = "(%s) " % self.prompt.strip()
996 self.message("ENTERING RECURSIVE DEBUGGER")
997 sys.call_tracing(p.run, (arg, globals, locals))
998 self.message("LEAVING RECURSIVE DEBUGGER")
999 sys.settrace(trace_function)
1000 self.lastcmd = p.lastcmd
1001
1002 def do_pdef(self, arg):
1003 """Print the call signature for any callable object.
1004
1005 The debugger interface to %pdef"""
1006 assert self.curframe is not None
1007 namespaces = [
1008 ("Locals", self.curframe_locals),
1009 ("Globals", self.curframe.f_globals),
1010 ]
1011 self.shell.find_line_magic("pdef")(arg, namespaces=namespaces)
1012
1013 def do_pdoc(self, arg):
1014 """Print the docstring for an object.
1015
1016 The debugger interface to %pdoc."""
1017 assert self.curframe is not None
1018 namespaces = [
1019 ("Locals", self.curframe_locals),
1020 ("Globals", self.curframe.f_globals),
1021 ]
1022 self.shell.find_line_magic("pdoc")(arg, namespaces=namespaces)
1023
1024 def do_pfile(self, arg):
1025 """Print (or run through pager) the file where an object is defined.
1026
1027 The debugger interface to %pfile.
1028 """
1029 assert self.curframe is not None
1030 namespaces = [
1031 ("Locals", self.curframe_locals),
1032 ("Globals", self.curframe.f_globals),
1033 ]
1034 self.shell.find_line_magic("pfile")(arg, namespaces=namespaces)
1035
1036 def do_pinfo(self, arg):
1037 """Provide detailed information about an object.
1038
1039 The debugger interface to %pinfo, i.e., obj?."""
1040 assert self.curframe is not None
1041 namespaces = [
1042 ("Locals", self.curframe_locals),
1043 ("Globals", self.curframe.f_globals),
1044 ]
1045 self.shell.find_line_magic("pinfo")(arg, namespaces=namespaces)
1046
1047 def do_pinfo2(self, arg):
1048 """Provide extra detailed information about an object.
1049
1050 The debugger interface to %pinfo2, i.e., obj??."""
1051 assert self.curframe is not None
1052 namespaces = [
1053 ("Locals", self.curframe_locals),
1054 ("Globals", self.curframe.f_globals),
1055 ]
1056 self.shell.find_line_magic("pinfo2")(arg, namespaces=namespaces)
1057
1058 def do_psource(self, arg):
1059 """Print (or run through pager) the source code for an object."""
1060 assert self.curframe is not None
1061 namespaces = [
1062 ("Locals", self.curframe_locals),
1063 ("Globals", self.curframe.f_globals),
1064 ]
1065 self.shell.find_line_magic("psource")(arg, namespaces=namespaces)
1066
1067 def do_where(self, arg: str):
1068 """w(here)
1069 Print a stack trace, with the most recent frame at the bottom.
1070 An arrow indicates the "current frame", which determines the
1071 context of most commands. 'bt' is an alias for this command.
1072
1073 Take a number as argument as an (optional) number of context line to
1074 print"""
1075 if arg:
1076 try:
1077 context = int(arg)
1078 except ValueError as err:
1079 self.error(str(err))
1080 return
1081 self.print_stack_trace(context)
1082 else:
1083 self.print_stack_trace()
1084
1085 do_w = do_where
1086
1087 def break_anywhere(self, frame):
1088 """
1089 _stop_in_decorator_internals is overly restrictive, as we may still want
1090 to trace function calls, so we need to also update break_anywhere so
1091 that is we don't `stop_here`, because of debugger skip, we may still
1092 stop at any point inside the function
1093
1094 """
1095
1096 sup = super().break_anywhere(frame)
1097 if sup:
1098 return sup
1099 if self._predicates["debuggerskip"]:
1100 if DEBUGGERSKIP in frame.f_code.co_varnames:
1101 return True
1102 if frame.f_back and self._get_frame_locals(frame.f_back).get(DEBUGGERSKIP):
1103 return True
1104 return False
1105
1106 def _is_in_decorator_internal_and_should_skip(self, frame):
1107 """
1108 Utility to tell us whether we are in a decorator internal and should stop.
1109
1110 """
1111 # if we are disabled don't skip
1112 if not self._predicates["debuggerskip"]:
1113 return False
1114
1115 return self._cachable_skip(frame)
1116
1117 @lru_cache(1024)
1118 def _cached_one_parent_frame_debuggerskip(self, frame):
1119 """
1120 Cache looking up for DEBUGGERSKIP on parent frame.
1121
1122 This should speedup walking through deep frame when one of the highest
1123 one does have a debugger skip.
1124
1125 This is likely to introduce fake positive though.
1126 """
1127 while getattr(frame, "f_back", None):
1128 frame = frame.f_back
1129 if self._get_frame_locals(frame).get(DEBUGGERSKIP):
1130 return True
1131 return None
1132
1133 @lru_cache(1024)
1134 def _cachable_skip(self, frame):
1135 # if frame is tagged, skip by default.
1136 if DEBUGGERSKIP in frame.f_code.co_varnames:
1137 return True
1138
1139 # if one of the parent frame value set to True skip as well.
1140 if self._cached_one_parent_frame_debuggerskip(frame):
1141 return True
1142
1143 return False
1144
1145 def stop_here(self, frame):
1146 if self._is_in_decorator_internal_and_should_skip(frame) is True:
1147 return False
1148
1149 hidden = False
1150 if self.skip_hidden:
1151 hidden = self._hidden_predicate(frame)
1152 if hidden:
1153 if self.report_skipped:
1154 print(
1155 self.theme.format(
1156 [
1157 (
1158 Token.ExcName,
1159 " [... skipped 1 hidden frame(s)]",
1160 ),
1161 (Token, "\n"),
1162 ]
1163 )
1164 )
1165 if self.skip and self.is_skipped_module(frame.f_globals.get("__name__", "")):
1166 print(
1167 self.theme.format(
1168 [
1169 (
1170 Token.ExcName,
1171 " [... skipped 1 ignored module(s)]",
1172 ),
1173 (Token, "\n"),
1174 ]
1175 )
1176 )
1177
1178 return False
1179
1180 return super().stop_here(frame)
1181
1182 def do_up(self, arg):
1183 """u(p) [count]
1184 Move the current frame count (default one) levels up in the
1185 stack trace (to an older frame).
1186
1187 Will skip hidden frames and ignored modules.
1188 """
1189 # modified version of upstream that skips
1190 # frames with __tracebackhide__ and ignored modules
1191 if self.curindex == 0:
1192 self.error("Oldest frame")
1193 return
1194 try:
1195 count = int(arg or 1)
1196 except ValueError:
1197 self.error("Invalid frame count (%s)" % arg)
1198 return
1199
1200 hidden_skipped = 0
1201 module_skipped = 0
1202
1203 if count < 0:
1204 _newframe = 0
1205 else:
1206 counter = 0
1207 hidden_frames = self.hidden_frames(self.stack)
1208
1209 for i in range(self.curindex - 1, -1, -1):
1210 should_skip_hidden = hidden_frames[i] and self.skip_hidden
1211 should_skip_module = self.skip and self.is_skipped_module(
1212 self.stack[i][0].f_globals.get("__name__", "")
1213 )
1214
1215 if should_skip_hidden or should_skip_module:
1216 if should_skip_hidden:
1217 hidden_skipped += 1
1218 if should_skip_module:
1219 module_skipped += 1
1220 continue
1221 counter += 1
1222 if counter >= count:
1223 break
1224 else:
1225 # if no break occurred.
1226 self.error(
1227 "all frames above skipped (hidden frames and ignored modules). Use `skip_hidden False` for hidden frames or unignore_module for ignored modules."
1228 )
1229 return
1230
1231 _newframe = i
1232 self._select_frame(_newframe)
1233
1234 total_skipped = hidden_skipped + module_skipped
1235 if total_skipped:
1236 print(
1237 self.theme.format(
1238 [
1239 (
1240 Token.ExcName,
1241 f" [... skipped {total_skipped} frame(s): {hidden_skipped} hidden frames + {module_skipped} ignored modules]",
1242 ),
1243 (Token, "\n"),
1244 ]
1245 )
1246 )
1247
1248 def do_down(self, arg):
1249 """d(own) [count]
1250 Move the current frame count (default one) levels down in the
1251 stack trace (to a newer frame).
1252
1253 Will skip hidden frames and ignored modules.
1254 """
1255 if self.curindex + 1 == len(self.stack):
1256 self.error("Newest frame")
1257 return
1258 try:
1259 count = int(arg or 1)
1260 except ValueError:
1261 self.error("Invalid frame count (%s)" % arg)
1262 return
1263 if count < 0:
1264 _newframe = len(self.stack) - 1
1265 else:
1266 counter = 0
1267 hidden_skipped = 0
1268 module_skipped = 0
1269 hidden_frames = self.hidden_frames(self.stack)
1270
1271 for i in range(self.curindex + 1, len(self.stack)):
1272 should_skip_hidden = hidden_frames[i] and self.skip_hidden
1273 should_skip_module = self.skip and self.is_skipped_module(
1274 self.stack[i][0].f_globals.get("__name__", "")
1275 )
1276
1277 if should_skip_hidden or should_skip_module:
1278 if should_skip_hidden:
1279 hidden_skipped += 1
1280 if should_skip_module:
1281 module_skipped += 1
1282 continue
1283 counter += 1
1284 if counter >= count:
1285 break
1286 else:
1287 self.error(
1288 "all frames below skipped (hidden frames and ignored modules). Use `skip_hidden False` for hidden frames or unignore_module for ignored modules."
1289 )
1290 return
1291
1292 total_skipped = hidden_skipped + module_skipped
1293 if total_skipped:
1294 print(
1295 self.theme.format(
1296 [
1297 (
1298 Token.ExcName,
1299 f" [... skipped {total_skipped} frame(s): {hidden_skipped} hidden frames + {module_skipped} ignored modules]",
1300 ),
1301 (Token, "\n"),
1302 ]
1303 )
1304 )
1305 _newframe = i
1306
1307 self._select_frame(_newframe)
1308
1309 do_d = do_down
1310 do_u = do_up
1311
1312 def _show_ignored_modules(self):
1313 """Display currently ignored modules."""
1314 if self.skip:
1315 print(f"Currently ignored modules: {sorted(self.skip)}")
1316 else:
1317 print("No modules are currently ignored.")
1318
1319 def do_ignore_module(self, arg):
1320 """ignore_module <module_name>
1321
1322 Add a module to the list of modules to skip when navigating frames.
1323 When a module is ignored, the debugger will automatically skip over
1324 frames from that module.
1325
1326 Supports wildcard patterns using fnmatch syntax:
1327
1328 Usage:
1329 ignore_module threading # Skip threading module frames
1330 ignore_module asyncio.\\* # Skip all asyncio submodules
1331 ignore_module \\*.tests # Skip all test modules
1332 ignore_module # List currently ignored modules
1333 """
1334
1335 if self.skip is None:
1336 self.skip = set()
1337
1338 module_name = arg.strip()
1339
1340 if not module_name:
1341 self._show_ignored_modules()
1342 return
1343
1344 self.skip.add(module_name)
1345
1346 def do_unignore_module(self, arg):
1347 """unignore_module <module_name>
1348
1349 Remove a module from the list of modules to skip when navigating frames.
1350 This will allow the debugger to step into frames from the specified module.
1351
1352 Usage:
1353 unignore_module threading # Stop ignoring threading module frames
1354 unignore_module asyncio.\\* # Remove asyncio.* pattern
1355 unignore_module # List currently ignored modules
1356 """
1357
1358 if self.skip is None:
1359 self.skip = set()
1360
1361 module_name = arg.strip()
1362
1363 if not module_name:
1364 self._show_ignored_modules()
1365 return
1366
1367 try:
1368 self.skip.remove(module_name)
1369 except KeyError:
1370 print(f"Module {module_name} is not currently ignored")
1371 self._show_ignored_modules()
1372
1373 def do_context(self, context: str):
1374 """context number_of_lines
1375 Set the number of lines of source code to show when displaying
1376 stacktrace information.
1377 """
1378 try:
1379 new_context = int(context)
1380 if new_context <= 0:
1381 raise ValueError()
1382 self.context = new_context
1383 except ValueError:
1384 self.error(
1385 f"The 'context' command requires a positive integer argument (current value {self.context})."
1386 )
1387
1388
1389class InterruptiblePdb(Pdb):
1390 """Version of debugger where KeyboardInterrupt exits the debugger altogether."""
1391
1392 def cmdloop(self, intro=None):
1393 """Wrap cmdloop() such that KeyboardInterrupt stops the debugger."""
1394 try:
1395 return OldPdb.cmdloop(self, intro=intro)
1396 except KeyboardInterrupt:
1397 self.stop_here = lambda frame: False # type: ignore[method-assign]
1398 self.do_quit("")
1399 sys.settrace(None)
1400 self.quitting = False
1401 raise
1402
1403 def _cmdloop(self):
1404 while True:
1405 try:
1406 # keyboard interrupts allow for an easy way to cancel
1407 # the current command, so allow them during interactive input
1408 self.allow_kbdint = True
1409 self.cmdloop()
1410 self.allow_kbdint = False
1411 break
1412 except KeyboardInterrupt:
1413 self.message("--KeyboardInterrupt--")
1414 raise
1415
1416
1417def set_trace(frame=None, header=None):
1418 """
1419 Start debugging from `frame`.
1420
1421 If frame is not specified, debugging starts from caller's frame.
1422 """
1423 pdb = Pdb()
1424 if header is not None:
1425 pdb.message(header)
1426 pdb.set_trace(frame or sys._getframe().f_back)