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