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 **kwargs,
247 ):
248 """Create a new IPython debugger.
249
250 Parameters
251 ----------
252 completekey : default None
253 Passed to pdb.Pdb.
254 stdin : default None
255 Passed to pdb.Pdb.
256 stdout : default None
257 Passed to pdb.Pdb.
258 context : int
259 Number of lines of source code context to show when
260 displaying stacktrace information.
261 **kwargs
262 Passed to pdb.Pdb.
263
264 Notes
265 -----
266 The possibilities are python version dependent, see the python
267 docs for more info.
268 """
269 # ipdb issue, see https://github.com/ipython/ipython/issues/14811
270 if context is None:
271 context = 5
272 if isinstance(context, str):
273 context = int(context)
274 self.context = context
275
276 # `kwargs` ensures full compatibility with stdlib's `pdb.Pdb`.
277 OldPdb.__init__(self, completekey, stdin, stdout, **kwargs)
278 # Python 3.15+ should define this, so no need to initialize
279 # this avoids some getattr(self, 'curframe')
280 if sys.version_info < (3, 15):
281 self.curframe = None
282
283 # IPython changes...
284 self.shell = get_ipython()
285
286 if self.shell is None:
287 save_main = sys.modules["__main__"]
288 # No IPython instance running, we must create one
289 from IPython.terminal.interactiveshell import TerminalInteractiveShell
290
291 self.shell = TerminalInteractiveShell.instance()
292 # needed by any code which calls __import__("__main__") after
293 # the debugger was entered. See also #9941.
294 sys.modules["__main__"] = save_main
295
296 self.aliases = {}
297
298 theme_name = self.shell.colors
299 assert isinstance(theme_name, str)
300 assert theme_name.lower() == theme_name
301
302 # Add a python parser so we can syntax highlight source while
303 # debugging.
304 self.parser = PyColorize.Parser(theme_name=theme_name)
305 self.set_theme_name(theme_name)
306
307 # Set the prompt - the default prompt is '(Pdb)'
308 self.prompt = prompt
309 self.skip_hidden = True
310 self.report_skipped = True
311
312 # list of predicates we use to skip frames
313 self._predicates = self.default_predicates
314
315 if CHAIN_EXCEPTIONS:
316 self._chained_exceptions = tuple()
317 self._chained_exception_index = 0
318
319 @property
320 def context(self) -> int:
321 return self._context
322
323 @context.setter
324 def context(self, value: int | str) -> None:
325 # ipdb issue see https://github.com/ipython/ipython/issues/14811
326 if not isinstance(value, int):
327 value = int(value)
328 assert isinstance(value, int)
329 assert value >= 0
330 self._context = value
331
332 def set_theme_name(self, name):
333 assert name.lower() == name
334 assert isinstance(name, str)
335 self._theme_name = name
336 self.parser.theme_name = name
337
338 @property
339 def theme(self):
340 return PyColorize.theme_table[self._theme_name]
341
342 #
343 def set_colors(self, scheme):
344 """Shorthand access to the color table scheme selector method."""
345 warnings.warn(
346 "set_colors is deprecated since IPython 9.0, use set_theme_name instead",
347 DeprecationWarning,
348 stacklevel=2,
349 )
350 assert scheme == scheme.lower()
351 self._theme_name = scheme.lower()
352 self.parser.theme_name = scheme.lower()
353
354 def set_trace(self, frame=None):
355 if frame is None:
356 frame = sys._getframe().f_back
357 self.initial_frame = frame
358 return super().set_trace(frame)
359
360 def get_stack(self, *args, **kwargs):
361 stack, pos = super().get_stack(*args, **kwargs)
362 if len(stack) >= 0 and self._is_internal_frame(stack[0][0]):
363 stack.pop(0)
364 pos -= 1
365 return stack, pos
366
367 def _is_internal_frame(self, frame):
368 """Determine if this frame should be skipped as internal"""
369 filename = frame.f_code.co_filename
370
371 # Skip bdb.py runcall and internal operations
372 if filename.endswith("bdb.py"):
373 func_name = frame.f_code.co_name
374 # Skip internal bdb operations but allow breakpoint hits
375 if func_name in ("runcall", "run", "runeval"):
376 return True
377
378 return False
379
380 def _hidden_predicate(self, frame):
381 """
382 Given a frame return whether it it should be hidden or not by IPython.
383 """
384
385 if self._predicates["readonly"]:
386 fname = frame.f_code.co_filename
387 # we need to check for file existence and interactively define
388 # function would otherwise appear as RO.
389 if os.path.isfile(fname) and not os.access(fname, os.W_OK):
390 return True
391
392 if self._predicates["tbhide"]:
393 if frame in (self.curframe, getattr(self, "initial_frame", None)):
394 return False
395 frame_locals = self._get_frame_locals(frame)
396 if "__tracebackhide__" not in frame_locals:
397 return False
398 return frame_locals["__tracebackhide__"]
399 return False
400
401 def hidden_frames(self, stack):
402 """
403 Given an index in the stack return whether it should be skipped.
404
405 This is used in up/down and where to skip frames.
406 """
407 # The f_locals dictionary is updated from the actual frame
408 # locals whenever the .f_locals accessor is called, so we
409 # avoid calling it here to preserve self.curframe_locals.
410 # Furthermore, there is no good reason to hide the current frame.
411 ip_hide = [self._hidden_predicate(s[0]) for s in stack]
412 ip_start = [i for i, s in enumerate(ip_hide) if s == "__ipython_bottom__"]
413 if ip_start and self._predicates["ipython_internal"]:
414 ip_hide = [h if i > ip_start[0] else True for (i, h) in enumerate(ip_hide)]
415 return ip_hide
416
417 if CHAIN_EXCEPTIONS:
418
419 def _get_tb_and_exceptions(self, tb_or_exc):
420 """
421 Given a tracecack or an exception, return a tuple of chained exceptions
422 and current traceback to inspect.
423 This will deal with selecting the right ``__cause__`` or ``__context__``
424 as well as handling cycles, and return a flattened list of exceptions we
425 can jump to with do_exceptions.
426 """
427 _exceptions = []
428 if isinstance(tb_or_exc, BaseException):
429 traceback, current = tb_or_exc.__traceback__, tb_or_exc
430
431 while current is not None:
432 if current in _exceptions:
433 break
434 _exceptions.append(current)
435 if current.__cause__ is not None:
436 current = current.__cause__
437 elif (
438 current.__context__ is not None
439 and not current.__suppress_context__
440 ):
441 current = current.__context__
442
443 if len(_exceptions) >= self.MAX_CHAINED_EXCEPTION_DEPTH:
444 self.message(
445 f"More than {self.MAX_CHAINED_EXCEPTION_DEPTH}"
446 " chained exceptions found, not all exceptions"
447 "will be browsable with `exceptions`."
448 )
449 break
450 else:
451 traceback = tb_or_exc
452 return tuple(reversed(_exceptions)), traceback
453
454 @contextmanager
455 def _hold_exceptions(self, exceptions):
456 """
457 Context manager to ensure proper cleaning of exceptions references
458 When given a chained exception instead of a traceback,
459 pdb may hold references to many objects which may leak memory.
460 We use this context manager to make sure everything is properly cleaned
461 """
462 try:
463 self._chained_exceptions = exceptions
464 self._chained_exception_index = len(exceptions) - 1
465 yield
466 finally:
467 # we can't put those in forget as otherwise they would
468 # be cleared on exception change
469 self._chained_exceptions = tuple()
470 self._chained_exception_index = 0
471
472 def do_exceptions(self, arg):
473 """exceptions [number]
474 List or change current exception in an exception chain.
475 Without arguments, list all the current exception in the exception
476 chain. Exceptions will be numbered, with the current exception indicated
477 with an arrow.
478 If given an integer as argument, switch to the exception at that index.
479 """
480 if not self._chained_exceptions:
481 self.message(
482 "Did not find chained exceptions. To move between"
483 " exceptions, pdb/post_mortem must be given an exception"
484 " object rather than a traceback."
485 )
486 return
487 if not arg:
488 for ix, exc in enumerate(self._chained_exceptions):
489 prompt = ">" if ix == self._chained_exception_index else " "
490 rep = repr(exc)
491 if len(rep) > 80:
492 rep = rep[:77] + "..."
493 indicator = (
494 " -"
495 if self._chained_exceptions[ix].__traceback__ is None
496 else f"{ix:>3}"
497 )
498 self.message(f"{prompt} {indicator} {rep}")
499 else:
500 try:
501 number = int(arg)
502 except ValueError:
503 self.error("Argument must be an integer")
504 return
505 if 0 <= number < len(self._chained_exceptions):
506 if self._chained_exceptions[number].__traceback__ is None:
507 self.error(
508 "This exception does not have a traceback, cannot jump to it"
509 )
510 return
511
512 self._chained_exception_index = number
513 self.setup(None, self._chained_exceptions[number].__traceback__)
514 self.print_stack_entry(self.stack[self.curindex])
515 else:
516 self.error("No exception with that number")
517
518 def interaction(self, frame, tb_or_exc):
519 try:
520 if CHAIN_EXCEPTIONS:
521 # this context manager is part of interaction in 3.13
522 _chained_exceptions, tb = self._get_tb_and_exceptions(tb_or_exc)
523 if isinstance(tb_or_exc, BaseException):
524 assert tb is not None, "main exception must have a traceback"
525 with self._hold_exceptions(_chained_exceptions):
526 OldPdb.interaction(self, frame, tb)
527 else:
528 OldPdb.interaction(self, frame, tb_or_exc)
529
530 except KeyboardInterrupt:
531 self.stdout.write("\n" + self.shell.get_exception_only())
532
533 def precmd(self, line):
534 """Perform useful escapes on the command before it is executed."""
535
536 if line.endswith("??"):
537 line = "pinfo2 " + line[:-2]
538 elif line.endswith("?"):
539 line = "pinfo " + line[:-1]
540
541 line = super().precmd(line)
542
543 return line
544
545 def new_do_quit(self, arg):
546 return OldPdb.do_quit(self, arg)
547
548 do_q = do_quit = decorate_fn_with_doc(new_do_quit, OldPdb.do_quit)
549
550 def print_stack_trace(self, context: int | None = None):
551 if context is None:
552 context = self.context
553 try:
554 skipped = 0
555 to_print = ""
556 for hidden, frame_lineno in zip(self.hidden_frames(self.stack), self.stack):
557 if hidden and self.skip_hidden:
558 skipped += 1
559 continue
560 if skipped:
561 to_print += self.theme.format(
562 [
563 (
564 Token.ExcName,
565 f" [... skipping {skipped} hidden frame(s)]",
566 ),
567 (Token, "\n"),
568 ]
569 )
570
571 skipped = 0
572 to_print += self.format_stack_entry(frame_lineno)
573 if skipped:
574 to_print += self.theme.format(
575 [
576 (
577 Token.ExcName,
578 f" [... skipping {skipped} hidden frame(s)]",
579 ),
580 (Token, "\n"),
581 ]
582 )
583 print(to_print, file=self.stdout)
584 except KeyboardInterrupt:
585 pass
586
587 def print_stack_entry(
588 self, frame_lineno: tuple[FrameType, int], prompt_prefix: str = "\n-> "
589 ) -> None:
590 """
591 Overwrite print_stack_entry from superclass (PDB)
592 """
593 print(self.format_stack_entry(frame_lineno, ""), file=self.stdout)
594
595 frame, lineno = frame_lineno
596 filename = frame.f_code.co_filename
597 self.shell.hooks.synchronize_with_editor(filename, lineno, 0)
598
599 def _get_frame_locals(self, frame):
600 """ "
601 Accessing f_local of current frame reset the namespace, so we want to avoid
602 that or the following can happen
603
604 ipdb> foo
605 "old"
606 ipdb> foo = "new"
607 ipdb> foo
608 "new"
609 ipdb> where
610 ipdb> foo
611 "old"
612
613 So if frame is self.current_frame we instead return self.curframe_locals
614
615 """
616 if frame is self.curframe:
617 return self.curframe_locals
618 else:
619 return frame.f_locals
620
621 def format_stack_entry(
622 self,
623 frame_lineno: tuple[FrameType, int], # type: ignore[override] # stubs are wrong
624 lprefix: str = ": ",
625 ) -> str:
626 """
627 overwrite from super class so must -> str
628 """
629 context = self.context
630 try:
631 context = int(context)
632 if context <= 0:
633 print("Context must be a positive integer", file=self.stdout)
634 except (TypeError, ValueError):
635 print("Context must be a positive integer", file=self.stdout)
636
637 import reprlib
638
639 ret_tok = []
640
641 frame, lineno = frame_lineno
642
643 return_value = ""
644 loc_frame = self._get_frame_locals(frame)
645 if "__return__" in loc_frame:
646 rv = loc_frame["__return__"]
647 # return_value += '->'
648 return_value += reprlib.repr(rv) + "\n"
649 ret_tok.extend([(Token, return_value)])
650
651 # s = filename + '(' + `lineno` + ')'
652 filename = self.canonic(frame.f_code.co_filename)
653 link_tok = (Token.FilenameEm, filename)
654
655 if frame.f_code.co_name:
656 func = frame.f_code.co_name
657 else:
658 func = "<lambda>"
659
660 call_toks = []
661 if func != "?":
662 if "__args__" in loc_frame:
663 args = reprlib.repr(loc_frame["__args__"])
664 else:
665 args = "()"
666 call_toks = [(Token.VName, func), (Token.ValEm, args)]
667
668 # The level info should be generated in the same format pdb uses, to
669 # avoid breaking the pdbtrack functionality of python-mode in *emacs.
670 if frame is self.curframe:
671 ret_tok.append((Token.CurrentFrame, self.theme.make_arrow(2)))
672 else:
673 ret_tok.append((Token, " "))
674
675 ret_tok.extend(
676 [
677 link_tok,
678 (Token, "("),
679 (Token.Lineno, str(lineno)),
680 (Token, ")"),
681 *call_toks,
682 (Token, "\n"),
683 ]
684 )
685
686 start = lineno - 1 - context // 2
687 lines = linecache.getlines(filename)
688 start = min(start, len(lines) - context)
689 start = max(start, 0)
690 lines = lines[start : start + context]
691
692 for i, line in enumerate(lines):
693 show_arrow = start + 1 + i == lineno
694
695 bp, num, colored_line = self.__line_content(
696 filename,
697 start + 1 + i,
698 line,
699 arrow=show_arrow,
700 )
701 if frame is self.curframe or show_arrow:
702 rlt = [
703 bp,
704 (Token.LinenoEm, num),
705 (Token, " "),
706 # TODO: investigate Toke.Line here, likely LineEm,
707 # Token is problematic here as line is already colored, a
708 # and this changes the full style of the colored line.
709 # ideally, __line_content returns the token and we modify the style.
710 (Token, colored_line),
711 ]
712 else:
713 rlt = [
714 bp,
715 (Token.Lineno, num),
716 (Token, " "),
717 # TODO: investigate Toke.Line here, likely Line
718 # Token is problematic here as line is already colored, a
719 # and this changes the full style of the colored line.
720 # ideally, __line_content returns the token and we modify the style.
721 (Token.Line, colored_line),
722 ]
723 ret_tok.extend(rlt)
724
725 return self.theme.format(ret_tok)
726
727 def __line_content(
728 self, filename: str, lineno: int, line: str, arrow: bool = False
729 ):
730 bp_mark = ""
731 BreakpointToken = Token.Breakpoint
732
733 new_line, err = self.parser.format2(line, "str")
734 if not err:
735 assert new_line is not None
736 line = new_line
737
738 bp = None
739 if lineno in self.get_file_breaks(filename):
740 bps = self.get_breaks(filename, lineno)
741 bp = bps[-1]
742
743 if bp:
744 bp_mark = str(bp.number)
745 BreakpointToken = Token.Breakpoint.Enabled
746 if not bp.enabled:
747 BreakpointToken = Token.Breakpoint.Disabled
748 numbers_width = 7
749 if arrow:
750 # This is the line with the error
751 pad = numbers_width - len(str(lineno)) - len(bp_mark)
752 num = "%s%s" % (self.theme.make_arrow(pad), str(lineno))
753 else:
754 num = "%*s" % (numbers_width - len(bp_mark), str(lineno))
755 bp_str = (BreakpointToken, bp_mark)
756 return (bp_str, num, line)
757
758 def print_list_lines(self, filename: str, first: int, last: int) -> None:
759 """The printing (as opposed to the parsing part of a 'list'
760 command."""
761 toks: TokenStream = []
762 try:
763 if filename == "<string>" and hasattr(self, "_exec_filename"):
764 filename = self._exec_filename
765
766 for lineno in range(first, last + 1):
767 line = linecache.getline(filename, lineno)
768 if not line:
769 break
770
771 assert self.curframe is not None
772
773 if lineno == self.curframe.f_lineno:
774 bp, num, colored_line = self.__line_content(
775 filename, lineno, line, arrow=True
776 )
777 toks.extend(
778 [
779 bp,
780 (Token.LinenoEm, num),
781 (Token, " "),
782 # TODO: investigate Token.Line here
783 (Token, colored_line),
784 ]
785 )
786 else:
787 bp, num, colored_line = self.__line_content(
788 filename, lineno, line, arrow=False
789 )
790 toks.extend(
791 [
792 bp,
793 (Token.Lineno, num),
794 (Token, " "),
795 (Token, colored_line),
796 ]
797 )
798
799 self.lineno = lineno
800
801 print(self.theme.format(toks), file=self.stdout)
802
803 except KeyboardInterrupt:
804 pass
805
806 def do_skip_predicates(self, args):
807 """
808 Turn on/off individual predicates as to whether a frame should be hidden/skip.
809
810 The global option to skip (or not) hidden frames is set with skip_hidden
811
812 To change the value of a predicate
813
814 skip_predicates key [true|false]
815
816 Call without arguments to see the current values.
817
818 To permanently change the value of an option add the corresponding
819 command to your ``~/.pdbrc`` file. If you are programmatically using the
820 Pdb instance you can also change the ``default_predicates`` class
821 attribute.
822 """
823 if not args.strip():
824 print("current predicates:")
825 for p, v in self._predicates.items():
826 print(" ", p, ":", v)
827 return
828 type_value = args.strip().split(" ")
829 if len(type_value) != 2:
830 print(
831 f"Usage: skip_predicates <type> <value>, with <type> one of {set(self._predicates.keys())}"
832 )
833 return
834
835 type_, value = type_value
836 if type_ not in self._predicates:
837 print(f"{type_!r} not in {set(self._predicates.keys())}")
838 return
839 if value.lower() not in ("true", "yes", "1", "no", "false", "0"):
840 print(
841 f"{value!r} is invalid - use one of ('true', 'yes', '1', 'no', 'false', '0')"
842 )
843 return
844
845 self._predicates[type_] = value.lower() in ("true", "yes", "1")
846 if not any(self._predicates.values()):
847 print(
848 "Warning, all predicates set to False, skip_hidden may not have any effects."
849 )
850
851 def do_skip_hidden(self, arg):
852 """
853 Change whether or not we should skip frames with the
854 __tracebackhide__ attribute.
855 """
856 if not arg.strip():
857 print(
858 f"skip_hidden = {self.skip_hidden}, use 'yes','no', 'true', or 'false' to change."
859 )
860 elif arg.strip().lower() in ("true", "yes"):
861 self.skip_hidden = True
862 elif arg.strip().lower() in ("false", "no"):
863 self.skip_hidden = False
864 if not any(self._predicates.values()):
865 print(
866 "Warning, all predicates set to False, skip_hidden may not have any effects."
867 )
868
869 def do_list(self, arg):
870 """Print lines of code from the current stack frame"""
871 self.lastcmd = "list"
872 last = None
873 if arg and arg != ".":
874 try:
875 x = eval(arg, {}, {})
876 if type(x) == type(()):
877 first, last = x
878 first = int(first)
879 last = int(last)
880 if last < first:
881 # Assume it's a count
882 last = first + last
883 else:
884 first = max(1, int(x) - 5)
885 except:
886 print("*** Error in argument:", repr(arg), file=self.stdout)
887 return
888 elif self.lineno is None or arg == ".":
889 assert self.curframe is not None
890 first = max(1, self.curframe.f_lineno - 5)
891 else:
892 first = self.lineno + 1
893 if last is None:
894 last = first + 10
895 assert self.curframe is not None
896 self.print_list_lines(self.curframe.f_code.co_filename, first, last)
897
898 lineno = first
899 filename = self.curframe.f_code.co_filename
900 self.shell.hooks.synchronize_with_editor(filename, lineno, 0)
901
902 do_l = do_list
903
904 def getsourcelines(self, obj):
905 lines, lineno = inspect.findsource(obj)
906 if inspect.isframe(obj) and obj.f_globals is self._get_frame_locals(obj):
907 # must be a module frame: do not try to cut a block out of it
908 return lines, 1
909 elif inspect.ismodule(obj):
910 return lines, 1
911 return inspect.getblock(lines[lineno:]), lineno + 1
912
913 def do_longlist(self, arg):
914 """Print lines of code from the current stack frame.
915
916 Shows more lines than 'list' does.
917 """
918 self.lastcmd = "longlist"
919 try:
920 lines, lineno = self.getsourcelines(self.curframe)
921 except OSError as err:
922 self.error(str(err))
923 return
924 last = lineno + len(lines)
925 assert self.curframe is not None
926 self.print_list_lines(self.curframe.f_code.co_filename, lineno, last)
927
928 do_ll = do_longlist
929
930 def do_debug(self, arg):
931 """debug code
932 Enter a recursive debugger that steps through the code
933 argument (which is an arbitrary expression or statement to be
934 executed in the current environment).
935 """
936 trace_function = sys.gettrace()
937 sys.settrace(None)
938 assert self.curframe is not None
939 globals = self.curframe.f_globals
940 locals = self.curframe_locals
941 p = self.__class__(
942 completekey=self.completekey, stdin=self.stdin, stdout=self.stdout
943 )
944 p.use_rawinput = self.use_rawinput
945 p.prompt = "(%s) " % self.prompt.strip()
946 self.message("ENTERING RECURSIVE DEBUGGER")
947 sys.call_tracing(p.run, (arg, globals, locals))
948 self.message("LEAVING RECURSIVE DEBUGGER")
949 sys.settrace(trace_function)
950 self.lastcmd = p.lastcmd
951
952 def do_pdef(self, arg):
953 """Print the call signature for any callable object.
954
955 The debugger interface to %pdef"""
956 assert self.curframe is not None
957 namespaces = [
958 ("Locals", self.curframe_locals),
959 ("Globals", self.curframe.f_globals),
960 ]
961 self.shell.find_line_magic("pdef")(arg, namespaces=namespaces)
962
963 def do_pdoc(self, arg):
964 """Print the docstring for an object.
965
966 The debugger interface to %pdoc."""
967 assert self.curframe is not None
968 namespaces = [
969 ("Locals", self.curframe_locals),
970 ("Globals", self.curframe.f_globals),
971 ]
972 self.shell.find_line_magic("pdoc")(arg, namespaces=namespaces)
973
974 def do_pfile(self, arg):
975 """Print (or run through pager) the file where an object is defined.
976
977 The debugger interface to %pfile.
978 """
979 assert self.curframe is not None
980 namespaces = [
981 ("Locals", self.curframe_locals),
982 ("Globals", self.curframe.f_globals),
983 ]
984 self.shell.find_line_magic("pfile")(arg, namespaces=namespaces)
985
986 def do_pinfo(self, arg):
987 """Provide detailed information about an object.
988
989 The debugger interface to %pinfo, i.e., obj?."""
990 assert self.curframe is not None
991 namespaces = [
992 ("Locals", self.curframe_locals),
993 ("Globals", self.curframe.f_globals),
994 ]
995 self.shell.find_line_magic("pinfo")(arg, namespaces=namespaces)
996
997 def do_pinfo2(self, arg):
998 """Provide extra detailed information about an object.
999
1000 The debugger interface to %pinfo2, i.e., obj??."""
1001 assert self.curframe is not None
1002 namespaces = [
1003 ("Locals", self.curframe_locals),
1004 ("Globals", self.curframe.f_globals),
1005 ]
1006 self.shell.find_line_magic("pinfo2")(arg, namespaces=namespaces)
1007
1008 def do_psource(self, arg):
1009 """Print (or run through pager) the source code for an object."""
1010 assert self.curframe is not None
1011 namespaces = [
1012 ("Locals", self.curframe_locals),
1013 ("Globals", self.curframe.f_globals),
1014 ]
1015 self.shell.find_line_magic("psource")(arg, namespaces=namespaces)
1016
1017 def do_where(self, arg: str):
1018 """w(here)
1019 Print a stack trace, with the most recent frame at the bottom.
1020 An arrow indicates the "current frame", which determines the
1021 context of most commands. 'bt' is an alias for this command.
1022
1023 Take a number as argument as an (optional) number of context line to
1024 print"""
1025 if arg:
1026 try:
1027 context = int(arg)
1028 except ValueError as err:
1029 self.error(str(err))
1030 return
1031 self.print_stack_trace(context)
1032 else:
1033 self.print_stack_trace()
1034
1035 do_w = do_where
1036
1037 def break_anywhere(self, frame):
1038 """
1039 _stop_in_decorator_internals is overly restrictive, as we may still want
1040 to trace function calls, so we need to also update break_anywhere so
1041 that is we don't `stop_here`, because of debugger skip, we may still
1042 stop at any point inside the function
1043
1044 """
1045
1046 sup = super().break_anywhere(frame)
1047 if sup:
1048 return sup
1049 if self._predicates["debuggerskip"]:
1050 if DEBUGGERSKIP in frame.f_code.co_varnames:
1051 return True
1052 if frame.f_back and self._get_frame_locals(frame.f_back).get(DEBUGGERSKIP):
1053 return True
1054 return False
1055
1056 def _is_in_decorator_internal_and_should_skip(self, frame):
1057 """
1058 Utility to tell us whether we are in a decorator internal and should stop.
1059
1060 """
1061 # if we are disabled don't skip
1062 if not self._predicates["debuggerskip"]:
1063 return False
1064
1065 return self._cachable_skip(frame)
1066
1067 @lru_cache(1024)
1068 def _cached_one_parent_frame_debuggerskip(self, frame):
1069 """
1070 Cache looking up for DEBUGGERSKIP on parent frame.
1071
1072 This should speedup walking through deep frame when one of the highest
1073 one does have a debugger skip.
1074
1075 This is likely to introduce fake positive though.
1076 """
1077 while getattr(frame, "f_back", None):
1078 frame = frame.f_back
1079 if self._get_frame_locals(frame).get(DEBUGGERSKIP):
1080 return True
1081 return None
1082
1083 @lru_cache(1024)
1084 def _cachable_skip(self, frame):
1085 # if frame is tagged, skip by default.
1086 if DEBUGGERSKIP in frame.f_code.co_varnames:
1087 return True
1088
1089 # if one of the parent frame value set to True skip as well.
1090 if self._cached_one_parent_frame_debuggerskip(frame):
1091 return True
1092
1093 return False
1094
1095 def stop_here(self, frame):
1096 if self._is_in_decorator_internal_and_should_skip(frame) is True:
1097 return False
1098
1099 hidden = False
1100 if self.skip_hidden:
1101 hidden = self._hidden_predicate(frame)
1102 if hidden:
1103 if self.report_skipped:
1104 print(
1105 self.theme.format(
1106 [
1107 (
1108 Token.ExcName,
1109 " [... skipped 1 hidden frame(s)]",
1110 ),
1111 (Token, "\n"),
1112 ]
1113 )
1114 )
1115 if self.skip and self.is_skipped_module(frame.f_globals.get("__name__", "")):
1116 print(
1117 self.theme.format(
1118 [
1119 (
1120 Token.ExcName,
1121 " [... skipped 1 ignored module(s)]",
1122 ),
1123 (Token, "\n"),
1124 ]
1125 )
1126 )
1127
1128 return False
1129
1130 return super().stop_here(frame)
1131
1132 def do_up(self, arg):
1133 """u(p) [count]
1134 Move the current frame count (default one) levels up in the
1135 stack trace (to an older frame).
1136
1137 Will skip hidden frames and ignored modules.
1138 """
1139 # modified version of upstream that skips
1140 # frames with __tracebackhide__ and ignored modules
1141 if self.curindex == 0:
1142 self.error("Oldest frame")
1143 return
1144 try:
1145 count = int(arg or 1)
1146 except ValueError:
1147 self.error("Invalid frame count (%s)" % arg)
1148 return
1149
1150 hidden_skipped = 0
1151 module_skipped = 0
1152
1153 if count < 0:
1154 _newframe = 0
1155 else:
1156 counter = 0
1157 hidden_frames = self.hidden_frames(self.stack)
1158
1159 for i in range(self.curindex - 1, -1, -1):
1160 should_skip_hidden = hidden_frames[i] and self.skip_hidden
1161 should_skip_module = self.skip and self.is_skipped_module(
1162 self.stack[i][0].f_globals.get("__name__", "")
1163 )
1164
1165 if should_skip_hidden or should_skip_module:
1166 if should_skip_hidden:
1167 hidden_skipped += 1
1168 if should_skip_module:
1169 module_skipped += 1
1170 continue
1171 counter += 1
1172 if counter >= count:
1173 break
1174 else:
1175 # if no break occurred.
1176 self.error(
1177 "all frames above skipped (hidden frames and ignored modules). Use `skip_hidden False` for hidden frames or unignore_module for ignored modules."
1178 )
1179 return
1180
1181 _newframe = i
1182 self._select_frame(_newframe)
1183
1184 total_skipped = hidden_skipped + module_skipped
1185 if total_skipped:
1186 print(
1187 self.theme.format(
1188 [
1189 (
1190 Token.ExcName,
1191 f" [... skipped {total_skipped} frame(s): {hidden_skipped} hidden frames + {module_skipped} ignored modules]",
1192 ),
1193 (Token, "\n"),
1194 ]
1195 )
1196 )
1197
1198 def do_down(self, arg):
1199 """d(own) [count]
1200 Move the current frame count (default one) levels down in the
1201 stack trace (to a newer frame).
1202
1203 Will skip hidden frames and ignored modules.
1204 """
1205 if self.curindex + 1 == len(self.stack):
1206 self.error("Newest frame")
1207 return
1208 try:
1209 count = int(arg or 1)
1210 except ValueError:
1211 self.error("Invalid frame count (%s)" % arg)
1212 return
1213 if count < 0:
1214 _newframe = len(self.stack) - 1
1215 else:
1216 counter = 0
1217 hidden_skipped = 0
1218 module_skipped = 0
1219 hidden_frames = self.hidden_frames(self.stack)
1220
1221 for i in range(self.curindex + 1, len(self.stack)):
1222 should_skip_hidden = hidden_frames[i] and self.skip_hidden
1223 should_skip_module = self.skip and self.is_skipped_module(
1224 self.stack[i][0].f_globals.get("__name__", "")
1225 )
1226
1227 if should_skip_hidden or should_skip_module:
1228 if should_skip_hidden:
1229 hidden_skipped += 1
1230 if should_skip_module:
1231 module_skipped += 1
1232 continue
1233 counter += 1
1234 if counter >= count:
1235 break
1236 else:
1237 self.error(
1238 "all frames below skipped (hidden frames and ignored modules). Use `skip_hidden False` for hidden frames or unignore_module for ignored modules."
1239 )
1240 return
1241
1242 total_skipped = hidden_skipped + module_skipped
1243 if total_skipped:
1244 print(
1245 self.theme.format(
1246 [
1247 (
1248 Token.ExcName,
1249 f" [... skipped {total_skipped} frame(s): {hidden_skipped} hidden frames + {module_skipped} ignored modules]",
1250 ),
1251 (Token, "\n"),
1252 ]
1253 )
1254 )
1255 _newframe = i
1256
1257 self._select_frame(_newframe)
1258
1259 do_d = do_down
1260 do_u = do_up
1261
1262 def _show_ignored_modules(self):
1263 """Display currently ignored modules."""
1264 if self.skip:
1265 print(f"Currently ignored modules: {sorted(self.skip)}")
1266 else:
1267 print("No modules are currently ignored.")
1268
1269 def do_ignore_module(self, arg):
1270 """ignore_module <module_name>
1271
1272 Add a module to the list of modules to skip when navigating frames.
1273 When a module is ignored, the debugger will automatically skip over
1274 frames from that module.
1275
1276 Supports wildcard patterns using fnmatch syntax:
1277
1278 Usage:
1279 ignore_module threading # Skip threading module frames
1280 ignore_module asyncio.\\* # Skip all asyncio submodules
1281 ignore_module \\*.tests # Skip all test modules
1282 ignore_module # List currently ignored modules
1283 """
1284
1285 if self.skip is None:
1286 self.skip = set()
1287
1288 module_name = arg.strip()
1289
1290 if not module_name:
1291 self._show_ignored_modules()
1292 return
1293
1294 self.skip.add(module_name)
1295
1296 def do_unignore_module(self, arg):
1297 """unignore_module <module_name>
1298
1299 Remove a module from the list of modules to skip when navigating frames.
1300 This will allow the debugger to step into frames from the specified module.
1301
1302 Usage:
1303 unignore_module threading # Stop ignoring threading module frames
1304 unignore_module asyncio.\\* # Remove asyncio.* pattern
1305 unignore_module # List currently ignored modules
1306 """
1307
1308 if self.skip is None:
1309 self.skip = set()
1310
1311 module_name = arg.strip()
1312
1313 if not module_name:
1314 self._show_ignored_modules()
1315 return
1316
1317 try:
1318 self.skip.remove(module_name)
1319 except KeyError:
1320 print(f"Module {module_name} is not currently ignored")
1321 self._show_ignored_modules()
1322
1323 def do_context(self, context: str):
1324 """context number_of_lines
1325 Set the number of lines of source code to show when displaying
1326 stacktrace information.
1327 """
1328 try:
1329 new_context = int(context)
1330 if new_context <= 0:
1331 raise ValueError()
1332 self.context = new_context
1333 except ValueError:
1334 self.error(
1335 f"The 'context' command requires a positive integer argument (current value {self.context})."
1336 )
1337
1338
1339class InterruptiblePdb(Pdb):
1340 """Version of debugger where KeyboardInterrupt exits the debugger altogether."""
1341
1342 def cmdloop(self, intro=None):
1343 """Wrap cmdloop() such that KeyboardInterrupt stops the debugger."""
1344 try:
1345 return OldPdb.cmdloop(self, intro=intro)
1346 except KeyboardInterrupt:
1347 self.stop_here = lambda frame: False # type: ignore[method-assign]
1348 self.do_quit("")
1349 sys.settrace(None)
1350 self.quitting = False
1351 raise
1352
1353 def _cmdloop(self):
1354 while True:
1355 try:
1356 # keyboard interrupts allow for an easy way to cancel
1357 # the current command, so allow them during interactive input
1358 self.allow_kbdint = True
1359 self.cmdloop()
1360 self.allow_kbdint = False
1361 break
1362 except KeyboardInterrupt:
1363 self.message("--KeyboardInterrupt--")
1364 raise
1365
1366
1367def set_trace(frame=None, header=None):
1368 """
1369 Start debugging from `frame`.
1370
1371 If frame is not specified, debugging starts from caller's frame.
1372 """
1373 pdb = Pdb()
1374 if header is not None:
1375 pdb.message(header)
1376 pdb.set_trace(frame or sys._getframe().f_back)