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 line = new_line
732
733 bp = None
734 if lineno in self.get_file_breaks(filename):
735 bps = self.get_breaks(filename, lineno)
736 bp = bps[-1]
737
738 if bp:
739 bp_mark = str(bp.number)
740 BreakpointToken = Token.Breakpoint.Enabled
741 if not bp.enabled:
742 BreakpointToken = Token.Breakpoint.Disabled
743 numbers_width = 7
744 if arrow:
745 # This is the line with the error
746 pad = numbers_width - len(str(lineno)) - len(bp_mark)
747 num = "%s%s" % (self.theme.make_arrow(pad), str(lineno))
748 else:
749 num = "%*s" % (numbers_width - len(bp_mark), str(lineno))
750 bp_str = (BreakpointToken, bp_mark)
751 return (bp_str, num, line)
752
753 def print_list_lines(self, filename: str, first: int, last: int) -> None:
754 """The printing (as opposed to the parsing part of a 'list'
755 command."""
756 toks: TokenStream = []
757 try:
758 if filename == "<string>" and hasattr(self, "_exec_filename"):
759 filename = self._exec_filename
760
761 for lineno in range(first, last + 1):
762 line = linecache.getline(filename, lineno)
763 if not line:
764 break
765
766 assert self.curframe is not None
767
768 if lineno == self.curframe.f_lineno:
769 bp, num, colored_line = self.__line_content(
770 filename, lineno, line, arrow=True
771 )
772 toks.extend(
773 [
774 bp,
775 (Token.LinenoEm, num),
776 (Token, " "),
777 # TODO: investigate Token.Line here
778 (Token, colored_line),
779 ]
780 )
781 else:
782 bp, num, colored_line = self.__line_content(
783 filename, lineno, line, arrow=False
784 )
785 toks.extend(
786 [
787 bp,
788 (Token.Lineno, num),
789 (Token, " "),
790 (Token, colored_line),
791 ]
792 )
793
794 self.lineno = lineno
795
796 print(self.theme.format(toks), file=self.stdout)
797
798 except KeyboardInterrupt:
799 pass
800
801 def do_skip_predicates(self, args):
802 """
803 Turn on/off individual predicates as to whether a frame should be hidden/skip.
804
805 The global option to skip (or not) hidden frames is set with skip_hidden
806
807 To change the value of a predicate
808
809 skip_predicates key [true|false]
810
811 Call without arguments to see the current values.
812
813 To permanently change the value of an option add the corresponding
814 command to your ``~/.pdbrc`` file. If you are programmatically using the
815 Pdb instance you can also change the ``default_predicates`` class
816 attribute.
817 """
818 if not args.strip():
819 print("current predicates:")
820 for p, v in self._predicates.items():
821 print(" ", p, ":", v)
822 return
823 type_value = args.strip().split(" ")
824 if len(type_value) != 2:
825 print(
826 f"Usage: skip_predicates <type> <value>, with <type> one of {set(self._predicates.keys())}"
827 )
828 return
829
830 type_, value = type_value
831 if type_ not in self._predicates:
832 print(f"{type_!r} not in {set(self._predicates.keys())}")
833 return
834 if value.lower() not in ("true", "yes", "1", "no", "false", "0"):
835 print(
836 f"{value!r} is invalid - use one of ('true', 'yes', '1', 'no', 'false', '0')"
837 )
838 return
839
840 self._predicates[type_] = value.lower() in ("true", "yes", "1")
841 if not any(self._predicates.values()):
842 print(
843 "Warning, all predicates set to False, skip_hidden may not have any effects."
844 )
845
846 def do_skip_hidden(self, arg):
847 """
848 Change whether or not we should skip frames with the
849 __tracebackhide__ attribute.
850 """
851 if not arg.strip():
852 print(
853 f"skip_hidden = {self.skip_hidden}, use 'yes','no', 'true', or 'false' to change."
854 )
855 elif arg.strip().lower() in ("true", "yes"):
856 self.skip_hidden = True
857 elif arg.strip().lower() in ("false", "no"):
858 self.skip_hidden = False
859 if not any(self._predicates.values()):
860 print(
861 "Warning, all predicates set to False, skip_hidden may not have any effects."
862 )
863
864 def do_list(self, arg):
865 """Print lines of code from the current stack frame"""
866 self.lastcmd = "list"
867 last = None
868 if arg and arg != ".":
869 try:
870 x = eval(arg, {}, {})
871 if type(x) == type(()):
872 first, last = x
873 first = int(first)
874 last = int(last)
875 if last < first:
876 # Assume it's a count
877 last = first + last
878 else:
879 first = max(1, int(x) - 5)
880 except:
881 print("*** Error in argument:", repr(arg), file=self.stdout)
882 return
883 elif self.lineno is None or arg == ".":
884 assert self.curframe is not None
885 first = max(1, self.curframe.f_lineno - 5)
886 else:
887 first = self.lineno + 1
888 if last is None:
889 last = first + 10
890 assert self.curframe is not None
891 self.print_list_lines(self.curframe.f_code.co_filename, first, last)
892
893 lineno = first
894 filename = self.curframe.f_code.co_filename
895 self.shell.hooks.synchronize_with_editor(filename, lineno, 0)
896
897 do_l = do_list
898
899 def getsourcelines(self, obj):
900 lines, lineno = inspect.findsource(obj)
901 if inspect.isframe(obj) and obj.f_globals is self._get_frame_locals(obj):
902 # must be a module frame: do not try to cut a block out of it
903 return lines, 1
904 elif inspect.ismodule(obj):
905 return lines, 1
906 return inspect.getblock(lines[lineno:]), lineno + 1
907
908 def do_longlist(self, arg):
909 """Print lines of code from the current stack frame.
910
911 Shows more lines than 'list' does.
912 """
913 self.lastcmd = "longlist"
914 try:
915 lines, lineno = self.getsourcelines(self.curframe)
916 except OSError as err:
917 self.error(str(err))
918 return
919 last = lineno + len(lines)
920 assert self.curframe is not None
921 self.print_list_lines(self.curframe.f_code.co_filename, lineno, last)
922
923 do_ll = do_longlist
924
925 def do_debug(self, arg):
926 """debug code
927 Enter a recursive debugger that steps through the code
928 argument (which is an arbitrary expression or statement to be
929 executed in the current environment).
930 """
931 trace_function = sys.gettrace()
932 sys.settrace(None)
933 assert self.curframe is not None
934 globals = self.curframe.f_globals
935 locals = self.curframe_locals
936 p = self.__class__(
937 completekey=self.completekey, stdin=self.stdin, stdout=self.stdout
938 )
939 p.use_rawinput = self.use_rawinput
940 p.prompt = "(%s) " % self.prompt.strip()
941 self.message("ENTERING RECURSIVE DEBUGGER")
942 sys.call_tracing(p.run, (arg, globals, locals))
943 self.message("LEAVING RECURSIVE DEBUGGER")
944 sys.settrace(trace_function)
945 self.lastcmd = p.lastcmd
946
947 def do_pdef(self, arg):
948 """Print the call signature for any callable object.
949
950 The debugger interface to %pdef"""
951 assert self.curframe is not None
952 namespaces = [
953 ("Locals", self.curframe_locals),
954 ("Globals", self.curframe.f_globals),
955 ]
956 self.shell.find_line_magic("pdef")(arg, namespaces=namespaces)
957
958 def do_pdoc(self, arg):
959 """Print the docstring for an object.
960
961 The debugger interface to %pdoc."""
962 assert self.curframe is not None
963 namespaces = [
964 ("Locals", self.curframe_locals),
965 ("Globals", self.curframe.f_globals),
966 ]
967 self.shell.find_line_magic("pdoc")(arg, namespaces=namespaces)
968
969 def do_pfile(self, arg):
970 """Print (or run through pager) the file where an object is defined.
971
972 The debugger interface to %pfile.
973 """
974 assert self.curframe is not None
975 namespaces = [
976 ("Locals", self.curframe_locals),
977 ("Globals", self.curframe.f_globals),
978 ]
979 self.shell.find_line_magic("pfile")(arg, namespaces=namespaces)
980
981 def do_pinfo(self, arg):
982 """Provide detailed information about an object.
983
984 The debugger interface to %pinfo, i.e., obj?."""
985 assert self.curframe is not None
986 namespaces = [
987 ("Locals", self.curframe_locals),
988 ("Globals", self.curframe.f_globals),
989 ]
990 self.shell.find_line_magic("pinfo")(arg, namespaces=namespaces)
991
992 def do_pinfo2(self, arg):
993 """Provide extra detailed information about an object.
994
995 The debugger interface to %pinfo2, i.e., obj??."""
996 assert self.curframe is not None
997 namespaces = [
998 ("Locals", self.curframe_locals),
999 ("Globals", self.curframe.f_globals),
1000 ]
1001 self.shell.find_line_magic("pinfo2")(arg, namespaces=namespaces)
1002
1003 def do_psource(self, arg):
1004 """Print (or run through pager) the source code for an object."""
1005 assert self.curframe is not None
1006 namespaces = [
1007 ("Locals", self.curframe_locals),
1008 ("Globals", self.curframe.f_globals),
1009 ]
1010 self.shell.find_line_magic("psource")(arg, namespaces=namespaces)
1011
1012 def do_where(self, arg: str):
1013 """w(here)
1014 Print a stack trace, with the most recent frame at the bottom.
1015 An arrow indicates the "current frame", which determines the
1016 context of most commands. 'bt' is an alias for this command.
1017
1018 Take a number as argument as an (optional) number of context line to
1019 print"""
1020 if arg:
1021 try:
1022 context = int(arg)
1023 except ValueError as err:
1024 self.error(str(err))
1025 return
1026 self.print_stack_trace(context)
1027 else:
1028 self.print_stack_trace()
1029
1030 do_w = do_where
1031
1032 def break_anywhere(self, frame):
1033 """
1034 _stop_in_decorator_internals is overly restrictive, as we may still want
1035 to trace function calls, so we need to also update break_anywhere so
1036 that is we don't `stop_here`, because of debugger skip, we may still
1037 stop at any point inside the function
1038
1039 """
1040
1041 sup = super().break_anywhere(frame)
1042 if sup:
1043 return sup
1044 if self._predicates["debuggerskip"]:
1045 if DEBUGGERSKIP in frame.f_code.co_varnames:
1046 return True
1047 if frame.f_back and self._get_frame_locals(frame.f_back).get(DEBUGGERSKIP):
1048 return True
1049 return False
1050
1051 def _is_in_decorator_internal_and_should_skip(self, frame):
1052 """
1053 Utility to tell us whether we are in a decorator internal and should stop.
1054
1055 """
1056 # if we are disabled don't skip
1057 if not self._predicates["debuggerskip"]:
1058 return False
1059
1060 return self._cachable_skip(frame)
1061
1062 @lru_cache(1024)
1063 def _cached_one_parent_frame_debuggerskip(self, frame):
1064 """
1065 Cache looking up for DEBUGGERSKIP on parent frame.
1066
1067 This should speedup walking through deep frame when one of the highest
1068 one does have a debugger skip.
1069
1070 This is likely to introduce fake positive though.
1071 """
1072 while getattr(frame, "f_back", None):
1073 frame = frame.f_back
1074 if self._get_frame_locals(frame).get(DEBUGGERSKIP):
1075 return True
1076 return None
1077
1078 @lru_cache(1024)
1079 def _cachable_skip(self, frame):
1080 # if frame is tagged, skip by default.
1081 if DEBUGGERSKIP in frame.f_code.co_varnames:
1082 return True
1083
1084 # if one of the parent frame value set to True skip as well.
1085 if self._cached_one_parent_frame_debuggerskip(frame):
1086 return True
1087
1088 return False
1089
1090 def stop_here(self, frame):
1091 if self._is_in_decorator_internal_and_should_skip(frame) is True:
1092 return False
1093
1094 hidden = False
1095 if self.skip_hidden:
1096 hidden = self._hidden_predicate(frame)
1097 if hidden:
1098 if self.report_skipped:
1099 print(
1100 self.theme.format(
1101 [
1102 (
1103 Token.ExcName,
1104 " [... skipped 1 hidden frame(s)]",
1105 ),
1106 (Token, "\n"),
1107 ]
1108 )
1109 )
1110 return super().stop_here(frame)
1111
1112 def do_up(self, arg):
1113 """u(p) [count]
1114 Move the current frame count (default one) levels up in the
1115 stack trace (to an older frame).
1116
1117 Will skip hidden frames.
1118 """
1119 # modified version of upstream that skips
1120 # frames with __tracebackhide__
1121 if self.curindex == 0:
1122 self.error("Oldest frame")
1123 return
1124 try:
1125 count = int(arg or 1)
1126 except ValueError:
1127 self.error("Invalid frame count (%s)" % arg)
1128 return
1129 skipped = 0
1130 if count < 0:
1131 _newframe = 0
1132 else:
1133 counter = 0
1134 hidden_frames = self.hidden_frames(self.stack)
1135 for i in range(self.curindex - 1, -1, -1):
1136 if hidden_frames[i] and self.skip_hidden:
1137 skipped += 1
1138 continue
1139 counter += 1
1140 if counter >= count:
1141 break
1142 else:
1143 # if no break occurred.
1144 self.error(
1145 "all frames above hidden, use `skip_hidden False` to get get into those."
1146 )
1147 return
1148
1149 _newframe = i
1150 self._select_frame(_newframe)
1151 if skipped:
1152 print(
1153 self.theme.format(
1154 [
1155 (
1156 Token.ExcName,
1157 f" [... skipped {skipped} hidden frame(s)]",
1158 ),
1159 (Token, "\n"),
1160 ]
1161 )
1162 )
1163
1164 def do_down(self, arg):
1165 """d(own) [count]
1166 Move the current frame count (default one) levels down in the
1167 stack trace (to a newer frame).
1168
1169 Will skip hidden frames.
1170 """
1171 if self.curindex + 1 == len(self.stack):
1172 self.error("Newest frame")
1173 return
1174 try:
1175 count = int(arg or 1)
1176 except ValueError:
1177 self.error("Invalid frame count (%s)" % arg)
1178 return
1179 if count < 0:
1180 _newframe = len(self.stack) - 1
1181 else:
1182 counter = 0
1183 skipped = 0
1184 hidden_frames = self.hidden_frames(self.stack)
1185 for i in range(self.curindex + 1, len(self.stack)):
1186 if hidden_frames[i] and self.skip_hidden:
1187 skipped += 1
1188 continue
1189 counter += 1
1190 if counter >= count:
1191 break
1192 else:
1193 self.error(
1194 "all frames below hidden, use `skip_hidden False` to get get into those."
1195 )
1196 return
1197
1198 if skipped:
1199 print(
1200 self.theme.format(
1201 [
1202 (
1203 Token.ExcName,
1204 f" [... skipped {skipped} hidden frame(s)]",
1205 ),
1206 (Token, "\n"),
1207 ]
1208 )
1209 )
1210 _newframe = i
1211
1212 self._select_frame(_newframe)
1213
1214 do_d = do_down
1215 do_u = do_up
1216
1217 def do_context(self, context: str):
1218 """context number_of_lines
1219 Set the number of lines of source code to show when displaying
1220 stacktrace information.
1221 """
1222 try:
1223 new_context = int(context)
1224 if new_context <= 0:
1225 raise ValueError()
1226 self.context = new_context
1227 except ValueError:
1228 self.error(
1229 f"The 'context' command requires a positive integer argument (current value {self.context})."
1230 )
1231
1232
1233class InterruptiblePdb(Pdb):
1234 """Version of debugger where KeyboardInterrupt exits the debugger altogether."""
1235
1236 def cmdloop(self, intro=None):
1237 """Wrap cmdloop() such that KeyboardInterrupt stops the debugger."""
1238 try:
1239 return OldPdb.cmdloop(self, intro=intro)
1240 except KeyboardInterrupt:
1241 self.stop_here = lambda frame: False # type: ignore[method-assign]
1242 self.do_quit("")
1243 sys.settrace(None)
1244 self.quitting = False
1245 raise
1246
1247 def _cmdloop(self):
1248 while True:
1249 try:
1250 # keyboard interrupts allow for an easy way to cancel
1251 # the current command, so allow them during interactive input
1252 self.allow_kbdint = True
1253 self.cmdloop()
1254 self.allow_kbdint = False
1255 break
1256 except KeyboardInterrupt:
1257 self.message("--KeyboardInterrupt--")
1258 raise
1259
1260
1261def set_trace(frame=None, header=None):
1262 """
1263 Start debugging from `frame`.
1264
1265 If frame is not specified, debugging starts from caller's frame.
1266 """
1267 pdb = Pdb()
1268 if header is not None:
1269 pdb.message(header)
1270 pdb.set_trace(frame or sys._getframe().f_back)