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