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