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