From ec60bda802f3c8cfd1fcc1db90da2fa4664dbe4c Mon Sep 17 00:00:00 2001 From: Shahar Naveh <50263213+ShaharNaveh@users.noreply.github.com> Date: Thu, 11 Jun 2026 07:36:30 +0300 Subject: [PATCH] Update `pdb.py` to 3.14.5 (#8070) * Update `pdb.py` to 3.14.5 * Add `test_pdb.py` from 3.14.5 * Update `_pyrepl/types.py` * Update `_pyrepl/utils.py` * Mark failing tests * Mark failing tests --- Lib/_pyrepl/types.py | 14 +- Lib/_pyrepl/utils.py | 376 ++- Lib/pdb.py | 2575 ++++++++++++++++++--- Lib/test/test_pdb.py | 5179 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 7817 insertions(+), 327 deletions(-) mode change 100755 => 100644 Lib/pdb.py create mode 100644 Lib/test/test_pdb.py diff --git a/Lib/_pyrepl/types.py b/Lib/_pyrepl/types.py index f9d48b828..c5b7ebc1a 100644 --- a/Lib/_pyrepl/types.py +++ b/Lib/_pyrepl/types.py @@ -1,8 +1,10 @@ from collections.abc import Callable, Iterator -Callback = Callable[[], object] -SimpleContextManager = Iterator[None] -KeySpec = str # like r"\C-c" -CommandName = str # like "interrupt" -EventTuple = tuple[CommandName, str] -Completer = Callable[[str, int], str | None] +type Callback = Callable[[], object] +type SimpleContextManager = Iterator[None] +type KeySpec = str # like r"\C-c" +type CommandName = str # like "interrupt" +type EventTuple = tuple[CommandName, str] +type Completer = Callable[[str, int], str | None] +type CharBuffer = list[str] +type CharWidths = list[int] diff --git a/Lib/_pyrepl/utils.py b/Lib/_pyrepl/utils.py index 4651717bd..06cddef85 100644 --- a/Lib/_pyrepl/utils.py +++ b/Lib/_pyrepl/utils.py @@ -1,25 +1,391 @@ -import re -import unicodedata +from __future__ import annotations +import builtins import functools +import keyword +import re +import token as T +import tokenize +import unicodedata +import _colorize + +from collections import deque +from io import StringIO +from tokenize import TokenInfo as TI +from typing import Iterable, Iterator, Match, NamedTuple, Self + +from .types import CharBuffer, CharWidths +from .trace import trace ANSI_ESCAPE_SEQUENCE = re.compile(r"\x1b\[[ -@]*[A-~]") +ZERO_WIDTH_BRACKET = re.compile(r"\x01.*?\x02") +ZERO_WIDTH_TRANS = str.maketrans({"\x01": "", "\x02": ""}) +IDENTIFIERS_AFTER = {"def", "class"} +KEYWORD_CONSTANTS = {"True", "False", "None"} +BUILTINS = {str(name) for name in dir(builtins) if not name.startswith('_')} + + +def THEME(**kwargs): + # Not cached: the user can modify the theme inside the interactive session. + return _colorize.get_theme(**kwargs).syntax + + +class Span(NamedTuple): + """Span indexing that's inclusive on both ends.""" + + start: int + end: int + + @classmethod + def from_re(cls, m: Match[str], group: int | str) -> Self: + re_span = m.span(group) + return cls(re_span[0], re_span[1] - 1) + + @classmethod + def from_token(cls, token: TI, line_len: list[int]) -> Self: + end_offset = -1 + if (token.type in {T.FSTRING_MIDDLE, T.TSTRING_MIDDLE} + and token.string.endswith(("{", "}"))): + # gh-134158: a visible trailing brace comes from a double brace in input + end_offset += 1 + + return cls( + line_len[token.start[0] - 1] + token.start[1], + line_len[token.end[0] - 1] + token.end[1] + end_offset, + ) + + +class ColorSpan(NamedTuple): + span: Span + tag: str @functools.cache def str_width(c: str) -> int: if ord(c) < 128: return 1 + # gh-139246 for zero-width joiner and combining characters + if unicodedata.combining(c): + return 0 + category = unicodedata.category(c) + if category == "Cf" and c != "\u00ad": + return 0 w = unicodedata.east_asian_width(c) - if w in ('N', 'Na', 'H', 'A'): + if w in ("N", "Na", "H", "A"): return 1 return 2 def wlen(s: str) -> int: - if len(s) == 1 and s != '\x1a': + if len(s) == 1 and s != "\x1a": return str_width(s) length = sum(str_width(i) for i in s) # remove lengths of any escape sequences sequence = ANSI_ESCAPE_SEQUENCE.findall(s) - ctrl_z_cnt = s.count('\x1a') + ctrl_z_cnt = s.count("\x1a") return length - sum(len(i) for i in sequence) + ctrl_z_cnt + + +def unbracket(s: str, including_content: bool = False) -> str: + r"""Return `s` with \001 and \002 characters removed. + + If `including_content` is True, content between \001 and \002 is also + stripped. + """ + if including_content: + return ZERO_WIDTH_BRACKET.sub("", s) + return s.translate(ZERO_WIDTH_TRANS) + + +def gen_colors(buffer: str) -> Iterator[ColorSpan]: + """Returns a list of index spans to color using the given color tag. + + The input `buffer` should be a valid start of a Python code block, i.e. + it cannot be a block starting in the middle of a multiline string. + """ + sio = StringIO(buffer) + line_lengths = [0] + [len(line) for line in sio.readlines()] + # make line_lengths cumulative + for i in range(1, len(line_lengths)): + line_lengths[i] += line_lengths[i-1] + + sio.seek(0) + gen = tokenize.generate_tokens(sio.readline) + last_emitted: ColorSpan | None = None + try: + for color in gen_colors_from_token_stream(gen, line_lengths): + yield color + last_emitted = color + except SyntaxError: + return + except tokenize.TokenError as te: + yield from recover_unterminated_string( + te, line_lengths, last_emitted, buffer + ) + + +def recover_unterminated_string( + exc: tokenize.TokenError, + line_lengths: list[int], + last_emitted: ColorSpan | None, + buffer: str, +) -> Iterator[ColorSpan]: + msg, loc = exc.args + if loc is None: + return + + line_no, column = loc + + if msg.startswith( + ( + "unterminated string literal", + "unterminated f-string literal", + "unterminated t-string literal", + "EOF in multi-line string", + "unterminated triple-quoted f-string literal", + "unterminated triple-quoted t-string literal", + ) + ): + start = line_lengths[line_no - 1] + column - 1 + end = line_lengths[-1] - 1 + + # in case FSTRING_START was already emitted + if last_emitted and start <= last_emitted.span.start: + trace("before last emitted = {s}", s=start) + start = last_emitted.span.end + 1 + + span = Span(start, end) + trace("yielding span {a} -> {b}", a=span.start, b=span.end) + yield ColorSpan(span, "string") + else: + trace( + "unhandled token error({buffer}) = {te}", + buffer=repr(buffer), + te=str(exc), + ) + + +def gen_colors_from_token_stream( + token_generator: Iterator[TI], + line_lengths: list[int], +) -> Iterator[ColorSpan]: + token_window = prev_next_window(token_generator) + + is_def_name = False + bracket_level = 0 + for prev_token, token, next_token in token_window: + assert token is not None + if token.start == token.end: + continue + + match token.type: + case ( + T.STRING + | T.FSTRING_START | T.FSTRING_MIDDLE | T.FSTRING_END + | T.TSTRING_START | T.TSTRING_MIDDLE | T.TSTRING_END + ): + span = Span.from_token(token, line_lengths) + yield ColorSpan(span, "string") + case T.COMMENT: + span = Span.from_token(token, line_lengths) + yield ColorSpan(span, "comment") + case T.NUMBER: + span = Span.from_token(token, line_lengths) + yield ColorSpan(span, "number") + case T.OP: + if token.string in "([{": + bracket_level += 1 + elif token.string in ")]}": + bracket_level -= 1 + span = Span.from_token(token, line_lengths) + yield ColorSpan(span, "op") + case T.NAME: + if is_def_name: + is_def_name = False + span = Span.from_token(token, line_lengths) + yield ColorSpan(span, "definition") + elif keyword.iskeyword(token.string): + span_cls = "keyword" + if token.string in KEYWORD_CONSTANTS: + span_cls = "keyword_constant" + span = Span.from_token(token, line_lengths) + yield ColorSpan(span, span_cls) + if token.string in IDENTIFIERS_AFTER: + is_def_name = True + elif ( + keyword.issoftkeyword(token.string) + and bracket_level == 0 + and is_soft_keyword_used(prev_token, token, next_token) + ): + span = Span.from_token(token, line_lengths) + yield ColorSpan(span, "soft_keyword") + elif ( + token.string in BUILTINS + and not (prev_token and prev_token.exact_type == T.DOT) + ): + span = Span.from_token(token, line_lengths) + yield ColorSpan(span, "builtin") + + +keyword_first_sets_match = {"False", "None", "True", "await", "lambda", "not"} +keyword_first_sets_case = {"False", "None", "True"} + + +def is_soft_keyword_used(*tokens: TI | None) -> bool: + """Returns True if the current token is a keyword in this context. + + For the `*tokens` to match anything, they have to be a three-tuple of + (previous, current, next). + """ + trace("is_soft_keyword_used{t}", t=tokens) + match tokens: + case ( + None | TI(T.NEWLINE) | TI(T.INDENT) | TI(string=":"), + TI(string="match"), + TI(T.NUMBER | T.STRING | T.FSTRING_START | T.TSTRING_START) + | TI(T.OP, string="(" | "*" | "[" | "{" | "~" | "...") + ): + return True + case ( + None | TI(T.NEWLINE) | TI(T.INDENT) | TI(string=":"), + TI(string="match"), + TI(T.NAME, string=s) + ): + if keyword.iskeyword(s): + return s in keyword_first_sets_match + return True + case ( + None | TI(T.NEWLINE) | TI(T.INDENT) | TI(T.DEDENT) | TI(string=":"), + TI(string="case"), + TI(T.NUMBER | T.STRING | T.FSTRING_START | T.TSTRING_START) + | TI(T.OP, string="(" | "*" | "-" | "[" | "{") + ): + return True + case ( + None | TI(T.NEWLINE) | TI(T.INDENT) | TI(T.DEDENT) | TI(string=":"), + TI(string="case"), + TI(T.NAME, string=s) + ): + if keyword.iskeyword(s): + return s in keyword_first_sets_case + return True + case (TI(string="case"), TI(string="_"), TI(string=":")): + return True + case ( + None | TI(T.NEWLINE) | TI(T.INDENT) | TI(T.DEDENT) | TI(string=":"), + TI(string="type"), + TI(T.NAME, string=s) + ): + return not keyword.iskeyword(s) + case _: + return False + + +def disp_str( + buffer: str, + colors: list[ColorSpan] | None = None, + start_index: int = 0, + force_color: bool = False, +) -> tuple[CharBuffer, CharWidths]: + r"""Decompose the input buffer into a printable variant with applied colors. + + Returns a tuple of two lists: + - the first list is the input buffer, character by character, with color + escape codes added (while those codes contain multiple ASCII characters, + each code is considered atomic *and is attached for the corresponding + visible character*); + - the second list is the visible width of each character in the input + buffer. + + Note on colors: + - The `colors` list, if provided, is partially consumed within. We're using + a list and not a generator since we need to hold onto the current + unfinished span between calls to disp_str in case of multiline strings. + - The `colors` list is computed from the start of the input block. `buffer` + is only a subset of that input block, a single line within. This is why + we need `start_index` to inform us which position is the start of `buffer` + actually within user input. This allows us to match color spans correctly. + + Examples: + >>> utils.disp_str("a = 9") + (['a', ' ', '=', ' ', '9'], [1, 1, 1, 1, 1]) + + >>> line = "while 1:" + >>> colors = list(utils.gen_colors(line)) + >>> utils.disp_str(line, colors=colors) + (['\x1b[1;34mw', 'h', 'i', 'l', 'e\x1b[0m', ' ', '1', ':'], [1, 1, 1, 1, 1, 1, 1, 1]) + + """ + chars: CharBuffer = [] + char_widths: CharWidths = [] + + if not buffer: + return chars, char_widths + + while colors and colors[0].span.end < start_index: + # move past irrelevant spans + colors.pop(0) + + theme = THEME(force_color=force_color) + pre_color = "" + post_color = "" + if colors and colors[0].span.start < start_index: + # looks like we're continuing a previous color (e.g. a multiline str) + pre_color = theme[colors[0].tag] + + for i, c in enumerate(buffer, start_index): + if colors and colors[0].span.start == i: # new color starts now + pre_color = theme[colors[0].tag] + + if c == "\x1a": # CTRL-Z on Windows + chars.append(c) + char_widths.append(2) + elif ord(c) < 128: + chars.append(c) + char_widths.append(1) + elif unicodedata.category(c).startswith("C"): + c = r"\u%04x" % ord(c) + chars.append(c) + char_widths.append(len(c)) + else: + chars.append(c) + char_widths.append(str_width(c)) + + if colors and colors[0].span.end == i: # current color ends now + post_color = theme.reset + colors.pop(0) + + chars[-1] = pre_color + chars[-1] + post_color + pre_color = "" + post_color = "" + + if colors and colors[0].span.start < i and colors[0].span.end > i: + # even though the current color should be continued, reset it for now. + # the next call to `disp_str()` will revive it. + chars[-1] += theme.reset + + return chars, char_widths + + +def prev_next_window[T]( + iterable: Iterable[T] +) -> Iterator[tuple[T | None, ...]]: + """Generates three-tuples of (previous, current, next) items. + + On the first iteration previous is None. On the last iteration next + is None. In case of exception next is None and the exception is re-raised + on a subsequent next() call. + + Inspired by `sliding_window` from `itertools` recipes. + """ + + iterator = iter(iterable) + window = deque((None, next(iterator)), maxlen=3) + try: + for x in iterator: + window.append(x) + yield tuple(window) + except Exception: + raise + finally: + window.append(None) + yield tuple(window) diff --git a/Lib/pdb.py b/Lib/pdb.py old mode 100755 new mode 100644 index ec6cf06e5..7dcd0cb1b --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -1,5 +1,3 @@ -#! /usr/bin/env python3 - """ The Python Debugger Pdb ======================= @@ -76,11 +74,34 @@ import bdb import dis import code import glob +import json +import stat +import token +import types +import atexit +import codeop import pprint import signal +import socket +import typing +import asyncio import inspect +import weakref +import builtins +import tempfile +import textwrap +import tokenize +import itertools import traceback import linecache +import selectors +import threading +import _colorize +import _pyrepl.utils + +from contextlib import ExitStack, closing, contextmanager +from types import CodeType +from warnings import deprecated class Restart(Exception): @@ -88,29 +109,56 @@ class Restart(Exception): pass __all__ = ["run", "pm", "Pdb", "runeval", "runctx", "runcall", "set_trace", - "post_mortem", "help"] + "post_mortem", "set_default_backend", "get_default_backend", "help"] + + +def find_first_executable_line(code): + """ Try to find the first executable line of the code object. + + Equivalently, find the line number of the instruction that's + after RESUME + + Return code.co_firstlineno if no executable line is found. + """ + prev = None + for instr in dis.get_instructions(code): + if prev is not None and prev.opname == 'RESUME': + if instr.positions.lineno is not None: + return instr.positions.lineno + return code.co_firstlineno + prev = instr + return code.co_firstlineno def find_function(funcname, filename): - cre = re.compile(r'def\s+%s\s*[(]' % re.escape(funcname)) + cre = re.compile(r'def\s+%s(\s*\[.+\])?\s*[(]' % re.escape(funcname)) try: - fp = open(filename) + fp = tokenize.open(filename) except OSError: - return None + lines = linecache.getlines(filename) + if not lines: + return None + fp = io.StringIO(''.join(lines)) + funcdef = "" + funcstart = 0 # consumer of this info expects the first line to be 1 with fp: for lineno, line in enumerate(fp, start=1): if cre.match(line): - return funcname, filename, lineno - return None + funcstart, funcdef = lineno, line + elif funcdef: + funcdef += line -def getsourcelines(obj): - lines, lineno = inspect.findsource(obj) - if inspect.isframe(obj) and obj.f_globals is obj.f_locals: - # must be a module frame: do not try to cut a block out of it - return lines, 1 - elif inspect.ismodule(obj): - return lines, 1 - return inspect.getblock(lines[lineno:]), lineno+1 + if funcdef: + try: + code = compile(funcdef, filename, 'exec') + except SyntaxError: + continue + # We should always be able to find the code object here + funccode = next(c for c in code.co_consts if + isinstance(c, CodeType) and c.co_name == funcname) + lineno_offset = find_first_executable_line(funccode) + return funcname, filename, funcstart + lineno_offset - 1 + return None def lasti2lineno(code, lasti): linestarts = list(dis.findlinestarts(code)) @@ -127,6 +175,153 @@ class _rstr(str): return self +class _ExecutableTarget: + filename: str + code: CodeType | str + namespace: dict + + +class _ScriptTarget(_ExecutableTarget): + def __init__(self, target): + self._check(target) + self._target = self._safe_realpath(target) + + # If PYTHONSAFEPATH (-P) is not set, sys.path[0] is the directory + # of pdb, and we should replace it with the directory of the script + if not sys.flags.safe_path: + sys.path[0] = os.path.dirname(self._target) + + @staticmethod + def _check(target): + """ + Check that target is plausibly a script. + """ + if not os.path.exists(target): + print(f'Error: {target} does not exist') + sys.exit(1) + if os.path.isdir(target): + print(f'Error: {target} is a directory') + sys.exit(1) + + @staticmethod + def _safe_realpath(path): + """ + Return the canonical path (realpath) if it is accessible from the userspace. + Otherwise (for example, if the path is a symlink to an anonymous pipe), + return the original path. + + See GH-142315. + """ + realpath = os.path.realpath(path) + return realpath if os.path.exists(realpath) else path + + def __repr__(self): + return self._target + + @property + def filename(self): + return self._target + + @property + def code(self): + # Open the file each time because the file may be modified + with io.open_code(self._target) as fp: + return f"exec(compile({fp.read()!r}, {self._target!r}, 'exec'))" + + @property + def namespace(self): + return dict( + __name__='__main__', + __file__=self._target, + __builtins__=__builtins__, + __spec__=None, + ) + + +class _ModuleTarget(_ExecutableTarget): + def __init__(self, target): + self._target = target + + import runpy + try: + _, self._spec, self._code = runpy._get_module_details(self._target) + except ImportError as e: + print(f"ImportError: {e}") + sys.exit(1) + except Exception: + traceback.print_exc() + sys.exit(1) + + def __repr__(self): + return self._target + + @property + def filename(self): + return self._code.co_filename + + @property + def code(self): + return self._code + + @property + def namespace(self): + return dict( + __name__='__main__', + __file__=os.path.normcase(os.path.abspath(self.filename)), + __package__=self._spec.parent, + __loader__=self._spec.loader, + __spec__=self._spec, + __builtins__=__builtins__, + ) + + +class _ZipTarget(_ExecutableTarget): + def __init__(self, target): + import runpy + + self._target = os.path.realpath(target) + sys.path.insert(0, self._target) + try: + _, self._spec, self._code = runpy._get_main_module_details() + except ImportError as e: + print(f"ImportError: {e}") + sys.exit(1) + except Exception: + traceback.print_exc() + sys.exit(1) + + def __repr__(self): + return self._target + + @property + def filename(self): + return self._code.co_filename + + @property + def code(self): + return self._code + + @property + def namespace(self): + return dict( + __name__='__main__', + __file__=os.path.normcase(os.path.abspath(self.filename)), + __package__=self._spec.parent, + __loader__=self._spec.loader, + __spec__=self._spec, + __builtins__=__builtins__, + ) + + +class _PdbInteractiveConsole(code.InteractiveConsole): + def __init__(self, ns, message): + self._message = message + super().__init__(locals=ns, local_exit=True) + + def write(self, data): + self._message(data, end='') + + # Interaction prompt line will separate file and call info from code # text using value of line_prefix string. A newline and arrow may # be to your liking. You can set it once pdb is imported using the @@ -134,13 +329,39 @@ class _rstr(str): # line_prefix = ': ' # Use this to get the old situation back line_prefix = '\n-> ' # Probably a better default -class Pdb(bdb.Bdb, cmd.Cmd): +# The default backend to use for Pdb instances if not specified +# Should be either 'settrace' or 'monitoring' +_default_backend = 'settrace' + + +def set_default_backend(backend): + """Set the default backend to use for Pdb instances.""" + global _default_backend + if backend not in ('settrace', 'monitoring'): + raise ValueError("Invalid backend: %s" % backend) + _default_backend = backend + + +def get_default_backend(): + """Get the default backend to use for Pdb instances.""" + return _default_backend + + +class Pdb(bdb.Bdb, cmd.Cmd): _previous_sigint_handler = None + # Limit the maximum depth of chained exceptions, we should be handling cycles, + # but in case there are recursions, we stop at 999. + MAX_CHAINED_EXCEPTION_DEPTH = 999 + + _file_mtime_table = {} + + _last_pdb_instance = None + def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None, - nosigint=False, readrc=True): - bdb.Bdb.__init__(self, skip=skip) + nosigint=False, readrc=True, mode=None, backend=None, colorize=False): + bdb.Bdb.__init__(self, skip=skip, backend=backend if backend else get_default_backend()) cmd.Cmd.__init__(self, completekey, stdin, stdout) sys.audit("pdb.Pdb") if stdout: @@ -151,41 +372,52 @@ class Pdb(bdb.Bdb, cmd.Cmd): self.mainpyfile = '' self._wait_for_mainpyfile = False self.tb_lineno = {} + self.mode = mode + self.colorize = colorize and _colorize.can_colorize(file=stdout or sys.stdout) # Try to load readline if it exists try: import readline # remove some common file name delimiters - readline.set_completer_delims(' \t\n`@#$%^&*()=+[{]}\\|;:\'",<>?') + readline.set_completer_delims(' \t\n`@#%^&*()=+[{]}\\|;:\'",<>?') except ImportError: pass + self.allow_kbdint = False self.nosigint = nosigint + # Consider these characters as part of the command so when the users type + # c.a or c['a'], it won't be recognized as a c(ontinue) command + self.identchars = cmd.Cmd.identchars + '=.[](),"\'+-*/%@&|<>~^' # Read ~/.pdbrc and ./.pdbrc self.rcLines = [] if readrc: try: - with open(os.path.expanduser('~/.pdbrc')) as rcFile: + with open(os.path.expanduser('~/.pdbrc'), encoding='utf-8') as rcFile: self.rcLines.extend(rcFile) except OSError: pass try: - with open(".pdbrc") as rcFile: + with open(".pdbrc", encoding='utf-8') as rcFile: self.rcLines.extend(rcFile) except OSError: pass self.commands = {} # associates a command list to breakpoint numbers - self.commands_doprompt = {} # for each bp num, tells if the prompt - # must be disp. after execing the cmd list - self.commands_silent = {} # for each bp num, tells if the stack trace - # must be disp. after execing the cmd list self.commands_defining = False # True while in the process of defining # a command list self.commands_bnum = None # The breakpoint number for which we are # defining a list + self.async_shim_frame = None + self.async_awaitable = None + + self._chained_exceptions = tuple() + self._chained_exception_index = 0 + + self._current_task = None + def set_trace(self, frame=None, *, commands=None): + Pdb._last_pdb_instance = self if frame is None: frame = sys._getframe().f_back @@ -194,6 +426,57 @@ class Pdb(bdb.Bdb, cmd.Cmd): super().set_trace(frame) + async def set_trace_async(self, frame=None, *, commands=None): + if self.async_awaitable is not None: + # We are already in a set_trace_async call, do not mess with it + return + + if frame is None: + frame = sys._getframe().f_back + + # We need set_trace to set up the basics, however, this will call + # set_stepinstr() will we need to compensate for, because we don't + # want to trigger on calls + self.set_trace(frame, commands=commands) + # Changing the stopframe will disable trace dispatch on calls + self.stopframe = frame + # We need to stop tracing because we don't have the privilege to avoid + # triggering tracing functions as normal, as we are not already in + # tracing functions + self.stop_trace() + + self.async_shim_frame = sys._getframe() + self.async_awaitable = None + + while True: + self.async_awaitable = None + # Simulate a trace event + # This should bring up pdb and make pdb believe it's debugging the + # caller frame + self.trace_dispatch(frame, "opcode", None) + if self.async_awaitable is not None: + try: + if self.breaks: + with self.set_enterframe(frame): + # set_continue requires enterframe to work + self.set_continue() + self.start_trace() + await self.async_awaitable + except Exception: + self._error_exc() + else: + break + + self.async_shim_frame = None + + # start the trace (the actual command is already set by set_* calls) + if self.returnframe is None and self.stoplineno == -1 and not self.breaks: + # This means we did a continue without any breakpoints, we should not + # start the trace + return + + self.start_trace() + def sigint_handler(self, signum, frame): if self.allow_kbdint: raise KeyboardInterrupt @@ -209,6 +492,8 @@ class Pdb(bdb.Bdb, cmd.Cmd): self.lineno = None self.stack = [] self.curindex = 0 + if hasattr(self, 'curframe') and self.curframe: + self.curframe.f_globals.pop('__pdb_convenience_variables', None) self.curframe = None self.tb_lineno.clear() @@ -223,30 +508,34 @@ class Pdb(bdb.Bdb, cmd.Cmd): self.tb_lineno[tb.tb_frame] = lineno tb = tb.tb_next self.curframe = self.stack[self.curindex][0] - # The f_locals dictionary is updated from the actual frame - # locals whenever the .f_locals accessor is called, so we - # cache it here to ensure that modifications are not overwritten. - self.curframe_locals = self.curframe.f_locals - return self.execRcLines() + self.set_convenience_variable(self.curframe, '_frame', self.curframe) + if self._current_task: + self.set_convenience_variable(self.curframe, '_asynctask', self._current_task) + self._save_initial_file_mtime(self.curframe) - # Can be executed earlier than 'setup' if desired - def execRcLines(self): - if not self.rcLines: - return - # local copy because of recursion - rcLines = self.rcLines - rcLines.reverse() - # execute every line only once - self.rcLines = [] - while rcLines: - line = rcLines.pop().strip() - if line and line[0] != '#': - if self.onecmd(line): - # if onecmd returns True, the command wants to exit - # from the interaction, save leftover rc lines - # to execute before next interaction - self.rcLines += reversed(rcLines) - return True + if self._chained_exceptions: + self.set_convenience_variable( + self.curframe, + '_exception', + self._chained_exceptions[self._chained_exception_index], + ) + + if self.rcLines: + self.cmdqueue = [ + line for line in self.rcLines + if line.strip() and not line.strip().startswith("#") + ] + self.rcLines = [] + + @property + @deprecated("The frame locals reference is no longer cached. Use 'curframe.f_locals' instead.") + def curframe_locals(self): + return self.curframe.f_locals + + @curframe_locals.setter + @deprecated("Setting 'curframe_locals' no longer has any effect. Update the contents of 'curframe.f_locals' instead.") + def curframe_locals(self, value): + pass # Override Bdb methods @@ -262,12 +551,20 @@ class Pdb(bdb.Bdb, cmd.Cmd): def user_line(self, frame): """This function is called when we stop or break at this line.""" if self._wait_for_mainpyfile: - if (self.mainpyfile != self.canonic(frame.f_code.co_filename) - or frame.f_lineno <= 0): + if (self.mainpyfile != self.canonic(frame.f_code.co_filename)): return self._wait_for_mainpyfile = False - if self.bp_commands(frame): - self.interaction(frame, None) + if self.trace_opcodes: + # GH-127321 + # We want to avoid stopping at an opcode that does not have + # an associated line number because pdb does not like it + if frame.f_lineno is None: + self.set_stepinstr() + return + self.bp_commands(frame) + self.interaction(frame, None) + + user_opcode = user_line def bp_commands(self, frame): """Call every command that was set for the current active breakpoint @@ -280,24 +577,16 @@ class Pdb(bdb.Bdb, cmd.Cmd): self.currentbp in self.commands: currentbp = self.currentbp self.currentbp = 0 - lastcmd_back = self.lastcmd - self.setup(frame, None) for line in self.commands[currentbp]: - self.onecmd(line) - self.lastcmd = lastcmd_back - if not self.commands_silent[currentbp]: - self.print_stack_entry(self.stack[self.curindex]) - if self.commands_doprompt[currentbp]: - self._cmdloop() - self.forget() - return - return 1 + self.cmdqueue.append(line) + self.cmdqueue.append(f'_pdbcmd_restore_lastcmd {self.lastcmd}') def user_return(self, frame, return_value): """This function is called when a return trap is set here.""" if self._wait_for_mainpyfile: return frame.f_locals['__return__'] = return_value + self.set_convenience_variable(frame, '_retval', return_value) self.message('--Return--') self.interaction(frame, None) @@ -308,6 +597,7 @@ class Pdb(bdb.Bdb, cmd.Cmd): return exc_type, exc_value, exc_traceback = exc_info frame.f_locals['__exception__'] = exc_type, exc_value + self.set_convenience_variable(frame, '_exception', exc_value) # An 'Internal StopIteration' exception is an exception debug event # issued by the interpreter when handling a subgenerator run with @@ -316,8 +606,7 @@ class Pdb(bdb.Bdb, cmd.Cmd): # stop when the debuggee is returning from such generators. prefix = 'Internal ' if (not exc_traceback and exc_type is StopIteration) else '' - self.message('%s%s' % (prefix, - traceback.format_exception_only(exc_type, exc_value)[-1].strip())) + self.message('%s%s' % (prefix, self._format_exc(exc_value))) self.interaction(frame, exc_traceback) # General interaction function @@ -333,8 +622,35 @@ class Pdb(bdb.Bdb, cmd.Cmd): except KeyboardInterrupt: self.message('--KeyboardInterrupt--') + def _save_initial_file_mtime(self, frame): + """save the mtime of the all the files in the frame stack in the file mtime table + if they haven't been saved yet.""" + while frame: + filename = frame.f_code.co_filename + if filename not in self._file_mtime_table: + try: + self._file_mtime_table[filename] = os.path.getmtime(filename) + except Exception: + pass + frame = frame.f_back + + def _validate_file_mtime(self): + """Check if the source file of the current frame has been modified. + If so, give a warning and reset the modify time to current.""" + try: + filename = self.curframe.f_code.co_filename + mtime = os.path.getmtime(filename) + except Exception: + return + if (filename in self._file_mtime_table and + mtime != self._file_mtime_table[filename]): + self.message(f"*** WARNING: file '{filename}' was edited, " + "running stale code until the program is rerun") + self._file_mtime_table[filename] = mtime + # Called before loop, handles display expressions - def preloop(self): + # Set up convenience variable containers + def _show_display(self): displaying = self.displaying.get(self.curframe) if displaying: for expr, oldvalue in displaying.items(): @@ -344,10 +660,75 @@ class Pdb(bdb.Bdb, cmd.Cmd): # fields are changed to be displayed if newvalue is not oldvalue and newvalue != oldvalue: displaying[expr] = newvalue - self.message('display %s: %r [old: %r]' % - (expr, newvalue, oldvalue)) + self.message('display %s: %s [old: %s]' % + (expr, self._safe_repr(newvalue, expr), + self._safe_repr(oldvalue, expr))) - def interaction(self, frame, traceback): + def _get_tb_and_exceptions(self, tb_or_exc): + """ + Given a tracecack or an exception, return a tuple of chained exceptions + and current traceback to inspect. + + This will deal with selecting the right ``__cause__`` or ``__context__`` + as well as handling cycles, and return a flattened list of exceptions we + can jump to with do_exceptions. + + """ + _exceptions = [] + if isinstance(tb_or_exc, BaseException): + traceback, current = tb_or_exc.__traceback__, tb_or_exc + + while current is not None: + if current in _exceptions: + break + _exceptions.append(current) + if current.__cause__ is not None: + current = current.__cause__ + elif ( + current.__context__ is not None and not current.__suppress_context__ + ): + current = current.__context__ + + if len(_exceptions) >= self.MAX_CHAINED_EXCEPTION_DEPTH: + self.message( + f"More than {self.MAX_CHAINED_EXCEPTION_DEPTH}" + " chained exceptions found, not all exceptions" + "will be browsable with `exceptions`." + ) + break + else: + traceback = tb_or_exc + return tuple(reversed(_exceptions)), traceback + + @contextmanager + def _hold_exceptions(self, exceptions): + """ + Context manager to ensure proper cleaning of exceptions references + + When given a chained exception instead of a traceback, + pdb may hold references to many objects which may leak memory. + + We use this context manager to make sure everything is properly cleaned + + """ + try: + self._chained_exceptions = exceptions + self._chained_exception_index = len(exceptions) - 1 + yield + finally: + # we can't put those in forget as otherwise they would + # be cleared on exception change + self._chained_exceptions = tuple() + self._chained_exception_index = 0 + + def _get_asyncio_task(self): + try: + task = asyncio.current_task() + except RuntimeError: + task = None + return task + + def interaction(self, frame, tb_or_exc): # Restore the previous signal handler at the Pdb prompt. if Pdb._previous_sigint_handler: try: @@ -356,14 +737,25 @@ class Pdb(bdb.Bdb, cmd.Cmd): pass else: Pdb._previous_sigint_handler = None - if self.setup(frame, traceback): - # no interaction desired at this time (happens if .pdbrc contains - # a command like "continue") + + self._current_task = self._get_asyncio_task() + + _chained_exceptions, tb = self._get_tb_and_exceptions(tb_or_exc) + if isinstance(tb_or_exc, BaseException): + assert tb is not None, "main exception must have a traceback" + with self._hold_exceptions(_chained_exceptions): + self.setup(frame, tb) + # We should print the stack entry if and only if the user input + # is expected, and we should print it right before the user input. + # We achieve this by appending _pdbcmd_print_frame_status to the + # command queue. If cmdqueue is not exhausted, the user input is + # not expected and we will not print the stack entry. + self.cmdqueue.append('_pdbcmd_print_frame_status') + self._cmdloop() + # If _pdbcmd_print_frame_status is not used, pop it out + if self.cmdqueue and self.cmdqueue[-1] == '_pdbcmd_print_frame_status': + self.cmdqueue.pop() self.forget() - return - self.print_stack_entry(self.stack[self.curindex]) - self._cmdloop() - self.forget() def displayhook(self, obj): """Custom displayhook for the exec in default(), which prevents @@ -373,12 +765,191 @@ class Pdb(bdb.Bdb, cmd.Cmd): if obj is not None: self.message(repr(obj)) + @contextmanager + def _enable_multiline_input(self): + try: + import readline + except ImportError: + yield + return + + def input_auto_indent(): + last_index = readline.get_current_history_length() + last_line = readline.get_history_item(last_index) + if last_line: + if last_line.isspace(): + # If the last line is empty, we don't need to indent + return + + last_line = last_line.rstrip('\r\n') + indent = len(last_line) - len(last_line.lstrip()) + if last_line.endswith(":"): + indent += 4 + readline.insert_text(' ' * indent) + + completenames = self.completenames + try: + self.completenames = self.complete_multiline_names + readline.set_startup_hook(input_auto_indent) + yield + finally: + readline.set_startup_hook() + self.completenames = completenames + return + + def _exec_in_closure(self, source, globals, locals): + """ Run source code in closure so code object created within source + can find variables in locals correctly + + returns True if the source is executed, False otherwise + """ + + # Determine if the source should be executed in closure. Only when the + # source compiled to multiple code objects, we should use this feature. + # Otherwise, we can just raise an exception and normal exec will be used. + + code = compile(source, "", "exec") + if not any(isinstance(const, CodeType) for const in code.co_consts): + return False + + # locals could be a proxy which does not support pop + # copy it first to avoid modifying the original locals + locals_copy = dict(locals) + + locals_copy["__pdb_eval__"] = { + "result": None, + "write_back": {} + } + + # If the source is an expression, we need to print its value + try: + compile(source, "", "eval") + except SyntaxError: + pass + else: + source = "__pdb_eval__['result'] = " + source + + # Add write-back to update the locals + source = ("try:\n" + + textwrap.indent(source, " ") + "\n" + + "finally:\n" + + " __pdb_eval__['write_back'] = locals()") + + # Build a closure source code with freevars from locals like: + # def __pdb_outer(): + # var = None + # def __pdb_scope(): # This is the code object we want to execute + # nonlocal var + # + # return __pdb_scope.__code__ + source_with_closure = ("def __pdb_outer():\n" + + "\n".join(f" {var} = None" for var in locals_copy) + "\n" + + " def __pdb_scope():\n" + + "\n".join(f" nonlocal {var}" for var in locals_copy) + "\n" + + textwrap.indent(source, " ") + "\n" + + " return __pdb_scope.__code__" + ) + + # Get the code object of __pdb_scope() + # The exec fills locals_copy with the __pdb_outer() function and we can call + # that to get the code object of __pdb_scope() + ns = {} + try: + exec(source_with_closure, {}, ns) + except Exception: + return False + code = ns["__pdb_outer"]() + + cells = tuple(types.CellType(locals_copy.get(var)) for var in code.co_freevars) + + try: + exec(code, globals, locals_copy, closure=cells) + except Exception: + return False + + # get the data we need from the statement + pdb_eval = locals_copy["__pdb_eval__"] + + # __pdb_eval__ should not be updated back to locals + pdb_eval["write_back"].pop("__pdb_eval__") + + # Write all local variables back to locals + locals.update(pdb_eval["write_back"]) + eval_result = pdb_eval["result"] + if eval_result is not None: + self.message(repr(eval_result)) + + return True + + def _exec_await(self, source, globals, locals): + """ Run source code that contains await by playing with async shim frame""" + # Put the source in an async function + source_async = ( + "async def __pdb_await():\n" + + textwrap.indent(source, " ") + '\n' + + " __pdb_locals.update(locals())" + ) + ns = globals | locals + # We use __pdb_locals to do write back + ns["__pdb_locals"] = locals + exec(source_async, ns) + self.async_awaitable = ns["__pdb_await"]() + + def _read_code(self, line): + buffer = line + is_await_code = False + code = None + try: + if (code := codeop.compile_command(line + '\n', '', 'single')) is None: + # Multi-line mode + with self._enable_multiline_input(): + buffer = line + continue_prompt = "... " + while (code := codeop.compile_command(buffer, '', 'single')) is None: + if self.use_rawinput: + try: + line = input(continue_prompt) + except (EOFError, KeyboardInterrupt): + self.lastcmd = "" + print('\n') + return None, None, False + else: + self.stdout.write(continue_prompt) + self.stdout.flush() + line = self.stdin.readline() + if not len(line): + self.lastcmd = "" + self.stdout.write('\n') + self.stdout.flush() + return None, None, False + else: + line = line.rstrip('\r\n') + if line.isspace(): + # empty line, just continue + buffer += '\n' + else: + buffer += '\n' + line + self.lastcmd = buffer + except SyntaxError as e: + # Maybe it's an await expression/statement + if ( + self.async_shim_frame is not None + and e.msg == "'await' outside function" + ): + is_await_code = True + else: + raise + + return code, buffer, is_await_code + def default(self, line): - if line[:1] == '!': line = line[1:] - locals = self.curframe_locals + if line[:1] == '!': line = line[1:].strip() + locals = self.curframe.f_locals globals = self.curframe.f_globals try: - code = compile(line + '\n', '', 'single') + code, buffer, is_await_code = self._read_code(line) + if buffer is None: + return save_stdout = sys.stdout save_stdin = sys.stdin save_displayhook = sys.displayhook @@ -386,14 +957,51 @@ class Pdb(bdb.Bdb, cmd.Cmd): sys.stdin = self.stdin sys.stdout = self.stdout sys.displayhook = self.displayhook - exec(code, globals, locals) + if is_await_code: + self._exec_await(buffer, globals, locals) + return True + else: + if not self._exec_in_closure(buffer, globals, locals): + exec(code, globals, locals) finally: sys.stdout = save_stdout sys.stdin = save_stdin sys.displayhook = save_displayhook except: - exc_info = sys.exc_info()[:2] - self.error(traceback.format_exception_only(*exc_info)[-1].strip()) + self._error_exc() + + def _replace_convenience_variables(self, line): + """Replace the convenience variables in 'line' with their values. + e.g. $foo is replaced by __pdb_convenience_variables["foo"]. + Note: such pattern in string literals will be skipped""" + + if "$" not in line: + return line + + dollar_start = dollar_end = (-1, -1) + replace_variables = [] + try: + for t in tokenize.generate_tokens(io.StringIO(line).readline): + token_type, token_string, start, end, _ = t + if token_type == token.OP and token_string == '$': + dollar_start, dollar_end = start, end + elif start == dollar_end and token_type == token.NAME: + # line is a one-line command so we only care about column + replace_variables.append((dollar_start[1], end[1], token_string)) + except tokenize.TokenError: + return line + + if not replace_variables: + return line + + last_end = 0 + line_pieces = [] + for start, end, name in replace_variables: + line_pieces.append(line[last_end:start] + f'__pdb_convenience_variables["{name}"]') + last_end = end + line_pieces.append(line[last_end:]) + + return ''.join(line_pieces) def precmd(self, line): """Handle alias expansion and ';;' separator.""" @@ -402,11 +1010,20 @@ class Pdb(bdb.Bdb, cmd.Cmd): args = line.split() while args[0] in self.aliases: line = self.aliases[args[0]] - ii = 1 - for tmpArg in args[1:]: - line = line.replace("%" + str(ii), - tmpArg) - ii += 1 + for idx in range(1, 10): + if f'%{idx}' in line: + if idx >= len(args): + self.error(f"Not enough arguments for alias '{args[0]}'") + # This is a no-op + return "!" + line = line.replace(f'%{idx}', args[idx]) + elif '%*' not in line: + if idx < len(args): + self.error(f"Too many arguments for alias '{args[0]}'") + # This is a no-op + return "!" + break + line = line.replace("%*", ' '.join(args[1:])) args = line.split() # split into ';;' separated commands @@ -416,8 +1033,12 @@ class Pdb(bdb.Bdb, cmd.Cmd): if marker >= 0: # queue up everything after marker next = line[marker+2:].lstrip() - self.cmdqueue.append(next) + self.cmdqueue.insert(0, next) line = line[:marker].rstrip() + + # Replace all the convenience variables + line = self._replace_convenience_variables(line) + return line def onecmd(self, line): @@ -428,6 +1049,10 @@ class Pdb(bdb.Bdb, cmd.Cmd): a breakpoint command list definition. """ if not self.commands_defining: + if line.startswith('_pdbcmd'): + command, arg, line = self.parseline(line) + if hasattr(self, command): + return getattr(self, command)(arg) return cmd.Cmd.onecmd(self, line) else: return self.handle_command_def(line) @@ -436,14 +1061,16 @@ class Pdb(bdb.Bdb, cmd.Cmd): """Handles one command line during command list definition.""" cmd, arg, line = self.parseline(line) if not cmd: - return - if cmd == 'silent': - self.commands_silent[self.commands_bnum] = True - return # continue to handle other cmd def in the cmd list - elif cmd == 'end': - self.cmdqueue = [] - return 1 # end of cmd list + return False + if cmd == 'end': + return True # end of cmd list + elif cmd == 'EOF': + self.message('') + return True # end of cmd list cmdlist = self.commands[self.commands_bnum] + if cmd == 'silent': + cmdlist.append('_pdbcmd_silence_frame_status') + return False # continue to handle other cmd def in the cmd list if arg: cmdlist.append(cmd+' '+arg) else: @@ -455,22 +1082,74 @@ class Pdb(bdb.Bdb, cmd.Cmd): func = self.default # one of the resuming commands if func.__name__ in self.commands_resuming: - self.commands_doprompt[self.commands_bnum] = False - self.cmdqueue = [] - return 1 - return + return True + return False + + def _colorize_code(self, code): + if self.colorize: + colors = list(_pyrepl.utils.gen_colors(code)) + chars, _ = _pyrepl.utils.disp_str(code, colors=colors, force_color=True) + code = "".join(chars) + return code # interface abstraction functions - def message(self, msg): - print(msg, file=self.stdout) + def message(self, msg, end='\n'): + print(msg, end=end, file=self.stdout) def error(self, msg): print('***', msg, file=self.stdout) + # convenience variables + + def set_convenience_variable(self, frame, name, value): + if '__pdb_convenience_variables' not in frame.f_globals: + frame.f_globals['__pdb_convenience_variables'] = {} + frame.f_globals['__pdb_convenience_variables'][name] = value + # Generic completion functions. Individual complete_foo methods can be # assigned below to one of these functions. + @property + def rlcompleter(self): + """Return the `Completer` class from `rlcompleter`, while avoiding the + side effects of changing the completer from `import rlcompleter`. + + This is a compromise between GH-138860 and GH-139289. If GH-139289 is + fixed, then we don't need this and we can just `import rlcompleter` in + `Pdb.__init__`. + """ + if not hasattr(self, "_rlcompleter"): + try: + import readline + except ImportError: + # readline is not available, just get the Completer + from rlcompleter import Completer + self._rlcompleter = Completer + else: + # importing rlcompleter could have side effect of changing + # the current completer, we need to restore it + prev_completer = readline.get_completer() + from rlcompleter import Completer + self._rlcompleter = Completer + readline.set_completer(prev_completer) + return self._rlcompleter + + def completenames(self, text, line, begidx, endidx): + # Overwrite completenames() of cmd so for the command completion, + # if no current command matches, check for expressions as well + commands = super().completenames(text, line, begidx, endidx) + for alias in self.aliases: + if alias.startswith(text): + commands.append(alias) + if commands: + return commands + else: + expressions = self._complete_expression(text, line, begidx, endidx) + if expressions: + return expressions + return self.completedefault(text, line, begidx, endidx) + def _complete_location(self, text, line, begidx, endidx): # Complete a file/module/function location for break/tbreak/clear. if line.strip().endswith((':', ',')): @@ -482,7 +1161,7 @@ class Pdb(bdb.Bdb, cmd.Cmd): except Exception: ret = [] # Then, try to complete file names as well. - globs = glob.glob(text + '*') + globs = glob.glob(glob.escape(text) + '*') for fn in globs: if os.path.isdir(fn): ret.append(fn + '/') @@ -504,14 +1183,17 @@ class Pdb(bdb.Bdb, cmd.Cmd): # Collect globals and locals. It is usually not really sensible to also # complete builtins, and they clutter the namespace quite heavily, so we # leave them out. - ns = {**self.curframe.f_globals, **self.curframe_locals} + ns = {**self.curframe.f_globals, **self.curframe.f_locals} if '.' in text: # Walk an attribute chain up to the last part, similar to what # rlcompleter does. This will bail if any of the parts are not # simple attribute access, which is what we want. dotted = text.split('.') try: - obj = ns[dotted[0]] + if dotted[0].startswith('$'): + obj = self.curframe.f_globals['__pdb_convenience_variables'][dotted[0][1:]] + else: + obj = ns[dotted[0]] for part in dotted[1:-1]: obj = getattr(obj, part) except (KeyError, AttributeError): @@ -519,15 +1201,78 @@ class Pdb(bdb.Bdb, cmd.Cmd): prefix = '.'.join(dotted[:-1]) + '.' return [prefix + n for n in dir(obj) if n.startswith(dotted[-1])] else: + if text.startswith("$"): + # Complete convenience variables + conv_vars = self.curframe.f_globals.get('__pdb_convenience_variables', {}) + return [f"${name}" for name in conv_vars if name.startswith(text[1:])] # Complete a simple name. return [n for n in ns.keys() if n.startswith(text)] + def _complete_indentation(self, text, line, begidx, endidx): + try: + import readline + except ImportError: + return [] + # Fill in spaces to form a 4-space indent + return [' ' * (4 - readline.get_begidx() % 4)] + + def complete_multiline_names(self, text, line, begidx, endidx): + # If text is space-only, the user entered before any text. + # That normally means they want to indent the current line. + if not text.strip(): + return self._complete_indentation(text, line, begidx, endidx) + return self.completedefault(text, line, begidx, endidx) + + def completedefault(self, text, line, begidx, endidx): + if text.startswith("$"): + # Complete convenience variables + conv_vars = self.curframe.f_globals.get('__pdb_convenience_variables', {}) + return [f"${name}" for name in conv_vars if name.startswith(text[1:])] + + state = 0 + matches = [] + completer = self.rlcompleter(self.curframe.f_globals | self.curframe.f_locals) + while (match := completer.complete(text, state)) is not None: + matches.append(match) + state += 1 + return matches + + @contextmanager + def _enable_rlcompleter(self, ns): + try: + import readline + except ImportError: + yield + return + + try: + completer = self.rlcompleter(ns) + old_completer = readline.get_completer() + readline.set_completer(completer.complete) + yield + finally: + readline.set_completer(old_completer) + + # Pdb meta commands, only intended to be used internally by pdb + + def _pdbcmd_print_frame_status(self, arg): + self.print_stack_trace(0) + self._validate_file_mtime() + self._show_display() + + def _pdbcmd_silence_frame_status(self, arg): + if self.cmdqueue and self.cmdqueue[-1] == '_pdbcmd_print_frame_status': + self.cmdqueue.pop() + + def _pdbcmd_restore_lastcmd(self, arg): + self.lastcmd = arg + # Command definitions, called by cmdloop() # The argument is the remaining string on the command line # Return true to exit from the command loop def do_commands(self, arg): - """commands [bpnumber] + """(Pdb) commands [bpnumber] (com) ... (com) end (Pdb) @@ -569,19 +1314,21 @@ class Pdb(bdb.Bdb, cmd.Cmd): try: bnum = int(arg) except: - self.error("Usage: commands [bnum]\n ...\n end") + self._print_invalid_arg(arg) return + try: + self.get_bpbynumber(bnum) + except ValueError as err: + self.error('cannot set commands: %s' % err) + return + self.commands_bnum = bnum # Save old definitions for the case of a keyboard interrupt. if bnum in self.commands: - old_command_defs = (self.commands[bnum], - self.commands_doprompt[bnum], - self.commands_silent[bnum]) + old_commands = self.commands[bnum] else: - old_command_defs = None + old_commands = None self.commands[bnum] = [] - self.commands_doprompt[bnum] = True - self.commands_silent[bnum] = False prompt_back = self.prompt self.prompt = '(com) ' @@ -590,14 +1337,10 @@ class Pdb(bdb.Bdb, cmd.Cmd): self.cmdloop() except KeyboardInterrupt: # Restore old definitions. - if old_command_defs: - self.commands[bnum] = old_command_defs[0] - self.commands_doprompt[bnum] = old_command_defs[1] - self.commands_silent[bnum] = old_command_defs[2] + if old_commands: + self.commands[bnum] = old_commands else: del self.commands[bnum] - del self.commands_doprompt[bnum] - del self.commands_silent[bnum] self.error('command definition aborted, old commands restored') finally: self.commands_defining = False @@ -605,8 +1348,9 @@ class Pdb(bdb.Bdb, cmd.Cmd): complete_commands = _complete_bpnumber - def do_break(self, arg, temporary = 0): + def do_break(self, arg, temporary=False): """b(reak) [ ([filename:]lineno | function) [, condition] ] + Without argument, list all breaks. With a line number argument, set a break at this line in the @@ -632,10 +1376,14 @@ class Pdb(bdb.Bdb, cmd.Cmd): filename = None lineno = None cond = None + module_globals = None comma = arg.find(',') if comma > 0: # parse stuff after comma: "condition" cond = arg[comma+1:].lstrip() + if err := self._compile_error_message(cond): + self.error('Invalid condition %s: %r' % (cond, err)) + return arg = arg[:comma].rstrip() # parse stuff before comma: [filename:]lineno | function colon = arg.rfind(':') @@ -662,7 +1410,7 @@ class Pdb(bdb.Bdb, cmd.Cmd): try: func = eval(arg, self.curframe.f_globals, - self.curframe_locals) + self.curframe.f_locals) except: func = arg try: @@ -672,8 +1420,9 @@ class Pdb(bdb.Bdb, cmd.Cmd): #use co_name to identify the bkpt (function names #could be aliased, but co_name is invariant) funcname = code.co_name - lineno = code.co_firstlineno + lineno = find_first_executable_line(code) filename = code.co_filename + module_globals = func.__globals__ except: # last thing to try (ok, filename, ln) = self.lineinfo(arg) @@ -685,8 +1434,9 @@ class Pdb(bdb.Bdb, cmd.Cmd): lineno = int(ln) if not filename: filename = self.defaultFile() + filename = self.canonic(filename) # Check for reasonable breakpoint - line = self.checkline(filename, lineno) + line = self.checkline(filename, lineno, module_globals) if line: # now set the break point err = self.set_break(filename, line, temporary, cond, funcname) @@ -712,10 +1462,11 @@ class Pdb(bdb.Bdb, cmd.Cmd): def do_tbreak(self, arg): """tbreak [ ([filename:]lineno | function) [, condition] ] + Same arguments as break, but sets a temporary breakpoint: it is automatically deleted when first hit. """ - self.do_break(arg, 1) + self.do_break(arg, True) complete_tbreak = _complete_location @@ -748,11 +1499,13 @@ class Pdb(bdb.Bdb, cmd.Cmd): f = self.lookupmodule(parts[0]) if f: fname = f - item = parts[1] - answer = find_function(item, fname) + item = parts[1] + else: + return failed + answer = find_function(item, self.canonic(fname)) return answer or failed - def checkline(self, filename, lineno): + def checkline(self, filename, lineno, module_globals=None): """Check whether specified line seems to be executable. Return `lineno` if it is, 0 if not (e.g. a docstring, comment, blank @@ -760,8 +1513,10 @@ class Pdb(bdb.Bdb, cmd.Cmd): """ # this method should be callable before starting debugging, so default # to "no globals" if there is no current frame - globs = self.curframe.f_globals if hasattr(self, 'curframe') else None - line = linecache.getline(filename, lineno, globs) + frame = getattr(self, 'curframe', None) + if module_globals is None: + module_globals = frame.f_globals if frame else None + line = linecache.getline(filename, lineno, module_globals) if not line: self.message('End of file') return 0 @@ -775,9 +1530,13 @@ class Pdb(bdb.Bdb, cmd.Cmd): def do_enable(self, arg): """enable bpnumber [bpnumber ...] + Enables the breakpoints given as a space separated list of breakpoint numbers. """ + if not arg: + self._print_invalid_arg(arg) + return args = arg.split() for i in args: try: @@ -792,12 +1551,16 @@ class Pdb(bdb.Bdb, cmd.Cmd): def do_disable(self, arg): """disable bpnumber [bpnumber ...] + Disables the breakpoints given as a space separated list of breakpoint numbers. Disabling a breakpoint means it cannot cause the program to stop execution, but unlike clearing a breakpoint, it remains in the list of breakpoints and can be (re-)enabled. """ + if not arg: + self._print_invalid_arg(arg) + return args = arg.split() for i in args: try: @@ -812,14 +1575,21 @@ class Pdb(bdb.Bdb, cmd.Cmd): def do_condition(self, arg): """condition bpnumber [condition] + Set a new condition for the breakpoint, an expression which must evaluate to true before the breakpoint is honored. If condition is absent, any existing condition is removed; i.e., the breakpoint is made unconditional. """ + if not arg: + self._print_invalid_arg(arg) + return args = arg.split(' ', 1) try: cond = args[1] + if err := self._compile_error_message(cond): + self.error('Invalid condition %s: %r' % (cond, err)) + return except IndexError: cond = None try: @@ -839,6 +1609,7 @@ class Pdb(bdb.Bdb, cmd.Cmd): def do_ignore(self, arg): """ignore bpnumber [count] + Set the ignore count for the given breakpoint number. If count is omitted, the ignore count is set to 0. A breakpoint becomes active when the ignore count is zero. When non-zero, @@ -846,15 +1617,26 @@ class Pdb(bdb.Bdb, cmd.Cmd): and the breakpoint is not disabled and any associated condition evaluates to true. """ + if not arg: + self._print_invalid_arg(arg) + return args = arg.split() - try: - count = int(args[1].strip()) - except: + if not args: + self.error('Breakpoint number expected') + return + if len(args) == 1: count = 0 + elif len(args) == 2: + try: + count = int(args[1]) + except ValueError: + self._print_invalid_arg(arg) + return + else: + self._print_invalid_arg(arg) + return try: bp = self.get_bpbynumber(args[0].strip()) - except IndexError: - self.error('Breakpoint number expected') except ValueError as err: self.error(err) else: @@ -872,19 +1654,26 @@ class Pdb(bdb.Bdb, cmd.Cmd): complete_ignore = _complete_bpnumber + def _prompt_for_confirmation(self, prompt, default): + try: + reply = input(prompt) + except EOFError: + reply = default + return reply.strip().lower() + def do_clear(self, arg): - """cl(ear) filename:lineno\ncl(ear) [bpnumber [bpnumber...]] + """cl(ear) [filename:lineno | bpnumber ...] + With a space separated list of breakpoint numbers, clear those breakpoints. Without argument, clear all breaks (but first ask confirmation). With a filename:lineno argument, clear all breaks at that line in that file. """ if not arg: - try: - reply = input('Clear all breaks? ') - except EOFError: - reply = 'no' - reply = reply.strip().lower() + reply = self._prompt_for_confirmation( + 'Clear all breaks? ', + default='no', + ) if reply in ('y', 'yes'): bplist = [bp for bp in bdb.Breakpoint.bpbynumber if bp] self.clear_all_breaks() @@ -901,7 +1690,7 @@ class Pdb(bdb.Bdb, cmd.Cmd): except ValueError: err = "Invalid line number (%s)" % arg else: - bplist = self.get_breaks(filename, lineno) + bplist = self.get_breaks(filename, lineno)[:] err = self.clear_break(filename, lineno) if err: self.error(err) @@ -924,12 +1713,24 @@ class Pdb(bdb.Bdb, cmd.Cmd): complete_cl = _complete_location def do_where(self, arg): - """w(here) - Print a stack trace, with the most recent frame at the bottom. + """w(here) [count] + + Print a stack trace. If count is not specified, print the full stack. + If count is 0, print the current frame entry. If count is positive, + print count entries from the most recent frame. If count is negative, + print -count entries from the least recent frame. An arrow indicates the "current frame", which determines the context of most commands. 'bt' is an alias for this command. """ - self.print_stack_trace() + if not arg: + count = None + else: + try: + count = int(arg) + except ValueError: + self.error('Invalid count (%s)' % arg) + return + self.print_stack_trace(count) do_w = do_where do_bt = do_where @@ -937,12 +1738,60 @@ class Pdb(bdb.Bdb, cmd.Cmd): assert 0 <= number < len(self.stack) self.curindex = number self.curframe = self.stack[self.curindex][0] - self.curframe_locals = self.curframe.f_locals + self.set_convenience_variable(self.curframe, '_frame', self.curframe) self.print_stack_entry(self.stack[self.curindex]) self.lineno = None + def do_exceptions(self, arg): + """exceptions [number] + + List or change current exception in an exception chain. + + Without arguments, list all the current exception in the exception + chain. Exceptions will be numbered, with the current exception indicated + with an arrow. + + If given an integer as argument, switch to the exception at that index. + """ + if not self._chained_exceptions: + self.message( + "Did not find chained exceptions. To move between" + " exceptions, pdb/post_mortem must be given an exception" + " object rather than a traceback." + ) + return + if not arg: + for ix, exc in enumerate(self._chained_exceptions): + prompt = ">" if ix == self._chained_exception_index else " " + rep = repr(exc) + if len(rep) > 80: + rep = rep[:77] + "..." + indicator = ( + " -" + if self._chained_exceptions[ix].__traceback__ is None + else f"{ix:>3}" + ) + self.message(f"{prompt} {indicator} {rep}") + else: + try: + number = int(arg) + except ValueError: + self.error("Argument must be an integer") + return + if 0 <= number < len(self._chained_exceptions): + if self._chained_exceptions[number].__traceback__ is None: + self.error("This exception does not have a traceback, cannot jump to it") + return + + self._chained_exception_index = number + self.setup(None, self._chained_exceptions[number].__traceback__) + self.print_stack_entry(self.stack[self.curindex]) + else: + self.error("No exception with that number") + def do_up(self, arg): """u(p) [count] + Move the current frame count (default one) levels up in the stack trace (to an older frame). """ @@ -963,6 +1812,7 @@ class Pdb(bdb.Bdb, cmd.Cmd): def do_down(self, arg): """d(own) [count] + Move the current frame count (default one) levels down in the stack trace (to a newer frame). """ @@ -983,6 +1833,7 @@ class Pdb(bdb.Bdb, cmd.Cmd): def do_until(self, arg): """unt(il) [lineno] + Without argument, continue execution until the line with a number greater than the current one is reached. With a line number, continue execution until a line with a number greater @@ -1007,34 +1858,52 @@ class Pdb(bdb.Bdb, cmd.Cmd): def do_step(self, arg): """s(tep) + Execute the current line, stop at the first possible occasion (either in a function that is called or in the current function). """ + if arg: + self._print_invalid_arg(arg) + return self.set_step() return 1 do_s = do_step def do_next(self, arg): """n(ext) + Continue execution until the next line in the current function is reached or it returns. """ + if arg: + self._print_invalid_arg(arg) + return self.set_next(self.curframe) return 1 do_n = do_next def do_run(self, arg): """run [args...] + Restart the debugged python program. If a string is supplied it is split with "shlex", and the result is used as the new sys.argv. History, breakpoints, actions and debugger options are preserved. "restart" is an alias for "run". """ + if self.mode == 'inline': + self.error('run/restart command is disabled when pdb is running in inline mode.\n' + 'Use the command line interface to enable restarting your program\n' + 'e.g. "python -m pdb myscript.py"') + return if arg: import shlex argv0 = sys.argv[0:1] - sys.argv = shlex.split(arg) + try: + sys.argv = shlex.split(arg) + except ValueError as e: + self.error('Cannot run %s: %s' % (arg, e)) + return sys.argv[:0] = argv0 # this is caught in the main debugger loop raise Restart @@ -1043,16 +1912,24 @@ class Pdb(bdb.Bdb, cmd.Cmd): def do_return(self, arg): """r(eturn) + Continue execution until the current function returns. """ + if arg: + self._print_invalid_arg(arg) + return self.set_return(self.curframe) return 1 do_r = do_return def do_continue(self, arg): """c(ont(inue)) + Continue execution, only stop when a breakpoint is encountered. """ + if arg: + self._print_invalid_arg(arg) + return if not self.nosigint: try: Pdb._previous_sigint_handler = \ @@ -1069,6 +1946,7 @@ class Pdb(bdb.Bdb, cmd.Cmd): def do_jump(self, arg): """j(ump) lineno + Set the next line that will be executed. Only available in the bottom-most frame. This lets you jump back and execute code again, or jump forward to skip code that you don't want @@ -1078,6 +1956,9 @@ class Pdb(bdb.Bdb, cmd.Cmd): instance it is not possible to jump into the middle of a for loop or out of a finally clause. """ + if not arg: + self._print_invalid_arg(arg) + return if self.curindex + 1 != len(self.stack): self.error('You can only jump within the bottom frame') return @@ -1096,33 +1977,56 @@ class Pdb(bdb.Bdb, cmd.Cmd): self.error('Jump failed: %s' % e) do_j = do_jump + def _create_recursive_debugger(self): + return Pdb(self.completekey, self.stdin, self.stdout) + def do_debug(self, arg): """debug code + Enter a recursive debugger that steps through the code argument (which is an arbitrary expression or statement to be executed in the current environment). """ - sys.settrace(None) + if not arg: + self._print_invalid_arg(arg) + return + self.stop_trace() globals = self.curframe.f_globals - locals = self.curframe_locals - p = Pdb(self.completekey, self.stdin, self.stdout) + locals = self.curframe.f_locals + p = self._create_recursive_debugger() p.prompt = "(%s) " % self.prompt.strip() self.message("ENTERING RECURSIVE DEBUGGER") try: sys.call_tracing(p.run, (arg, globals, locals)) except Exception: - exc_info = sys.exc_info()[:2] - self.error(traceback.format_exception_only(*exc_info)[-1].strip()) + self._error_exc() self.message("LEAVING RECURSIVE DEBUGGER") - sys.settrace(self.trace_dispatch) + self.start_trace() self.lastcmd = p.lastcmd complete_debug = _complete_expression def do_quit(self, arg): - """q(uit)\nexit + """q(uit) | exit + Quit from the debugger. The program being executed is aborted. """ + # Show prompt to kill process when in 'inline' mode and if pdb was not + # started from an interactive console. The attribute sys.ps1 is only + # defined if the interpreter is in interactive mode. + if self.mode == 'inline' and not hasattr(sys, 'ps1'): + while True: + try: + reply = input('Quitting pdb will kill the process. Quit anyway? [y/n] ') + reply = reply.lower().strip() + except EOFError: + reply = 'y' + self.message('') + if reply == 'y' or reply == '': + sys.exit(1) + elif reply.lower() == 'n': + return + self._user_requested_quit = True self.set_quit() return 1 @@ -1132,83 +2036,109 @@ class Pdb(bdb.Bdb, cmd.Cmd): def do_EOF(self, arg): """EOF + Handles the receipt of EOF as a command. """ self.message('') - self._user_requested_quit = True - self.set_quit() - return 1 + return self.do_quit(arg) def do_args(self, arg): """a(rgs) + Print the argument list of the current function. """ + if arg: + self._print_invalid_arg(arg) + return co = self.curframe.f_code - dict = self.curframe_locals + dict = self.curframe.f_locals n = co.co_argcount + co.co_kwonlyargcount if co.co_flags & inspect.CO_VARARGS: n = n+1 if co.co_flags & inspect.CO_VARKEYWORDS: n = n+1 for i in range(n): name = co.co_varnames[i] if name in dict: - self.message('%s = %r' % (name, dict[name])) + self.message('%s = %s' % (name, self._safe_repr(dict[name], name))) else: self.message('%s = *** undefined ***' % (name,)) do_a = do_args def do_retval(self, arg): """retval + Print the return value for the last return of a function. """ - if '__return__' in self.curframe_locals: - self.message(repr(self.curframe_locals['__return__'])) + if arg: + self._print_invalid_arg(arg) + return + if '__return__' in self.curframe.f_locals: + self.message(self._safe_repr(self.curframe.f_locals['__return__'], "retval")) else: self.error('Not yet returned!') do_rv = do_retval def _getval(self, arg): try: - return eval(arg, self.curframe.f_globals, self.curframe_locals) + return eval(arg, self.curframe.f_globals, self.curframe.f_locals) except: - exc_info = sys.exc_info()[:2] - self.error(traceback.format_exception_only(*exc_info)[-1].strip()) + self._error_exc() raise def _getval_except(self, arg, frame=None): try: if frame is None: - return eval(arg, self.curframe.f_globals, self.curframe_locals) + return eval(arg, self.curframe.f_globals, self.curframe.f_locals) else: return eval(arg, frame.f_globals, frame.f_locals) + except BaseException as exc: + return _rstr('** raised %s **' % self._format_exc(exc)) + + def _error_exc(self): + exc = sys.exception() + self.error(self._format_exc(exc)) + + def _msg_val_func(self, arg, func): + try: + val = self._getval(arg) except: - exc_info = sys.exc_info()[:2] - err = traceback.format_exception_only(*exc_info)[-1].strip() - return _rstr('** raised %s **' % err) + return # _getval() has displayed the error + try: + self.message(func(val)) + except: + self._error_exc() + + def _safe_repr(self, obj, expr): + try: + return repr(obj) + except Exception as e: + return _rstr(f"*** repr({expr}) failed: {self._format_exc(e)} ***") def do_p(self, arg): """p expression + Print the value of the expression. """ - try: - self.message(repr(self._getval(arg))) - except: - pass + if not arg: + self._print_invalid_arg(arg) + return + self._msg_val_func(arg, repr) def do_pp(self, arg): """pp expression + Pretty-print the value of the expression. """ - try: - self.message(pprint.pformat(self._getval(arg))) - except: - pass + if not arg: + self._print_invalid_arg(arg) + return + self._msg_val_func(arg, pprint.pformat) complete_print = _complete_expression complete_p = _complete_expression complete_pp = _complete_expression def do_list(self, arg): - """l(ist) [first [,last] | .] + """l(ist) [first[, last] | .] List source code for the current file. Without arguments, list 11 lines around the current line or continue the previous @@ -1256,32 +2186,42 @@ class Pdb(bdb.Bdb, cmd.Cmd): self.message('[EOF]') except KeyboardInterrupt: pass + self._validate_file_mtime() do_l = do_list def do_longlist(self, arg): - """longlist | ll + """ll | longlist + List the whole source code for the current function or frame. """ + if arg: + self._print_invalid_arg(arg) + return filename = self.curframe.f_code.co_filename breaklist = self.get_file_breaks(filename) try: - lines, lineno = getsourcelines(self.curframe) + lines, lineno = self._getsourcelines(self.curframe) except OSError as err: self.error(err) return self._print_lines(lines, lineno, breaklist, self.curframe) + self._validate_file_mtime() do_ll = do_longlist def do_source(self, arg): """source expression + Try to get source code for the given object and display it. """ + if not arg: + self._print_invalid_arg(arg) + return try: obj = self._getval(arg) except: return try: - lines, lineno = getsourcelines(obj) + lines, lineno = self._getsourcelines(obj) except (OSError, TypeError) as err: self.error(err) return @@ -1308,26 +2248,24 @@ class Pdb(bdb.Bdb, cmd.Cmd): s += '->' elif lineno == exc_lineno: s += '>>' + if self.colorize: + line = self._colorize_code(line) self.message(s + '\t' + line.rstrip()) def do_whatis(self, arg): - """whatis arg + """whatis expression + Print the type of the argument. """ + if not arg: + self._print_invalid_arg(arg) + return try: value = self._getval(arg) except: # _getval() already printed the error return code = None - # Is it a function? - try: - code = value.__code__ - except Exception: - pass - if code: - self.message('Function %s' % code.co_name) - return # Is it an instance method? try: code = value.__func__.__code__ @@ -1336,6 +2274,14 @@ class Pdb(bdb.Bdb, cmd.Cmd): if code: self.message('Method %s' % code.co_name) return + # Is it a function? + try: + code = value.__code__ + except Exception: + pass + if code: + self.message('Function %s' % code.co_name) + return # Is it a class? if value.__class__ is type: self.message('Class %s.%s' % (value.__module__, value.__qualname__)) @@ -1354,13 +2300,19 @@ class Pdb(bdb.Bdb, cmd.Cmd): Without expression, list all display expressions for the current frame. """ if not arg: - self.message('Currently displaying:') - for item in self.displaying.get(self.curframe, {}).items(): - self.message('%s: %r' % item) + if self.displaying: + self.message('Currently displaying:') + for key, val in self.displaying.get(self.curframe, {}).items(): + self.message('%s: %s' % (key, self._safe_repr(val, key))) + else: + self.message('No expression is being displayed') else: - val = self._getval_except(arg) - self.displaying.setdefault(self.curframe, {})[arg] = val - self.message('display %s: %r' % (arg, val)) + if err := self._compile_error_message(arg): + self.error('Unable to display %s: %r' % (arg, err)) + else: + val = self._getval_except(arg) + self.displaying.setdefault(self.curframe, {})[arg] = val + self.message('display %s: %s' % (arg, self._safe_repr(val, arg))) complete_display = _complete_expression @@ -1389,11 +2341,15 @@ class Pdb(bdb.Bdb, cmd.Cmd): Start an interactive interpreter whose global namespace contains all the (global and local) names found in the current scope. """ - ns = {**self.curframe.f_globals, **self.curframe_locals} - code.interact("*interactive*", local=ns) + ns = {**self.curframe.f_globals, **self.curframe.f_locals} + with self._enable_rlcompleter(ns): + console = _PdbInteractiveConsole(ns, message=self.message) + console.interact(banner="*pdb interact start*", + exitmsg="*exit from pdb interact command*") def do_alias(self, arg): - """alias [name [command [parameter parameter ...] ]] + """alias [name [command]] + Create an alias called 'name' that executes 'command'. The command must *not* be enclosed in quotes. Replaceable parameters can be indicated by %1, %2, and so on, while %* is @@ -1422,17 +2378,34 @@ class Pdb(bdb.Bdb, cmd.Cmd): for alias in keys: self.message("%s = %s" % (alias, self.aliases[alias])) return - if args[0] in self.aliases and len(args) == 1: - self.message("%s = %s" % (args[0], self.aliases[args[0]])) + if len(args) == 1: + if args[0] in self.aliases: + self.message("%s = %s" % (args[0], self.aliases[args[0]])) + else: + self.error(f"Unknown alias '{args[0]}'") else: - self.aliases[args[0]] = ' '.join(args[1:]) + # Do a validation check to make sure no replaceable parameters + # are skipped if %* is not used. + alias = ' '.join(args[1:]) + if '%*' not in alias: + consecutive = True + for idx in range(1, 10): + if f'%{idx}' not in alias: + consecutive = False + if f'%{idx}' in alias and not consecutive: + self.error("Replaceable parameters must be consecutive") + return + self.aliases[args[0]] = alias def do_unalias(self, arg): """unalias name + Delete the specified alias. """ args = arg.split() - if len(args) == 0: return + if len(args) == 0: + self._print_invalid_arg(arg) + return if args[0] in self.aliases: del self.aliases[args[0]] @@ -1441,7 +2414,7 @@ class Pdb(bdb.Bdb, cmd.Cmd): # List of all the commands making the program resume execution. commands_resuming = ['do_continue', 'do_step', 'do_next', 'do_return', - 'do_quit', 'do_jump'] + 'do_until', 'do_quit', 'do_jump'] # Print a traceback starting at the top stack frame. # The most recently entered frame is printed last; @@ -1450,10 +2423,22 @@ class Pdb(bdb.Bdb, cmd.Cmd): # It is also consistent with the up/down commands (which are # compatible with dbx and gdb: up moves towards 'main()' # and down moves towards the most recent stack frame). + # * if count is None, prints the full stack + # * if count = 0, prints the current frame entry + # * if count < 0, prints -count least recent frame entries + # * if count > 0, prints count most recent frame entries - def print_stack_trace(self): + def print_stack_trace(self, count=None): + if count is None: + stack_to_print = self.stack + elif count == 0: + stack_to_print = [self.stack[self.curindex]] + elif count < 0: + stack_to_print = self.stack[:-count] + else: + stack_to_print = self.stack[-count:] try: - for frame_lineno in self.stack: + for frame_lineno in stack_to_print: self.print_stack_entry(frame_lineno) except KeyboardInterrupt: pass @@ -1464,13 +2449,20 @@ class Pdb(bdb.Bdb, cmd.Cmd): prefix = '> ' else: prefix = ' ' - self.message(prefix + - self.format_stack_entry(frame_lineno, prompt_prefix)) + stack_entry = self.format_stack_entry(frame_lineno, prompt_prefix) + if self.colorize: + lines = stack_entry.split(prompt_prefix, 1) + if len(lines) > 1: + # We have some code to display + lines[1] = self._colorize_code(lines[1]) + stack_entry = prompt_prefix.join(lines) + self.message(prefix + stack_entry) # Provide help def do_help(self, arg): """h(elp) + Without argument, print the list of available commands. With a command name as argument, print help about that command. "help pdb" shows the full pdb documentation. @@ -1491,17 +2483,24 @@ class Pdb(bdb.Bdb, cmd.Cmd): self.error('No help for %r; please do not run Python with -OO ' 'if you need command help' % arg) return - self.message(command.__doc__.rstrip()) + if command.__doc__ is None: + self.error('No help for %r; __doc__ string missing' % arg) + return + self.message(self._help_message_from_doc(command.__doc__)) do_h = do_help def help_exec(self): """(!) statement + Execute the (one-line) statement in the context of the current stack frame. The exclamation point can be omitted unless the - first word of the statement resembles a debugger command. To - assign to a global variable you must always prefix the command - with a 'global' command, e.g.: + first word of the statement resembles a debugger command, e.g.: + (Pdb) ! n=42 + (Pdb) + + To assign to a global variable you must always prefix the command with + a 'global' command, e.g.: (Pdb) global list_options; list_options = ['-l'] (Pdb) """ @@ -1517,17 +2516,23 @@ class Pdb(bdb.Bdb, cmd.Cmd): lookupmodule() translates (possibly incomplete) file or module name into an absolute file name. + + filename could be in format of: + * an absolute path like '/path/to/file.py' + * a relative path like 'file.py' or 'dir/file.py' + * a module name like 'module' or 'package.module' + + files and modules will be searched in sys.path. """ - if os.path.isabs(filename) and os.path.exists(filename): - return filename - f = os.path.join(sys.path[0], filename) - if os.path.exists(f) and self.canonic(f) == self.mainpyfile: - return f - root, ext = os.path.splitext(filename) - if ext == '': - filename = filename + '.py' + if not filename.endswith('.py'): + # A module is passed in so convert it to equivalent file + filename = filename.replace('.', os.sep) + '.py' + if os.path.isabs(filename): - return filename + if os.path.exists(filename): + return filename + return None + for dirname in sys.path: while os.path.islink(dirname): dirname = os.readlink(dirname) @@ -1536,49 +2541,87 @@ class Pdb(bdb.Bdb, cmd.Cmd): return fullname return None - def _runmodule(self, module_name): - self._wait_for_mainpyfile = True - self._user_requested_quit = False - import runpy - mod_name, mod_spec, code = runpy._get_module_details(module_name) - self.mainpyfile = self.canonic(code.co_filename) - import __main__ - __main__.__dict__.clear() - __main__.__dict__.update({ - "__name__": "__main__", - "__file__": self.mainpyfile, - "__package__": mod_spec.parent, - "__loader__": mod_spec.loader, - "__spec__": mod_spec, - "__builtins__": __builtins__, - }) - self.run(code) - - def _runscript(self, filename): - # The script has to run in __main__ namespace (or imports from - # __main__ will break). - # - # So we clear up the __main__ and set several special variables - # (this gets rid of pdb's globals and cleans old variables on restarts). - import __main__ - __main__.__dict__.clear() - __main__.__dict__.update({"__name__" : "__main__", - "__file__" : filename, - "__builtins__": __builtins__, - }) - - # When bdb sets tracing, a number of call and line events happens + def _run(self, target: _ExecutableTarget): + # When bdb sets tracing, a number of call and line events happen # BEFORE debugger even reaches user's code (and the exact sequence of - # events depends on python version). So we take special measures to - # avoid stopping before we reach the main script (see user_line and + # events depends on python version). Take special measures to + # avoid stopping before reaching the main script (see user_line and # user_call for details). self._wait_for_mainpyfile = True - self.mainpyfile = self.canonic(filename) self._user_requested_quit = False - with io.open_code(filename) as fp: - statement = "exec(compile(%r, %r, 'exec'))" % \ - (fp.read(), self.mainpyfile) - self.run(statement) + + self.mainpyfile = self.canonic(target.filename) + + # The target has to run in __main__ namespace (or imports from + # __main__ will break). Clear __main__ and replace with + # the target namespace. + import __main__ + __main__.__dict__.clear() + __main__.__dict__.update(target.namespace) + + # Clear the mtime table for program reruns, assume all the files + # are up to date. + self._file_mtime_table.clear() + + self.run(target.code) + + def _format_exc(self, exc: BaseException): + return traceback.format_exception_only(exc)[-1].strip() + + def _compile_error_message(self, expr): + """Return the error message as string if compiling `expr` fails.""" + try: + compile(expr, "", "eval") + except SyntaxError as exc: + return _rstr(self._format_exc(exc)) + return "" + + def _getsourcelines(self, obj): + # GH-103319 + # inspect.getsourcelines() returns lineno = 0 for + # module-level frame which breaks our code print line number + # This method should be replaced by inspect.getsourcelines(obj) + # once this bug is fixed in inspect + lines, lineno = inspect.getsourcelines(obj) + lineno = max(1, lineno) + return lines, lineno + + def _help_message_from_doc(self, doc, usage_only=False): + lines = [line.strip() for line in doc.rstrip().splitlines()] + if not lines: + return "No help message found." + if "" in lines: + usage_end = lines.index("") + else: + usage_end = 1 + formatted = [] + indent = " " * len(self.prompt) + for i, line in enumerate(lines): + if i == 0: + prefix = "Usage: " + elif i < usage_end: + prefix = " " + else: + if usage_only: + break + prefix = "" + formatted.append(indent + prefix + line) + return "\n".join(formatted) + + def _print_invalid_arg(self, arg): + """Return the usage string for a function.""" + + if not arg: + self.error("Argument is required for this command") + else: + self.error(f"Invalid argument: {arg}") + + # Yes it's a bit hacky. Get the caller name, get the method based on + # that name, and get the docstring from that method. + # This should NOT fail if the caller is a method of this class. + doc = inspect.getdoc(getattr(self, sys._getframe(1).f_code.co_name)) + if doc is not None: + self.message(self._help_message_from_doc(doc, usage_only=True)) # Collect all command help into docstring, if not run with -OO @@ -1602,9 +2645,27 @@ if __doc__ is not None: # Simplified interface def run(statement, globals=None, locals=None): + """Execute the *statement* (given as a string or a code object) + under debugger control. + + The debugger prompt appears before any code is executed; you can set + breakpoints and type continue, or you can step through the statement + using step or next. + + The optional *globals* and *locals* arguments specify the + environment in which the code is executed; by default the + dictionary of the module __main__ is used (see the explanation of + the built-in exec() or eval() functions.). + """ Pdb().run(statement, globals, locals) def runeval(expression, globals=None, locals=None): + """Evaluate the *expression* (given as a string or a code object) + under debugger control. + + When runeval() returns, it returns the value of the expression. + Otherwise this function is similar to run(). + """ return Pdb().runeval(expression, globals, locals) def runctx(statement, globals, locals): @@ -1612,32 +2673,853 @@ def runctx(statement, globals, locals): run(statement, globals, locals) def runcall(*args, **kwds): + """Call the function (a function or method object, not a string) + with the given arguments. + + When runcall() returns, it returns whatever the function call + returned. The debugger prompt appears as soon as the function is + entered. + """ return Pdb().runcall(*args, **kwds) -def set_trace(*, header=None): - pdb = Pdb() +def set_trace(*, header=None, commands=None): + """Enter the debugger at the calling stack frame. + + This is useful to hard-code a breakpoint at a given point in a + program, even if the code is not otherwise being debugged (e.g. when + an assertion fails). If given, *header* is printed to the console + just before debugging begins. *commands* is an optional list of + pdb commands to run when the debugger starts. + """ + if Pdb._last_pdb_instance is not None: + pdb = Pdb._last_pdb_instance + else: + pdb = Pdb(mode='inline', backend='monitoring', colorize=True) if header is not None: pdb.message(header) - pdb.set_trace(sys._getframe().f_back) + pdb.set_trace(sys._getframe().f_back, commands=commands) + +async def set_trace_async(*, header=None, commands=None): + """Enter the debugger at the calling stack frame, but in async mode. + + This should be used as await pdb.set_trace_async(). Users can do await + if they enter the debugger with this function. Otherwise it's the same + as set_trace(). + """ + if Pdb._last_pdb_instance is not None: + pdb = Pdb._last_pdb_instance + else: + pdb = Pdb(mode='inline', backend='monitoring', colorize=True) + if header is not None: + pdb.message(header) + await pdb.set_trace_async(sys._getframe().f_back, commands=commands) + +# Remote PDB + +class _PdbServer(Pdb): + def __init__( + self, + sockfile, + signal_server=None, + owns_sockfile=True, + colorize=False, + **kwargs, + ): + self._owns_sockfile = owns_sockfile + self._interact_state = None + self._sockfile = sockfile + self._command_name_cache = [] + self._write_failed = False + if signal_server: + # Only started by the top level _PdbServer, not recursive ones. + self._start_signal_listener(signal_server) + # Override the `colorize` attribute set by the parent constructor, + # because it checks the server's stdout, rather than the client's. + super().__init__(colorize=False, **kwargs) + self.colorize = colorize + + @staticmethod + def protocol_version(): + # By default, assume a client and server are compatible if they run + # the same Python major.minor version. We'll try to keep backwards + # compatibility between patch versions of a minor version if possible. + # If we do need to change the protocol in a patch version, we'll change + # `revision` to the patch version where the protocol changed. + # We can ignore compatibility for pre-release versions; sys.remote_exec + # can't attach to a pre-release version except from that same version. + v = sys.version_info + revision = 0 + return int(f"{v.major:02X}{v.minor:02X}{revision:02X}F0", 16) + + def _ensure_valid_message(self, msg): + # Ensure the message conforms to our protocol. + # If anything needs to be changed here for a patch release of Python, + # the 'revision' in protocol_version() should be updated. + match msg: + case {"message": str(), "type": str()}: + # Have the client show a message. The client chooses how to + # format the message based on its type. The currently defined + # types are "info" and "error". If a message has a type the + # client doesn't recognize, it must be treated as "info". + pass + case {"help": str()}: + # Have the client show the help for a given argument. + pass + case {"prompt": str(), "state": str()}: + # Have the client display the given prompt and wait for a reply + # from the user. If the client recognizes the state it may + # enable mode-specific features like multi-line editing. + # If it doesn't recognize the state it must prompt for a single + # line only and send it directly to the server. A server won't + # progress until it gets a "reply" or "signal" message, but can + # process "complete" requests while waiting for the reply. + pass + case { + "completions": list(completions) + } if all(isinstance(c, str) for c in completions): + # Return valid completions for a client's "complete" request. + pass + case { + "command_list": list(command_list) + } if all(isinstance(c, str) for c in command_list): + # Report the list of legal PDB commands to the client. + # Due to aliases this list is not static, but the client + # needs to know it for multi-line editing. + pass + case _: + raise AssertionError( + f"PDB message doesn't follow the schema! {msg}" + ) + + @classmethod + def _start_signal_listener(cls, address): + def listener(sock): + with closing(sock): + # Check if the interpreter is finalizing every quarter of a second. + # Clean up and exit if so. + sock.settimeout(0.25) + sock.shutdown(socket.SHUT_WR) + while not shut_down.is_set(): + try: + data = sock.recv(1024) + except socket.timeout: + continue + if data == b"": + return # EOF + signal.raise_signal(signal.SIGINT) + + def stop_thread(): + shut_down.set() + thread.join() + + # Use a daemon thread so that we don't detach until after all non-daemon + # threads are done. Use an atexit handler to stop gracefully at that point, + # so that our thread is stopped before the interpreter is torn down. + shut_down = threading.Event() + thread = threading.Thread( + target=listener, + args=[socket.create_connection(address, timeout=5)], + daemon=True, + ) + atexit.register(stop_thread) + thread.start() + + def _send(self, **kwargs): + self._ensure_valid_message(kwargs) + json_payload = json.dumps(kwargs) + try: + self._sockfile.write(json_payload.encode() + b"\n") + self._sockfile.flush() + except (OSError, ValueError): + # We get an OSError if the network connection has dropped, and a + # ValueError if detach() if the sockfile has been closed. We'll + # handle this the next time we try to read from the client instead + # of trying to handle it from everywhere _send() may be called. + # Track this with a flag rather than assuming readline() will ever + # return an empty string because the socket may be half-closed. + self._write_failed = True + + @typing.override + def message(self, msg, end="\n"): + self._send(message=str(msg) + end, type="info") + + @typing.override + def error(self, msg): + self._send(message=str(msg), type="error") + + def _get_input(self, prompt, state) -> str: + # Before displaying a (Pdb) prompt, send the list of PDB commands + # unless we've already sent an up-to-date list. + if state == "pdb" and not self._command_name_cache: + self._command_name_cache = self.completenames("", "", 0, 0) + self._send(command_list=self._command_name_cache) + self._send(prompt=prompt, state=state) + return self._read_reply() + + def _read_reply(self): + # Loop until we get a 'reply' or 'signal' from the client, + # processing out-of-band 'complete' requests as they arrive. + while True: + if self._write_failed: + raise EOFError + + msg = self._sockfile.readline() + if not msg: + raise EOFError + + try: + payload = json.loads(msg) + except json.JSONDecodeError: + self.error(f"Disconnecting: client sent invalid JSON {msg!r}") + raise EOFError + + match payload: + case {"reply": str(reply)}: + return reply + case {"signal": str(signal)}: + if signal == "INT": + raise KeyboardInterrupt + elif signal == "EOF": + raise EOFError + else: + self.error( + f"Received unrecognized signal: {signal}" + ) + # Our best hope of recovering is to pretend we + # got an EOF to exit whatever mode we're in. + raise EOFError + case { + "complete": { + "text": str(text), + "line": str(line), + "begidx": int(begidx), + "endidx": int(endidx), + } + }: + items = self._complete_any(text, line, begidx, endidx) + self._send(completions=items) + continue + # Valid JSON, but doesn't meet the schema. + self.error(f"Ignoring invalid message from client: {msg}") + + def _complete_any(self, text, line, begidx, endidx): + # If we're in 'interact' mode, we need to use the default completer + if self._interact_state: + compfunc = self.completedefault + else: + if begidx == 0: + return self.completenames(text, line, begidx, endidx) + + cmd = self.parseline(line)[0] + if cmd: + compfunc = getattr(self, "complete_" + cmd, self.completedefault) + else: + compfunc = self.completedefault + return compfunc(text, line, begidx, endidx) + + def cmdloop(self, intro=None): + self.preloop() + if intro is not None: + self.intro = intro + if self.intro: + self.message(str(self.intro)) + stop = None + while not stop: + if self._interact_state is not None: + try: + reply = self._get_input(prompt=">>> ", state="interact") + except KeyboardInterrupt: + # Match how KeyboardInterrupt is handled in a REPL + self.message("\nKeyboardInterrupt") + except EOFError: + self.message("\n*exit from pdb interact command*") + self._interact_state = None + else: + self._run_in_python_repl(reply) + continue + + if not self.cmdqueue: + try: + state = "commands" if self.commands_defining else "pdb" + reply = self._get_input(prompt=self.prompt, state=state) + except EOFError: + reply = "EOF" + + self.cmdqueue.append(reply) + + line = self.cmdqueue.pop(0) + line = self.precmd(line) + stop = self.onecmd(line) + stop = self.postcmd(stop, line) + self.postloop() + + def postloop(self): + super().postloop() + if self.quitting: + self.detach() + + def detach(self): + # Detach the debugger and close the socket without raising BdbQuit + self.quitting = False + if self._owns_sockfile: + # Don't try to reuse this instance, it's not valid anymore. + Pdb._last_pdb_instance = None + try: + self._sockfile.close() + except OSError: + # close() can fail if the connection was broken unexpectedly. + pass + + def do_debug(self, arg): + # Clear our cached list of valid commands; the recursive debugger might + # send its own differing list, and so ours needs to be re-sent. + self._command_name_cache = [] + return super().do_debug(arg) + + def do_alias(self, arg): + # Clear our cached list of valid commands; one might be added. + self._command_name_cache = [] + return super().do_alias(arg) + + def do_unalias(self, arg): + # Clear our cached list of valid commands; one might be removed. + self._command_name_cache = [] + return super().do_unalias(arg) + + def do_help(self, arg): + # Tell the client to render the help, since it might need a pager. + self._send(help=arg) + + do_h = do_help + + def _interact_displayhook(self, obj): + # Like the default `sys.displayhook` except sending a socket message. + if obj is not None: + self.message(repr(obj)) + builtins._ = obj + + def _run_in_python_repl(self, lines): + # Run one 'interact' mode code block against an existing namespace. + assert self._interact_state + save_displayhook = sys.displayhook + try: + sys.displayhook = self._interact_displayhook + code_obj = self._interact_state["compiler"](lines + "\n") + if code_obj is None: + raise SyntaxError("Incomplete command") + exec(code_obj, self._interact_state["ns"]) + except: + self._error_exc() + finally: + sys.displayhook = save_displayhook + + def do_interact(self, arg): + # Prepare to run 'interact' mode code blocks, and trigger the client + # to start treating all input as Python commands, not PDB ones. + self.message("*pdb interact start*") + self._interact_state = dict( + compiler=codeop.CommandCompiler(), + ns={**self.curframe.f_globals, **self.curframe.f_locals}, + ) + + @typing.override + def _create_recursive_debugger(self): + return _PdbServer( + self._sockfile, + owns_sockfile=False, + colorize=self.colorize, + ) + + @typing.override + def _prompt_for_confirmation(self, prompt, default): + try: + return self._get_input(prompt=prompt, state="confirm") + except (EOFError, KeyboardInterrupt): + return default + + def do_run(self, arg): + self.error("remote PDB cannot restart the program") + + do_restart = do_run + + def _error_exc(self): + if self._interact_state and isinstance(sys.exception(), SystemExit): + # If we get a SystemExit in 'interact' mode, exit the REPL. + self._interact_state = None + ret = super()._error_exc() + self.message("*exit from pdb interact command*") + return ret + else: + return super()._error_exc() + + def default(self, line): + # Unlike Pdb, don't prompt for more lines of a multi-line command. + # The remote needs to send us the whole block in one go. + try: + candidate = line.removeprefix("!") + "\n" + if codeop.compile_command(candidate, "", "single") is None: + raise SyntaxError("Incomplete command") + return super().default(candidate) + except: + self._error_exc() + + +class _PdbClient: + def __init__(self, pid, server_socket, interrupt_sock): + self.pid = pid + self.read_buf = b"" + self.signal_read = None + self.signal_write = None + self.sigint_received = False + self.raise_on_sigint = False + self.server_socket = server_socket + self.interrupt_sock = interrupt_sock + self.pdb_instance = Pdb() + self.pdb_commands = set() + self.completion_matches = [] + self.state = "dumb" + self.write_failed = False + self.multiline_block = False + + def _ensure_valid_message(self, msg): + # Ensure the message conforms to our protocol. + # If anything needs to be changed here for a patch release of Python, + # the 'revision' in protocol_version() should be updated. + match msg: + case {"reply": str()}: + # Send input typed by a user at a prompt to the remote PDB. + pass + case {"signal": "EOF"}: + # Tell the remote PDB that the user pressed ^D at a prompt. + pass + case {"signal": "INT"}: + # Tell the remote PDB that the user pressed ^C at a prompt. + pass + case { + "complete": { + "text": str(), + "line": str(), + "begidx": int(), + "endidx": int(), + } + }: + # Ask the remote PDB what completions are valid for the given + # parameters, using readline's completion protocol. + pass + case _: + raise AssertionError( + f"PDB message doesn't follow the schema! {msg}" + ) + + def _send(self, **kwargs): + self._ensure_valid_message(kwargs) + json_payload = json.dumps(kwargs) + try: + self.server_socket.sendall(json_payload.encode() + b"\n") + except OSError: + # This means that the client has abruptly disconnected, but we'll + # handle that the next time we try to read from the client instead + # of trying to handle it from everywhere _send() may be called. + # Track this with a flag rather than assuming readline() will ever + # return an empty string because the socket may be half-closed. + self.write_failed = True + + def _readline(self): + if self.sigint_received: + # There's a pending unhandled SIGINT. Handle it now. + self.sigint_received = False + raise KeyboardInterrupt + + # Wait for either a SIGINT or a line or EOF from the PDB server. + selector = selectors.DefaultSelector() + selector.register(self.signal_read, selectors.EVENT_READ) + selector.register(self.server_socket, selectors.EVENT_READ) + + while b"\n" not in self.read_buf: + for key, _ in selector.select(): + if key.fileobj == self.signal_read: + self.signal_read.recv(1024) + if self.sigint_received: + # If not, we're reading wakeup events for sigints that + # we've previously handled, and can ignore them. + self.sigint_received = False + raise KeyboardInterrupt + elif key.fileobj == self.server_socket: + data = self.server_socket.recv(16 * 1024) + self.read_buf += data + if not data and b"\n" not in self.read_buf: + # EOF without a full final line. Drop the partial line. + self.read_buf = b"" + return b"" + + ret, sep, self.read_buf = self.read_buf.partition(b"\n") + return ret + sep + + def read_input(self, prompt, multiline_block): + self.multiline_block = multiline_block + with self._sigint_raises_keyboard_interrupt(): + return input(prompt) + + def read_command(self, prompt): + reply = self.read_input(prompt, multiline_block=False) + if self.state == "dumb": + # No logic applied whatsoever, just pass the raw reply back. + return reply + + prefix = "" + if self.state == "pdb": + # PDB command entry mode + cmd = self.pdb_instance.parseline(reply)[0] + if cmd in self.pdb_commands or reply.strip() == "": + # Recognized PDB command, or blank line repeating last command + return reply + + # Otherwise, explicit or implicit exec command + if reply.startswith("!"): + prefix = "!" + reply = reply.removeprefix(prefix).lstrip() + + if codeop.compile_command(reply + "\n", "", "single") is not None: + # Valid single-line statement + return prefix + reply + + # Otherwise, valid first line of a multi-line statement + more_prompt = "...".ljust(len(prompt)) + while codeop.compile_command(reply, "", "single") is None: + reply += "\n" + self.read_input(more_prompt, multiline_block=True) + + return prefix + reply + + @contextmanager + def readline_completion(self, completer): + try: + import readline + except ImportError: + yield + return + + old_completer = readline.get_completer() + try: + readline.set_completer(completer) + if readline.backend == "editline": + # libedit uses "^I" instead of "tab" + command_string = "bind ^I rl_complete" + else: + command_string = "tab: complete" + readline.parse_and_bind(command_string) + yield + finally: + readline.set_completer(old_completer) + + @contextmanager + def _sigint_handler(self): + # Signal handling strategy: + # - When we call input() we want a SIGINT to raise KeyboardInterrupt + # - Otherwise we want to write to the wakeup FD and set a flag. + # We'll break out of select() when the wakeup FD is written to, + # and we'll check the flag whenever we're about to accept input. + def handler(signum, frame): + self.sigint_received = True + if self.raise_on_sigint: + # One-shot; don't raise again until the flag is set again. + self.raise_on_sigint = False + self.sigint_received = False + raise KeyboardInterrupt + + sentinel = object() + old_handler = sentinel + old_wakeup_fd = sentinel + + self.signal_read, self.signal_write = socket.socketpair() + with (closing(self.signal_read), closing(self.signal_write)): + self.signal_read.setblocking(False) + self.signal_write.setblocking(False) + + try: + old_handler = signal.signal(signal.SIGINT, handler) + + try: + old_wakeup_fd = signal.set_wakeup_fd( + self.signal_write.fileno(), + warn_on_full_buffer=False, + ) + yield + finally: + # Restore the old wakeup fd if we installed a new one + if old_wakeup_fd is not sentinel: + signal.set_wakeup_fd(old_wakeup_fd) + finally: + self.signal_read = self.signal_write = None + if old_handler is not sentinel: + # Restore the old handler if we installed a new one + signal.signal(signal.SIGINT, old_handler) + + @contextmanager + def _sigint_raises_keyboard_interrupt(self): + if self.sigint_received: + # There's a pending unhandled SIGINT. Handle it now. + self.sigint_received = False + raise KeyboardInterrupt + + try: + self.raise_on_sigint = True + yield + finally: + self.raise_on_sigint = False + + def cmdloop(self): + with ( + self._sigint_handler(), + self.readline_completion(self.complete), + ): + while not self.write_failed: + try: + if not (payload_bytes := self._readline()): + break + except KeyboardInterrupt: + self.send_interrupt() + continue + + try: + payload = json.loads(payload_bytes) + except json.JSONDecodeError: + print( + f"*** Invalid JSON from remote: {payload_bytes!r}", + flush=True, + ) + continue + + self.process_payload(payload) + + def send_interrupt(self): + if self.interrupt_sock is not None: + # Write to a socket that the PDB server listens on. This triggers + # the remote to raise a SIGINT for itself. We do this because + # Windows doesn't allow triggering SIGINT remotely. + # See https://stackoverflow.com/a/35792192 for many more details. + self.interrupt_sock.sendall(signal.SIGINT.to_bytes()) + else: + # On Unix we can just send a SIGINT to the remote process. + # This is preferable to using the signal thread approach that we + # use on Windows because it can interrupt IO in the main thread. + os.kill(self.pid, signal.SIGINT) + + def process_payload(self, payload): + match payload: + case { + "command_list": command_list + } if all(isinstance(c, str) for c in command_list): + self.pdb_commands = set(command_list) + case {"message": str(msg), "type": str(msg_type)}: + if msg_type == "error": + print("***", msg, flush=True) + else: + print(msg, end="", flush=True) + case {"help": str(arg)}: + self.pdb_instance.do_help(arg) + case {"prompt": str(prompt), "state": str(state)}: + if state not in ("pdb", "interact"): + state = "dumb" + self.state = state + self.prompt_for_reply(prompt) + case _: + raise RuntimeError(f"Unrecognized payload {payload}") + + def prompt_for_reply(self, prompt): + while True: + try: + payload = {"reply": self.read_command(prompt)} + except EOFError: + payload = {"signal": "EOF"} + except KeyboardInterrupt: + payload = {"signal": "INT"} + except Exception as exc: + msg = traceback.format_exception_only(exc)[-1].strip() + print("***", msg, flush=True) + continue + + self._send(**payload) + return + + def complete(self, text, state): + import readline + + if state == 0: + self.completion_matches = [] + if self.state not in ("pdb", "interact"): + return None + + origline = readline.get_line_buffer() + line = origline.lstrip() + if self.multiline_block: + # We're completing a line contained in a multi-line block. + # Force the remote to treat it as a Python expression. + line = "! " + line + offset = len(origline) - len(line) + begidx = readline.get_begidx() - offset + endidx = readline.get_endidx() - offset + + msg = { + "complete": { + "text": text, + "line": line, + "begidx": begidx, + "endidx": endidx, + } + } + + self._send(**msg) + if self.write_failed: + return None + + payload = self._readline() + if not payload: + return None + + payload = json.loads(payload) + if "completions" not in payload: + raise RuntimeError( + f"Failed to get valid completions. Got: {payload}" + ) + + self.completion_matches = payload["completions"] + try: + return self.completion_matches[state] + except IndexError: + return None + + +def _connect( + *, + host, + port, + frame, + commands, + version, + signal_raising_thread, + colorize, +): + with closing(socket.create_connection((host, port))) as conn: + sockfile = conn.makefile("rwb") + + # The client requests this thread on Windows but not on Unix. + # Most tests don't request this thread, to keep them simpler. + if signal_raising_thread: + signal_server = (host, port) + else: + signal_server = None + + remote_pdb = _PdbServer( + sockfile, + signal_server=signal_server, + colorize=colorize, + ) + weakref.finalize(remote_pdb, sockfile.close) + + if Pdb._last_pdb_instance is not None: + remote_pdb.error("Another PDB instance is already attached.") + elif version != remote_pdb.protocol_version(): + target_ver = f"0x{remote_pdb.protocol_version():08X}" + attach_ver = f"0x{version:08X}" + remote_pdb.error( + f"The target process is running a Python version that is" + f" incompatible with this PDB module." + f"\nTarget process pdb protocol version: {target_ver}" + f"\nLocal pdb module's protocol version: {attach_ver}" + ) + else: + remote_pdb.rcLines.extend(commands.splitlines()) + remote_pdb.set_trace(frame=frame) + + +def attach(pid, commands=()): + """Attach to a running process with the given PID.""" + with ExitStack() as stack: + server = stack.enter_context( + closing(socket.create_server(("localhost", 0))) + ) + port = server.getsockname()[1] + + connect_script = stack.enter_context( + tempfile.NamedTemporaryFile("w", delete_on_close=False) + ) + + use_signal_thread = sys.platform == "win32" + colorize = _colorize.can_colorize() + + connect_script.write( + textwrap.dedent( + f""" + import pdb, sys + pdb._connect( + host="localhost", + port={port}, + frame=sys._getframe(1), + commands={json.dumps("\n".join(commands))}, + version={_PdbServer.protocol_version()}, + signal_raising_thread={use_signal_thread!r}, + colorize={colorize!r}, + ) + """ + ) + ) + connect_script.close() + orig_mode = os.stat(connect_script.name).st_mode + os.chmod(connect_script.name, orig_mode | stat.S_IROTH | stat.S_IRGRP) + sys.remote_exec(pid, connect_script.name) + + # TODO Add a timeout? Or don't bother since the user can ^C? + client_sock, _ = server.accept() + stack.enter_context(closing(client_sock)) + + if use_signal_thread: + interrupt_sock, _ = server.accept() + stack.enter_context(closing(interrupt_sock)) + interrupt_sock.setblocking(False) + else: + interrupt_sock = None + + _PdbClient(pid, client_sock, interrupt_sock).cmdloop() + # Post-Mortem interface def post_mortem(t=None): + """Enter post-mortem debugging of the given *traceback*, or *exception* + object. + + If no traceback is given, it uses the one of the exception that is + currently being handled (an exception must be being handled if the + default is to be used). + + If `t` is an exception object, the `exceptions` command makes it possible to + list and inspect its chained exceptions (if any). + """ + return _post_mortem(t, Pdb()) + + +def _post_mortem(t, pdb_instance): + """ + Private version of post_mortem, which allow to pass a pdb instance + for testing purposes. + """ # handling the default if t is None: - # sys.exc_info() returns (type, value, traceback) if an exception is - # being handled, otherwise it returns None - t = sys.exc_info()[2] - if t is None: + exc = sys.exception() + if exc is not None: + t = exc.__traceback__ + + if t is None or (isinstance(t, BaseException) and t.__traceback__ is None): raise ValueError("A valid traceback must be passed if no " "exception is being handled") - p = Pdb() - p.reset() - p.interaction(None, t) + pdb_instance.reset() + pdb_instance.interaction(None, t) + def pm(): - post_mortem(sys.last_traceback) + """Enter post-mortem debugging of the traceback found in sys.last_exc.""" + post_mortem(sys.last_exc) # Main program for testing @@ -1653,11 +3535,10 @@ def help(): pydoc.pager(__doc__) _usage = """\ -usage: pdb.py [-c command] ... [-m module | pyfile] [arg] ... - Debug the Python program given by pyfile. Alternatively, an executable module or package to debug can be specified using -the -m switch. +the -m switch. You can also attach to a running Python process +using the -p option with its PID. Initial commands are read from .pdbrc files in your home directory and in the current directory, if they exist. Commands supplied with @@ -1667,70 +3548,132 @@ To let the script run until an exception occurs, use "-c continue". To let the script run up to a given line X in the debugged file, use "-c 'until X'".""" -def main(): - import getopt - opts, args = getopt.getopt(sys.argv[1:], 'mhc:', ['help', 'command=']) +def exit_with_permission_help_text(): + """ + Prints a message pointing to platform-specific permission help text and exits the program. + This function is called when a PermissionError is encountered while trying + to attach to a process. + """ + print( + "Error: The specified process cannot be attached to due to insufficient permissions.\n" + "See the Python documentation for details on required privileges and troubleshooting:\n" + "https://docs.python.org/3.14/howto/remote_debugging.html#permission-requirements\n" + ) + sys.exit(1) + + +def parse_args(): + # We want pdb to be as intuitive as possible to users, so we need to do some + # heuristic parsing to deal with ambiguity. + # For example: + # "python -m pdb -m foo -p 1" should pass "-p 1" to "foo". + # "python -m pdb foo.py -m bar" should pass "-m bar" to "foo.py". + # "python -m pdb -m foo -m bar" should pass "-m bar" to "foo". + # This require some customized parsing logic to find the actual debug target. + + import argparse + + parser = argparse.ArgumentParser( + usage="%(prog)s [-h] [-c command] (-m module | -p pid | pyfile) [args ...]", + description=_usage, + formatter_class=argparse.RawDescriptionHelpFormatter, + allow_abbrev=False, + color=True, + ) + + # Get all the commands out first. For backwards compatibility, we allow + # -c commands to be after the target. + parser.add_argument('-c', '--command', action='append', default=[], metavar='command', dest='commands', + help='pdb commands to execute as if given in a .pdbrc file') + + opts, args = parser.parse_known_args() if not args: - print(_usage) + # If no arguments were given (python -m pdb), print the whole help message. + # Without this check, argparse would only complain about missing required arguments. + # We need to add the arguments definitions here to get a proper help message. + parser.add_argument('-m', metavar='module', dest='module') + parser.add_argument('-p', '--pid', type=int, help="attach to the specified PID", default=None) + parser.print_help() sys.exit(2) + elif args[0] == '-p' or args[0] == '--pid': + # Attach to a pid + parser.add_argument('-p', '--pid', type=int, help="attach to the specified PID", default=None) + opts, args = parser.parse_known_args() + if args: + # For --pid, any extra arguments are invalid. + parser.error(f"unrecognized arguments: {' '.join(args)}") + elif args[0] == '-m': + # Debug a module, we only need the first -m module argument. + # The rest is passed to the module itself. + parser.add_argument('-m', metavar='module', dest='module') + opt_module = parser.parse_args(args[:2]) + opts.module = opt_module.module + args = args[2:] + elif args[0] == '--': + args.pop(0) + if not args: + parser.error("missing script or module to run") + elif args[0].startswith('-'): + # Invalid argument before the script name. + invalid_args = list(itertools.takewhile(lambda a: a.startswith('-'), args)) + parser.error(f"unrecognized arguments: {' '.join(invalid_args)}") - commands = [] - run_as_module = False - for opt, optarg in opts: - if opt in ['-h', '--help']: - print(_usage) - sys.exit() - elif opt in ['-c', '--command']: - commands.append(optarg) - elif opt in ['-m']: - run_as_module = True + # Otherwise it's debugging a script and we already parsed all -c commands. - mainpyfile = args[0] # Get script filename - if not run_as_module and not os.path.exists(mainpyfile): - print('Error:', mainpyfile, 'does not exist') - sys.exit(1) + return opts, args - sys.argv[:] = args # Hide "pdb.py" and pdb options from argument list +def main(): + opts, args = parse_args() - # Replace pdb's dir with script's dir in front of module search path. - if not run_as_module: - sys.path[0] = os.path.dirname(mainpyfile) + if getattr(opts, 'pid', None) is not None: + try: + attach(opts.pid, opts.commands) + except PermissionError as e: + exit_with_permission_help_text() + return + elif getattr(opts, 'module', None) is not None: + file = opts.module + target = _ModuleTarget(file) + else: + file = args.pop(0) + if file.endswith('.pyz'): + target = _ZipTarget(file) + else: + target = _ScriptTarget(file) + + sys.argv[:] = [file] + args # Hide "pdb.py" and pdb options from argument list # Note on saving/restoring sys.argv: it's a good idea when sys.argv was # modified by the script being debugged. It's a bad idea when it was # changed by the user from the command line. There is a "restart" command # which allows explicit specification of command line arguments. - pdb = Pdb() - pdb.rcLines.extend(commands) + pdb = Pdb(mode='cli', backend='monitoring', colorize=True) + pdb.rcLines.extend(opts.commands) while True: try: - if run_as_module: - pdb._runmodule(mainpyfile) - else: - pdb._runscript(mainpyfile) - if pdb._user_requested_quit: - break - print("The program finished and will be restarted") + pdb._run(target) except Restart: - print("Restarting", mainpyfile, "with arguments:") - print("\t" + " ".join(args)) - except SystemExit: + print("Restarting", target, "with arguments:") + print("\t" + " ".join(sys.argv[1:])) + except SystemExit as e: # In most cases SystemExit does not warrant a post-mortem session. print("The program exited via sys.exit(). Exit status:", end=' ') - print(sys.exc_info()[1]) - except SyntaxError: - traceback.print_exc() - sys.exit(1) - except: - traceback.print_exc() + print(e) + except BaseException as e: + traceback.print_exception(e, colorize=_colorize.can_colorize()) print("Uncaught exception. Entering post mortem debugging") print("Running 'cont' or 'step' will restart the program") - t = sys.exc_info()[2] - pdb.interaction(None, t) - print("Post mortem debugger finished. The " + mainpyfile + - " will be restarted") + try: + pdb.interaction(None, e) + except Restart: + print("Restarting", target, "with arguments:") + print("\t" + " ".join(sys.argv[1:])) + continue + if pdb._user_requested_quit: + break + print("The program finished and will be restarted") # When invoked as main program, invoke the debugger on a script diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py new file mode 100644 index 000000000..8b2806781 --- /dev/null +++ b/Lib/test/test_pdb.py @@ -0,0 +1,5179 @@ +# A test suite for pdb; not very comprehensive at the moment. + +import _colorize +import doctest +import gc +import io +import os +import pdb +import sys +import types +import codecs +import unittest +import subprocess +import textwrap +import linecache +import zipapp +import zipfile + +from asyncio.events import _set_event_loop_policy +from contextlib import ExitStack, redirect_stdout +from io import StringIO +from test import support +from test.support import has_socket_support, os_helper +from test.support.import_helper import import_module +from test.support.pty_helper import run_pty, FakeInput +from test.support.script_helper import kill_python +from unittest.mock import patch + +SKIP_CORO_TESTS = False + + +class PdbTestInput(object): + """Context manager that makes testing Pdb in doctests easier.""" + + def __init__(self, input): + self.input = input + + def __enter__(self): + self.real_stdin = sys.stdin + sys.stdin = FakeInput(self.input) + self.orig_trace = sys.gettrace() if hasattr(sys, 'gettrace') else None + + def __exit__(self, *exc): + sys.stdin = self.real_stdin + if self.orig_trace: + sys.settrace(self.orig_trace) + + +def test_pdb_displayhook(): + """This tests the custom displayhook for pdb. + + >>> def test_function(foo, bar): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + + >>> with PdbTestInput([ + ... 'foo', + ... 'bar', + ... 'for i in range(5): print(i)', + ... 'continue', + ... ]): + ... test_function(1, None) + > (2)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) foo + 1 + (Pdb) bar + (Pdb) for i in range(5): print(i) + 0 + 1 + 2 + 3 + 4 + (Pdb) continue + """ + + +def test_pdb_basic_commands(): + """Test the basic commands of pdb. + + >>> def test_function_2(foo, bar='default'): + ... print(foo) + ... for i in range(5): + ... print(i) + ... print(bar) + ... for i in range(10): + ... never_executed + ... print('after for') + ... print('...') + ... return foo.upper() + + >>> def test_function3(arg=None, *, kwonly=None): + ... pass + + >>> def test_function4(a, b, c, /): + ... pass + + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... ret = test_function_2('baz') + ... test_function3(kwonly=True) + ... test_function4(1, 2, 3) + ... print(ret) + + >>> with PdbTestInput([ # TODO: RUSTPYTHON; hangs # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE +SKIP + ... 'step', # go to line ret = test_function_2('baz') + ... 'step', # entering the function call + ... 'args', # display function args + ... 'list', # list function source + ... 'bt', # display backtrace + ... 'up', # step up to test_function() + ... 'down', # step down to test_function_2() again + ... 'next', # stepping to print(foo) + ... 'next', # stepping to the for loop + ... 'step', # stepping into the for loop + ... 'until', # continuing until out of the for loop + ... 'next', # executing the print(bar) + ... 'jump 8', # jump over second for loop + ... 'return', # return out of function + ... 'retval', # display return value + ... 'next', # step to test_function3() + ... 'step', # stepping into test_function3() + ... 'args', # display function args + ... 'return', # return out of function + ... 'next', # step to test_function4() + ... 'step', # stepping to test_function4() + ... 'args', # display function args + ... 'continue', + ... ]): + ... test_function() + > (2)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) step + > (3)test_function() + -> ret = test_function_2('baz') + (Pdb) step + --Call-- + > (1)test_function_2() + -> def test_function_2(foo, bar='default'): + (Pdb) args + foo = 'baz' + bar = 'default' + (Pdb) list + 1 -> def test_function_2(foo, bar='default'): + 2 print(foo) + 3 for i in range(5): + 4 print(i) + 5 print(bar) + 6 for i in range(10): + 7 never_executed + 8 print('after for') + 9 print('...') + 10 return foo.upper() + [EOF] + (Pdb) bt + ... + (26)() + -> test_function() + (3)test_function() + -> ret = test_function_2('baz') + > (1)test_function_2() + -> def test_function_2(foo, bar='default'): + (Pdb) up + > (3)test_function() + -> ret = test_function_2('baz') + (Pdb) down + > (1)test_function_2() + -> def test_function_2(foo, bar='default'): + (Pdb) next + > (2)test_function_2() + -> print(foo) + (Pdb) next + baz + > (3)test_function_2() + -> for i in range(5): + (Pdb) step + > (4)test_function_2() + -> print(i) + (Pdb) until + 0 + 1 + 2 + 3 + 4 + > (5)test_function_2() + -> print(bar) + (Pdb) next + default + > (6)test_function_2() + -> for i in range(10): + (Pdb) jump 8 + > (8)test_function_2() + -> print('after for') + (Pdb) return + after for + ... + --Return-- + > (10)test_function_2()->'BAZ' + -> return foo.upper() + (Pdb) retval + 'BAZ' + (Pdb) next + > (4)test_function() + -> test_function3(kwonly=True) + (Pdb) step + --Call-- + > (1)test_function3() + -> def test_function3(arg=None, *, kwonly=None): + (Pdb) args + arg = None + kwonly = True + (Pdb) return + --Return-- + > (2)test_function3()->None + -> pass + (Pdb) next + > (5)test_function() + -> test_function4(1, 2, 3) + (Pdb) step + --Call-- + > (1)test_function4() + -> def test_function4(a, b, c, /): + (Pdb) args + a = 1 + b = 2 + c = 3 + (Pdb) continue + BAZ + """ + +def test_pdb_breakpoint_commands(): + """Test basic commands related to breakpoints. + + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... print(1) + ... print(2) + ... print(3) + ... print(4) + + Now test the breakpoint commands. NORMALIZE_WHITESPACE is needed because + the breakpoint list outputs a tab for the "stop only" and "ignore next" + lines, which we don't want to put in here. + + >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +NORMALIZE_WHITESPACE +SKIP + ... 'break 3', + ... 'break 4, +', + ... 'disable 1', + ... 'ignore 1 10', + ... 'condition 1 1 < 2', + ... 'condition 1 1 <', + ... 'break 4', + ... 'break 4', + ... 'break', + ... 'clear 3', + ... 'break', + ... 'condition 1', + ... 'commands 1', + ... 'EOF', # Simulate Ctrl-D/Ctrl-Z from user, should end input + ... 'enable 1', + ... 'clear 1', + ... 'commands 2', + ... 'p "42"', + ... 'print("42", 7*6)', # Issue 18764 (not about breakpoints) + ... 'end', + ... 'continue', # will stop at breakpoint 2 (line 4) + ... 'clear', # clear all! + ... 'y', + ... 'tbreak 5', + ... 'continue', # will stop at temporary breakpoint + ... 'break', # make sure breakpoint is gone + ... 'commands 10', # out of range + ... 'commands a', # display help + ... 'commands 4', # already deleted + ... 'break 6, undefined', # condition causing `NameError` during evaluation + ... 'continue', # will stop, ignoring runtime error + ... 'continue', + ... ]): + ... test_function() + > (2)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) break 3 + Breakpoint 1 at :3 + (Pdb) break 4, + + *** Invalid condition +: SyntaxError: invalid syntax + (Pdb) disable 1 + Disabled breakpoint 1 at :3 + (Pdb) ignore 1 10 + Will ignore next 10 crossings of breakpoint 1. + (Pdb) condition 1 1 < 2 + New condition set for breakpoint 1. + (Pdb) condition 1 1 < + *** Invalid condition 1 <: SyntaxError: invalid syntax + (Pdb) break 4 + Breakpoint 2 at :4 + (Pdb) break 4 + Breakpoint 3 at :4 + (Pdb) break + Num Type Disp Enb Where + 1 breakpoint keep no at :3 + stop only if 1 < 2 + ignore next 10 hits + 2 breakpoint keep yes at :4 + 3 breakpoint keep yes at :4 + (Pdb) clear 3 + Deleted breakpoint 3 at :4 + (Pdb) break + Num Type Disp Enb Where + 1 breakpoint keep no at :3 + stop only if 1 < 2 + ignore next 10 hits + 2 breakpoint keep yes at :4 + (Pdb) condition 1 + Breakpoint 1 is now unconditional. + (Pdb) commands 1 + (com) EOF + + (Pdb) enable 1 + Enabled breakpoint 1 at :3 + (Pdb) clear 1 + Deleted breakpoint 1 at :3 + (Pdb) commands 2 + (com) p "42" + (com) print("42", 7*6) + (com) end + (Pdb) continue + 1 + '42' + 42 42 + > (4)test_function() + -> print(2) + (Pdb) clear + Clear all breaks? y + Deleted breakpoint 2 at :4 + (Pdb) tbreak 5 + Breakpoint 4 at :5 + (Pdb) continue + 2 + Deleted breakpoint 4 at :5 + > (5)test_function() + -> print(3) + (Pdb) break + (Pdb) commands 10 + *** cannot set commands: Breakpoint number 10 out of range + (Pdb) commands a + *** Invalid argument: a + Usage: (Pdb) commands [bpnumber] + (com) ... + (com) end + (Pdb) + (Pdb) commands 4 + *** cannot set commands: Breakpoint 4 already deleted + (Pdb) break 6, undefined + Breakpoint 5 at :6 + (Pdb) continue + 3 + > (6)test_function() + -> print(4) + (Pdb) continue + 4 + """ + +def test_pdb_breakpoint_ignore_and_condition(): + """ + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... for i in range(5): + ... print(i) + + >>> with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE + ... 'break 4', + ... 'ignore 1 2', # ignore once + ... 'continue', + ... 'condition 1 i == 4', + ... 'continue', + ... 'clear 1', + ... 'continue', + ... ]): + ... test_function() + > (2)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) break 4 + Breakpoint 1 at :4 + (Pdb) ignore 1 2 + Will ignore next 2 crossings of breakpoint 1. + (Pdb) continue + 0 + 1 + > (4)test_function() + -> print(i) + (Pdb) condition 1 i == 4 + New condition set for breakpoint 1. + (Pdb) continue + 2 + 3 + > (4)test_function() + -> print(i) + (Pdb) clear 1 + Deleted breakpoint 1 at :4 + (Pdb) continue + 4 + """ + +def test_pdb_breakpoint_on_annotated_function_def(): + """Test breakpoints on function definitions with annotation. + + >>> def foo[T](): + ... return 0 + + >>> def bar() -> int: + ... return 0 + + >>> def foobar[T]() -> int: + ... return 0 + + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... pass + + >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +NORMALIZE_WHITESPACE +SKIP + ... 'break foo', + ... 'break bar', + ... 'break foobar', + ... 'continue', + ... ]): + ... test_function() + > (2)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) break foo + Breakpoint 1 at :2 + (Pdb) break bar + Breakpoint 2 at :2 + (Pdb) break foobar + Breakpoint 3 at :2 + (Pdb) continue + """ + +def test_pdb_commands(): + """Test the commands command of pdb. + + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... print(1) + ... print(2) + ... print(3) + + >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +NORMALIZE_WHITESPACE +SKIP + ... 'b 3', + ... 'commands', + ... 'silent', # suppress the frame status output + ... 'p "hello"', + ... 'end', + ... 'b 4', + ... 'commands', + ... 'until 5', # no output, should stop at line 5 + ... 'continue', # hit breakpoint at line 3 + ... '', # repeat continue, hit breakpoint at line 4 then `until` to line 5 + ... '', + ... ]): + ... test_function() + > (2)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) b 3 + Breakpoint 1 at :3 + (Pdb) commands + (com) silent + (com) p "hello" + (com) end + (Pdb) b 4 + Breakpoint 2 at :4 + (Pdb) commands + (com) until 5 + (Pdb) continue + 'hello' + (Pdb) + 1 + 2 + > (5)test_function() + -> print(3) + (Pdb) + 3 + """ + +def test_pdb_breakpoint_with_filename(): + """Breakpoints with filename:lineno + + >>> def test_function(): + ... # inspect_fodder2 is a great module as the line number is stable + ... from test.test_inspect import inspect_fodder2 as mod2 + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... mod2.func88() + ... mod2.func114() + + >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS +SKIP + ... 'break test.test_inspect.inspect_fodder2:90', + ... 'continue', # will stop at func88 + ... 'break test/test_inspect/inspect_fodder2.py:115', + ... 'continue', # will stop at func114 + ... 'continue', + ... ]): + ... test_function() + > (4)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) break test.test_inspect.inspect_fodder2:90 + Breakpoint 1 at ...inspect_fodder2.py:90 + (Pdb) continue + > ...inspect_fodder2.py(90)func88() + -> return 90 + (Pdb) break test/test_inspect/inspect_fodder2.py:115 + Breakpoint 2 at ...inspect_fodder2.py:115 + (Pdb) continue + > ...inspect_fodder2.py(115)func114() + -> return 115 + (Pdb) continue + """ + +def test_pdb_breakpoint_on_disabled_line(): + """New breakpoint on once disabled line should work + + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... for i in range(3): + ... j = i * 2 + ... print(j) + + >>> with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE + ... 'break 5', + ... 'c', + ... 'clear 1', + ... 'break 4', + ... 'c', + ... 'clear 2', + ... 'c' + ... ]): + ... test_function() + > (2)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) break 5 + Breakpoint 1 at :5 + (Pdb) c + > (5)test_function() + -> print(j) + (Pdb) clear 1 + Deleted breakpoint 1 at :5 + (Pdb) break 4 + Breakpoint 2 at :4 + (Pdb) c + 0 + > (4)test_function() + -> j = i * 2 + (Pdb) clear 2 + Deleted breakpoint 2 at :4 + (Pdb) c + 2 + 4 + """ + +def test_pdb_breakpoints_preserved_across_interactive_sessions(): + """Breakpoints are remembered between interactive sessions + + >>> with PdbTestInput([ # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE + ... 'import test.test_pdb', + ... 'break test.test_pdb.do_something', + ... 'break test.test_pdb.do_nothing', + ... 'break', + ... 'continue', + ... ]): + ... pdb.run('print()') + > (1)()... + (Pdb) import test.test_pdb + (Pdb) break test.test_pdb.do_something + Breakpoint 1 at ...test_pdb.py:... + (Pdb) break test.test_pdb.do_nothing + Breakpoint 2 at ...test_pdb.py:... + (Pdb) break + Num Type Disp Enb Where + 1 breakpoint keep yes at ...test_pdb.py:... + 2 breakpoint keep yes at ...test_pdb.py:... + (Pdb) continue + + >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE +SKIP + ... 'break', + ... 'break pdb.find_function', + ... 'break', + ... 'clear 1', + ... 'continue', + ... ]): + ... pdb.run('print()') + > (1)()... + (Pdb) break + Num Type Disp Enb Where + 1 breakpoint keep yes at ...test_pdb.py:... + 2 breakpoint keep yes at ...test_pdb.py:... + (Pdb) break pdb.find_function + Breakpoint 3 at ...pdb.py:... + (Pdb) break + Num Type Disp Enb Where + 1 breakpoint keep yes at ...test_pdb.py:... + 2 breakpoint keep yes at ...test_pdb.py:... + 3 breakpoint keep yes at ...pdb.py:... + (Pdb) clear 1 + Deleted breakpoint 1 at ...test_pdb.py:... + (Pdb) continue + + >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE +SKIP + ... 'break', + ... 'clear 2', + ... 'clear 3', + ... 'continue', + ... ]): + ... pdb.run('print()') + > (1)()... + (Pdb) break + Num Type Disp Enb Where + 2 breakpoint keep yes at ...test_pdb.py:... + 3 breakpoint keep yes at ...pdb.py:... + (Pdb) clear 2 + Deleted breakpoint 2 at ...test_pdb.py:... + (Pdb) clear 3 + Deleted breakpoint 3 at ...pdb.py:... + (Pdb) continue + """ + +def test_pdb_break_anywhere(): + """Test break_anywhere() method of Pdb. + + >>> def outer(): + ... def inner(): + ... import pdb + ... import sys + ... p = pdb.Pdb(nosigint=True, readrc=False) + ... p.set_trace() + ... frame = sys._getframe() + ... print(p.break_anywhere(frame)) # inner + ... print(p.break_anywhere(frame.f_back)) # outer + ... print(p.break_anywhere(frame.f_back.f_back)) # caller + ... inner() + + >>> def caller(): + ... outer() + + >>> def test_function(): + ... caller() + + >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +NORMALIZE_WHITESPACE +SKIP + ... 'b 3', + ... 'c', + ... ]): + ... test_function() + > (6)inner() + -> p.set_trace() + (Pdb) b 3 + Breakpoint 1 at :3 + (Pdb) c + True + False + False + """ + +def test_pdb_pp_repr_exc(): + """Test that do_p/do_pp do not swallow exceptions. + + >>> class BadRepr: + ... def __repr__(self): + ... raise Exception('repr_exc') + >>> obj = BadRepr() + + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + + >>> with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE + ... 'p obj', + ... 'pp obj', + ... 'continue', + ... ]): + ... test_function() + > (2)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) p obj + *** Exception: repr_exc + (Pdb) pp obj + *** Exception: repr_exc + (Pdb) continue + """ + +def test_pdb_empty_line(): + """Test that empty line repeats the last command. + + >>> def test_function(): + ... x = 1 + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... y = 2 + + >>> with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE + ... 'p x', + ... '', # Should repeat p x + ... 'n ;; p 0 ;; p x', # Fill cmdqueue with multiple commands + ... '', # Should still repeat p x + ... 'continue', + ... ]): + ... test_function() + > (3)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) p x + 1 + (Pdb) + 1 + (Pdb) n ;; p 0 ;; p x + 0 + 1 + > (4)test_function() + -> y = 2 + (Pdb) + 1 + (Pdb) continue + """ + +def do_nothing(): + pass + +def do_something(): + print(42) + +def test_list_commands(): + """Test the list and source commands of pdb. + + >>> def test_function_2(foo): + ... import test.test_pdb + ... test.test_pdb.do_nothing() + ... 'some...' + ... 'more...' + ... 'code...' + ... 'to...' + ... 'make...' + ... 'a...' + ... 'long...' + ... 'listing...' + ... 'useful...' + ... '...' + ... '...' + ... return foo + + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... ret = test_function_2('baz') + + >>> with PdbTestInput([ # TODO: RUSTPYTHON; Flaky # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE +SKIP + ... 'step', # go to the test function line + ... 'list', # list first function + ... 'step', # step into second function + ... 'list', # list second function + ... 'list', # continue listing to EOF + ... 'list 1,3', # list specific lines + ... 'list x', # invalid argument + ... 'next', # step to import + ... 'next', # step over import + ... 'step', # step into do_nothing + ... 'longlist', # list all lines + ... 'source do_something', # list all lines of function + ... 'source fooxxx', # something that doesn't exit + ... 'continue', + ... ]): + ... test_function() + > (2)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) step + > (3)test_function() + -> ret = test_function_2('baz') + (Pdb) list + 1 def test_function(): + 2 import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + 3 -> ret = test_function_2('baz') + [EOF] + (Pdb) step + --Call-- + > (1)test_function_2() + -> def test_function_2(foo): + (Pdb) list + 1 -> def test_function_2(foo): + 2 import test.test_pdb + 3 test.test_pdb.do_nothing() + 4 'some...' + 5 'more...' + 6 'code...' + 7 'to...' + 8 'make...' + 9 'a...' + 10 'long...' + 11 'listing...' + (Pdb) list + 12 'useful...' + 13 '...' + 14 '...' + 15 return foo + [EOF] + (Pdb) list 1,3 + 1 -> def test_function_2(foo): + 2 import test.test_pdb + 3 test.test_pdb.do_nothing() + (Pdb) list x + *** ... + (Pdb) next + > (2)test_function_2() + -> import test.test_pdb + (Pdb) next + > (3)test_function_2() + -> test.test_pdb.do_nothing() + (Pdb) step + --Call-- + > ...test_pdb.py(...)do_nothing() + -> def do_nothing(): + (Pdb) longlist + ... -> def do_nothing(): + ... pass + (Pdb) source do_something + ... def do_something(): + ... print(42) + (Pdb) source fooxxx + *** ... + (Pdb) continue + """ + +def test_pdb_whatis_command(): + """Test the whatis command + + >>> myvar = (1,2) + >>> def myfunc(): + ... pass + + >>> class MyClass: + ... def mymethod(self): + ... pass + + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + + >>> with PdbTestInput([ # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE + ... 'whatis myvar', + ... 'whatis myfunc', + ... 'whatis MyClass', + ... 'whatis MyClass()', + ... 'whatis MyClass.mymethod', + ... 'whatis MyClass().mymethod', + ... 'continue', + ... ]): + ... test_function() + > (2)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) whatis myvar + + (Pdb) whatis myfunc + Function myfunc + (Pdb) whatis MyClass + Class test.test_pdb.MyClass + (Pdb) whatis MyClass() + + (Pdb) whatis MyClass.mymethod + Function mymethod + (Pdb) whatis MyClass().mymethod + Method mymethod + (Pdb) continue + """ + +def test_pdb_display_command(): + """Test display command + + >>> def test_function(): + ... a = 0 + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... a = 1 + ... a = 2 + ... a = 3 + ... a = 4 + + >>> with PdbTestInput([ # doctest: +ELLIPSIS + ... 's', + ... 'display +', + ... 'display', + ... 'display a', + ... 'n', + ... 'display', + ... 'undisplay a', + ... 'n', + ... 'display a', + ... 'undisplay', + ... 'display a < 1', + ... 'n', + ... 'display undefined', + ... 'continue', + ... ]): + ... test_function() + > (3)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) s + > (4)test_function() + -> a = 1 + (Pdb) display + + *** Unable to display +: SyntaxError: invalid syntax + (Pdb) display + No expression is being displayed + (Pdb) display a + display a: 0 + (Pdb) n + > (5)test_function() + -> a = 2 + display a: 1 [old: 0] + (Pdb) display + Currently displaying: + a: 1 + (Pdb) undisplay a + (Pdb) n + > (6)test_function() + -> a = 3 + (Pdb) display a + display a: 2 + (Pdb) undisplay + (Pdb) display a < 1 + display a < 1: False + (Pdb) n + > (7)test_function() + -> a = 4 + (Pdb) display undefined + display undefined: ** raised NameError: name 'undefined' is not defined ** + (Pdb) continue + """ + +def test_pdb_alias_command(): + """Test alias command + + >>> class A: + ... def __init__(self): + ... self.attr1 = 10 + ... self.attr2 = 'str' + ... def method(self): + ... pass + + >>> def test_function(): + ... o = A() + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... o.method() + + >>> with PdbTestInput([ # doctest: +ELLIPSIS + ... 's', + ... 'alias pi', + ... 'alias pi for k in %1.__dict__.keys(): print(f"%1.{k} = {%1.__dict__[k]}")', + ... 'alias ps pi self', + ... 'alias ps', + ... 'pi o', + ... 's', + ... 'ps', + ... 'alias myp p %2', + ... 'alias myp', + ... 'alias myp p %1', + ... 'myp', + ... 'myp 1', + ... 'myp 1 2', + ... 'alias repeat_second_arg p "%* %2"', + ... 'repeat_second_arg 1 2 3', + ... 'continue', + ... ]): + ... test_function() + > (3)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) s + > (4)test_function() + -> o.method() + (Pdb) alias pi + *** Unknown alias 'pi' + (Pdb) alias pi for k in %1.__dict__.keys(): print(f"%1.{k} = {%1.__dict__[k]}") + (Pdb) alias ps pi self + (Pdb) alias ps + ps = pi self + (Pdb) pi o + o.attr1 = 10 + o.attr2 = str + (Pdb) s + --Call-- + > (5)method() + -> def method(self): + (Pdb) ps + self.attr1 = 10 + self.attr2 = str + (Pdb) alias myp p %2 + *** Replaceable parameters must be consecutive + (Pdb) alias myp + *** Unknown alias 'myp' + (Pdb) alias myp p %1 + (Pdb) myp + *** Not enough arguments for alias 'myp' + (Pdb) myp 1 + 1 + (Pdb) myp 1 2 + *** Too many arguments for alias 'myp' + (Pdb) alias repeat_second_arg p "%* %2" + (Pdb) repeat_second_arg 1 2 3 + '1 2 3 2' + (Pdb) continue + """ + +def test_pdb_where_command(): + """Test where command + + >>> def g(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + + >>> def f(): + ... g() + + >>> def test_function(): + ... f() + + >>> with PdbTestInput([ # doctest: +ELLIPSIS + ... 'w', + ... 'where', + ... 'w 1', + ... 'w invalid', + ... 'u', + ... 'w', + ... 'w 0', + ... 'w 100', + ... 'w -100', + ... 'continue', + ... ]): + ... test_function() + > (2)g() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) w + ... + (13)() + -> test_function() + (2)test_function() + -> f() + (2)f() + -> g() + > (2)g() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) where + ... + (13)() + -> test_function() + (2)test_function() + -> f() + (2)f() + -> g() + > (2)g() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) w 1 + > (2)g() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) w invalid + *** Invalid count (invalid) + (Pdb) u + > (2)f() + -> g() + (Pdb) w + ... + (13)() + -> test_function() + (2)test_function() + -> f() + > (2)f() + -> g() + (2)g() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) w 0 + > (2)f() + -> g() + (Pdb) w 100 + ... + (13)() + -> test_function() + (2)test_function() + -> f() + > (2)f() + -> g() + (2)g() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) w -100 + ... + (13)() + -> test_function() + (2)test_function() + -> f() + > (2)f() + -> g() + (2)g() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) continue + """ + +def test_pdb_restart_command(): + """Test restart command + + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False, mode='inline').set_trace() + ... x = 1 + + >>> with PdbTestInput([ # doctest: +ELLIPSIS + ... 'restart', + ... 'continue', + ... ]): + ... test_function() + > (2)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False, mode='inline').set_trace() + (Pdb) restart + *** run/restart command is disabled when pdb is running in inline mode. + Use the command line interface to enable restarting your program + e.g. "python -m pdb myscript.py" + (Pdb) continue + """ + +def test_pdb_commands_with_set_trace(): + """Test that commands can be passed to Pdb.set_trace() + + >>> def test_function(): + ... x = 1 + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace(commands=['p x', 'c']) + + >>> test_function() + 1 + """ + + +# skip this test if sys.flags.no_site = True; +# exit() isn't defined unless there's a site module. +if not sys.flags.no_site: + def test_pdb_interact_command(): + """Test interact command + + >>> g = 0 + >>> dict_g = {} + + >>> def test_function(): + ... x = 1 + ... lst_local = [] + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + + >>> with PdbTestInput([ # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE + ... 'interact', + ... 'x', + ... 'g', + ... 'x = 2', + ... 'g = 3', + ... 'dict_g["a"] = True', + ... 'lst_local.append(x)', + ... 'exit()', + ... 'p x', + ... 'p g', + ... 'p dict_g', + ... 'p lst_local', + ... 'continue', + ... ]): + ... test_function() + > (4)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) interact + *pdb interact start* + ... x + 1 + ... g + 0 + ... x = 2 + ... g = 3 + ... dict_g["a"] = True + ... lst_local.append(x) + ... exit() + *exit from pdb interact command* + (Pdb) p x + 1 + (Pdb) p g + 0 + (Pdb) p dict_g + {'a': True} + (Pdb) p lst_local + [2] + (Pdb) continue + """ + +def test_convenience_variables(): + """Test convenience variables + + >>> def util_function(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... try: + ... raise Exception('test') + ... except Exception: + ... pass + ... return 1 + + >>> def test_function(): + ... util_function() + + >>> with PdbTestInput([ # TODO: RUSTPYTHON; hangs # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE +SKIP + ... 'step', # Step to try statement + ... '$_frame.f_lineno', # Check frame convenience variable + ... '$ _frame', # This should be a syntax error + ... '$a = 10', # Set a convenience variable + ... '$a', # Print its value + ... 'p "$a"', # Print the string $a + ... 'p $a + 2', # Do some calculation + ... 'p f"$a = {$a}"', # Make sure $ in string is not converted and f-string works + ... 'u', # Switch frame + ... '$_frame.f_lineno', # Make sure the frame changed + ... '$a', # Make sure the value persists + ... 'd', # Go back to the original frame + ... 'next', + ... '$a', # The value should be gone + ... 'next', + ... '$_exception', # Check exception convenience variable + ... 'next', + ... '$_exception', # Exception should be gone + ... 'return', + ... '$_retval', # Check return convenience variable + ... 'continue', + ... ]): + ... test_function() + > (2)util_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) step + > (3)util_function() + -> try: + (Pdb) $_frame.f_lineno + 3 + (Pdb) $ _frame + *** SyntaxError: invalid syntax + (Pdb) $a = 10 + (Pdb) $a + 10 + (Pdb) p "$a" + '$a' + (Pdb) p $a + 2 + 12 + (Pdb) p f"$a = {$a}" + '$a = 10' + (Pdb) u + > (2)test_function() + -> util_function() + (Pdb) $_frame.f_lineno + 2 + (Pdb) $a + 10 + (Pdb) d + > (3)util_function() + -> try: + (Pdb) next + > (4)util_function() + -> raise Exception('test') + (Pdb) $a + *** KeyError: 'a' + (Pdb) next + Exception: test + > (4)util_function() + -> raise Exception('test') + (Pdb) $_exception + Exception('test') + (Pdb) next + > (5)util_function() + -> except Exception: + (Pdb) $_exception + *** KeyError: '_exception' + (Pdb) return + --Return-- + > (7)util_function()->1 + -> return 1 + (Pdb) $_retval + 1 + (Pdb) continue + """ + + +def test_post_mortem_chained(): + """Test post mortem traceback debugging of chained exception + + >>> def test_function_2(): + ... try: + ... 1/0 + ... finally: + ... print('Exception!') + + >>> def test_function_reraise(): + ... try: + ... test_function_2() + ... except ZeroDivisionError as e: + ... raise ZeroDivisionError('reraised') from e + + >>> def test_function(): + ... import pdb; + ... instance = pdb.Pdb(nosigint=True, readrc=False) + ... try: + ... test_function_reraise() + ... except Exception as e: + ... pdb._post_mortem(e, instance) + + >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE +EXPECTED_FAILURE + ... 'exceptions', + ... 'exceptions 0', + ... '$_exception', + ... 'up', + ... 'down', + ... 'exceptions 1', + ... '$_exception', + ... 'up', + ... 'down', + ... 'exceptions -1', + ... 'exceptions 3', + ... 'up', + ... 'exit', + ... ]): + ... try: + ... test_function() + ... except ZeroDivisionError: + ... print('Correctly reraised.') + Exception! + > (5)test_function_reraise() + -> raise ZeroDivisionError('reraised') from e + (Pdb) exceptions + 0 ZeroDivisionError('division by zero') + > 1 ZeroDivisionError('reraised') + (Pdb) exceptions 0 + > (3)test_function_2() + -> 1/0 + (Pdb) $_exception + ZeroDivisionError('division by zero') + (Pdb) up + > (3)test_function_reraise() + -> test_function_2() + (Pdb) down + > (3)test_function_2() + -> 1/0 + (Pdb) exceptions 1 + > (5)test_function_reraise() + -> raise ZeroDivisionError('reraised') from e + (Pdb) $_exception + ZeroDivisionError('reraised') + (Pdb) up + > (5)test_function() + -> test_function_reraise() + (Pdb) down + > (5)test_function_reraise() + -> raise ZeroDivisionError('reraised') from e + (Pdb) exceptions -1 + *** No exception with that number + (Pdb) exceptions 3 + *** No exception with that number + (Pdb) up + > (5)test_function() + -> test_function_reraise() + (Pdb) exit + """ + + +def test_post_mortem_cause_no_context(): + """Test post mortem traceback debugging of chained exception + + >>> def make_exc_with_stack(type_, *content, from_=None): + ... try: + ... raise type_(*content) from from_ + ... except Exception as out: + ... return out + ... + + >>> def main(): + ... try: + ... raise ValueError('Context Not Shown') + ... except Exception as e1: + ... raise ValueError("With Cause") from make_exc_with_stack(TypeError,'The Cause') + + >>> def test_function(): + ... import pdb; + ... instance = pdb.Pdb(nosigint=True, readrc=False) + ... try: + ... main() + ... except Exception as e: + ... pdb._post_mortem(e, instance) + + >>> with PdbTestInput([ # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE + ... 'exceptions', + ... 'exceptions 0', + ... 'exceptions 1', + ... 'up', + ... 'down', + ... 'exit', + ... ]): + ... try: + ... test_function() + ... except ValueError: + ... print('Ok.') + > (5)main() + -> raise ValueError("With Cause") from make_exc_with_stack(TypeError,'The Cause') + (Pdb) exceptions + 0 TypeError('The Cause') + > 1 ValueError('With Cause') + (Pdb) exceptions 0 + > (3)make_exc_with_stack() + -> raise type_(*content) from from_ + (Pdb) exceptions 1 + > (5)main() + -> raise ValueError("With Cause") from make_exc_with_stack(TypeError,'The Cause') + (Pdb) up + > (5)test_function() + -> main() + (Pdb) down + > (5)main() + -> raise ValueError("With Cause") from make_exc_with_stack(TypeError,'The Cause') + (Pdb) exit""" + + +def test_post_mortem_context_of_the_cause(): + """Test post mortem traceback debugging of chained exception + + + >>> def main(): + ... try: + ... raise TypeError('Context of the cause') + ... except Exception as e1: + ... try: + ... raise ValueError('Root Cause') + ... except Exception as e2: + ... ex = e2 + ... raise ValueError("With Cause, and cause has context") from ex + + >>> def test_function(): + ... import pdb; + ... instance = pdb.Pdb(nosigint=True, readrc=False) + ... try: + ... main() + ... except Exception as e: + ... pdb._post_mortem(e, instance) + + >>> with PdbTestInput([ # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE + ... 'exceptions', + ... 'exceptions 2', + ... 'up', + ... 'down', + ... 'exceptions 3', + ... 'up', + ... 'down', + ... 'exceptions 4', + ... 'up', + ... 'down', + ... 'exit', + ... ]): + ... try: + ... test_function() + ... except ValueError: + ... print('Correctly reraised.') + > (9)main() + -> raise ValueError("With Cause, and cause has context") from ex + (Pdb) exceptions + 0 TypeError('Context of the cause') + 1 ValueError('Root Cause') + > 2 ValueError('With Cause, and cause has context') + (Pdb) exceptions 2 + > (9)main() + -> raise ValueError("With Cause, and cause has context") from ex + (Pdb) up + > (5)test_function() + -> main() + (Pdb) down + > (9)main() + -> raise ValueError("With Cause, and cause has context") from ex + (Pdb) exceptions 3 + *** No exception with that number + (Pdb) up + > (5)test_function() + -> main() + (Pdb) down + > (9)main() + -> raise ValueError("With Cause, and cause has context") from ex + (Pdb) exceptions 4 + *** No exception with that number + (Pdb) up + > (5)test_function() + -> main() + (Pdb) down + > (9)main() + -> raise ValueError("With Cause, and cause has context") from ex + (Pdb) exit + """ + + +def test_post_mortem_from_none(): + """Test post mortem traceback debugging of chained exception + + In particular that cause from None (which sets __suppress_context__ to True) + does not show context. + + + >>> def main(): + ... try: + ... raise TypeError('Context of the cause') + ... except Exception as e1: + ... raise ValueError("With Cause, and cause has context") from None + + >>> def test_function(): + ... import pdb; + ... instance = pdb.Pdb(nosigint=True, readrc=False) + ... try: + ... main() + ... except Exception as e: + ... pdb._post_mortem(e, instance) + + >>> with PdbTestInput([ # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE + ... 'exceptions', + ... 'exit', + ... ]): + ... try: + ... test_function() + ... except ValueError: + ... print('Correctly reraised.') + > (5)main() + -> raise ValueError("With Cause, and cause has context") from None + (Pdb) exceptions + > 0 ValueError('With Cause, and cause has context') + (Pdb) exit + """ + + +def test_post_mortem_from_no_stack(): + """Test post mortem traceback debugging of chained exception + + especially when one exception has no stack. + + >>> def main(): + ... raise Exception() from Exception() + + + >>> def test_function(): + ... import pdb; + ... instance = pdb.Pdb(nosigint=True, readrc=False) + ... try: + ... main() + ... except Exception as e: + ... pdb._post_mortem(e, instance) + + >>> with PdbTestInput( # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE + ... ["exceptions", + ... "exceptions 0", + ... "exit"], + ... ): + ... try: + ... test_function() + ... except ValueError: + ... print('Correctly reraised.') + > (2)main() + -> raise Exception() from Exception() + (Pdb) exceptions + - Exception() + > 1 Exception() + (Pdb) exceptions 0 + *** This exception does not have a traceback, cannot jump to it + (Pdb) exit + """ + + +def test_post_mortem_single_no_stack(): + """Test post mortem called when origin exception has no stack + + + >>> def test_function(): + ... import pdb; + ... instance = pdb.Pdb(nosigint=True, readrc=False) + ... import sys + ... sys.last_exc = Exception() + ... pdb._post_mortem(sys.last_exc, instance) + + >>> with PdbTestInput( # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE + ... [] + ... ): + ... try: + ... test_function() + ... except ValueError as e: + ... print(e) + A valid traceback must be passed if no exception is being handled + """ + +def test_post_mortem_complex(): + """Test post mortem traceback debugging of chained exception + + Test with simple and complex cycles, exception groups,... + + >>> def make_ex_with_stack(type_, *content, from_=None): + ... try: + ... raise type_(*content) from from_ + ... except Exception as out: + ... return out + ... + + >>> def cycle(): + ... try: + ... raise ValueError("Cycle Leaf") + ... except Exception as e: + ... raise e from e + ... + + >>> def tri_cycle(): + ... a = make_ex_with_stack(ValueError, "Cycle1") + ... b = make_ex_with_stack(ValueError, "Cycle2") + ... c = make_ex_with_stack(ValueError, "Cycle3") + ... + ... a.__cause__ = b + ... b.__cause__ = c + ... + ... raise c from a + ... + + >>> def cause(): + ... try: + ... raise ValueError("Cause Leaf") + ... except Exception as e: + ... raise e + ... + + >>> def context(n=10): + ... try: + ... raise ValueError(f"Context Leaf {n}") + ... except Exception as e: + ... if n == 0: + ... raise ValueError(f"With Context {n}") from e + ... else: + ... context(n - 1) + ... + + >>> def main(): + ... try: + ... cycle() + ... except Exception as e1: + ... try: + ... tri_cycle() + ... except Exception as e2: + ... ex = e2 + ... raise ValueError("With Context and With Cause") from ex + + + >>> def test_function(): + ... import pdb; + ... instance = pdb.Pdb(nosigint=True, readrc=False) + ... try: + ... main() + ... except Exception as e: + ... pdb._post_mortem(e, instance) + + >>> with PdbTestInput( # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE + ... ["exceptions", + ... "exceptions 0", + ... "exceptions 1", + ... "exceptions 2", + ... "exceptions 3", + ... "exit"], + ... ): + ... try: + ... test_function() + ... except ValueError: + ... print('Correctly reraised.') + > (9)main() + -> raise ValueError("With Context and With Cause") from ex + (Pdb) exceptions + 0 ValueError('Cycle2') + 1 ValueError('Cycle1') + 2 ValueError('Cycle3') + > 3 ValueError('With Context and With Cause') + (Pdb) exceptions 0 + > (3)make_ex_with_stack() + -> raise type_(*content) from from_ + (Pdb) exceptions 1 + > (3)make_ex_with_stack() + -> raise type_(*content) from from_ + (Pdb) exceptions 2 + > (3)make_ex_with_stack() + -> raise type_(*content) from from_ + (Pdb) exceptions 3 + > (9)main() + -> raise ValueError("With Context and With Cause") from ex + (Pdb) exit + """ + + +def test_post_mortem(): + """Test post mortem traceback debugging. + + >>> def test_function_2(): + ... try: + ... 1/0 + ... finally: + ... print('Exception!') + + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... test_function_2() + ... print('Not reached.') + + >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE +EXPECTED_FAILURE + ... 'step', # step to test_function_2() line + ... 'next', # step over exception-raising call + ... 'bt', # get a backtrace + ... 'list', # list code of test_function() + ... 'down', # step into test_function_2() + ... 'list', # list code of test_function_2() + ... 'continue', + ... ]): + ... try: + ... test_function() + ... except ZeroDivisionError: + ... print('Correctly reraised.') + > (2)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) step + > (3)test_function() + -> test_function_2() + (Pdb) next + Exception! + ZeroDivisionError: division by zero + > (3)test_function() + -> test_function_2() + (Pdb) bt + ... + (11)() + -> test_function() + > (3)test_function() + -> test_function_2() + (3)test_function_2() + -> 1/0 + (Pdb) list + 1 def test_function(): + 2 import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + 3 -> test_function_2() + 4 print('Not reached.') + [EOF] + (Pdb) down + > (3)test_function_2() + -> 1/0 + (Pdb) list + 1 def test_function_2(): + 2 try: + 3 >> 1/0 + 4 finally: + 5 -> print('Exception!') + [EOF] + (Pdb) continue + Correctly reraised. + """ + + +def test_pdb_return_to_different_file(): + """When pdb returns to a different file, it should not skip if f_trace is + not already set + + >>> import pprint + + >>> class A: + ... def __repr__(self): + ... return 'A' + + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... pprint.pprint(A()) + + >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE +SKIP + ... 'b A.__repr__', + ... 'continue', + ... 'return', + ... 'next', + ... 'return', + ... 'return', + ... 'continue', + ... ]): + ... test_function() + > (2)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) b A.__repr__ + Breakpoint 1 at :3 + (Pdb) continue + > (3)__repr__() + -> return 'A' + (Pdb) return + --Return-- + > (3)__repr__()->'A' + -> return 'A' + (Pdb) next + > ...pprint.py..._safe_repr() + -> return rep,... + (Pdb) return + --Return-- + > ...pprint.py..._safe_repr()->('A'...) + -> return rep,... + (Pdb) return + --Return-- + > ...pprint.py...format()->('A'...) + -> return... + (Pdb) continue + A + """ + + +def test_pdb_skip_modules(): + """This illustrates the simple case of module skipping. + + >>> def skip_module(): + ... import string + ... import pdb; pdb.Pdb(skip=['stri*'], nosigint=True, readrc=False).set_trace() + ... string.capwords('FOO') + + >>> with PdbTestInput([ + ... 'step', + ... 'step', + ... 'continue', + ... ]): + ... skip_module() + > (3)skip_module() + -> import pdb; pdb.Pdb(skip=['stri*'], nosigint=True, readrc=False).set_trace() + (Pdb) step + > (4)skip_module() + -> string.capwords('FOO') + (Pdb) step + --Return-- + > (4)skip_module()->None + -> string.capwords('FOO') + (Pdb) continue + """ + +def test_pdb_invalid_arg(): + """This tests pdb commands that have invalid arguments + + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + + >>> with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE + ... 'a = 3', + ... 'll 4', + ... 'step 1', + ... 'p', + ... 'enable ', + ... 'continue' + ... ]): + ... test_function() + > (2)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) a = 3 + *** Invalid argument: = 3 + Usage: a(rgs) + (Pdb) ll 4 + *** Invalid argument: 4 + Usage: ll | longlist + (Pdb) step 1 + *** Invalid argument: 1 + Usage: s(tep) + (Pdb) p + *** Argument is required for this command + Usage: p expression + (Pdb) enable + *** Argument is required for this command + Usage: enable bpnumber [bpnumber ...] + (Pdb) continue + """ + + +# Module for testing skipping of module that makes a callback +mod = types.ModuleType('module_to_skip') +exec('def foo_pony(callback): x = 1; callback(); return None', mod.__dict__) + + +def test_pdb_skip_modules_with_callback(): + """This illustrates skipping of modules that call into other code. + + >>> def skip_module(): + ... def callback(): + ... return None + ... import pdb; pdb.Pdb(skip=['module_to_skip*'], nosigint=True, readrc=False).set_trace() + ... mod.foo_pony(callback) + + >>> with PdbTestInput([ # TODO: RUSTPYTHON; Flaky # doctest: +SKIP + ... 'step', + ... 'step', + ... 'step', + ... 'step', + ... 'step', + ... 'step', + ... 'continue', + ... ]): + ... skip_module() + ... pass # provides something to "step" to + > (4)skip_module() + -> import pdb; pdb.Pdb(skip=['module_to_skip*'], nosigint=True, readrc=False).set_trace() + (Pdb) step + > (5)skip_module() + -> mod.foo_pony(callback) + (Pdb) step + --Call-- + > (2)callback() + -> def callback(): + (Pdb) step + > (3)callback() + -> return None + (Pdb) step + --Return-- + > (3)callback()->None + -> return None + (Pdb) step + --Return-- + > (5)skip_module()->None + -> mod.foo_pony(callback) + (Pdb) step + > (11)() + -> pass # provides something to "step" to + (Pdb) continue + """ + + +def test_pdb_continue_in_bottomframe(): + """Test that "continue" and "next" work properly in bottom frame (issue #5294). + + >>> def test_function(): + ... import pdb, sys; inst = pdb.Pdb(nosigint=True, readrc=False) + ... inst.set_trace() + ... inst.botframe = sys._getframe() # hackery to get the right botframe + ... print(1) + ... print(2) + ... print(3) + ... print(4) + + >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +ELLIPSIS +SKIP + ... 'step', + ... 'next', + ... 'break 7', + ... 'continue', + ... 'next', + ... 'continue', + ... 'continue', + ... ]): + ... test_function() + > (3)test_function() + -> inst.set_trace() + (Pdb) step + > (4)test_function() + -> inst.botframe = sys._getframe() # hackery to get the right botframe + (Pdb) next + > (5)test_function() + -> print(1) + (Pdb) break 7 + Breakpoint ... at :7 + (Pdb) continue + 1 + 2 + > (7)test_function() + -> print(3) + (Pdb) next + 3 + > (8)test_function() + -> print(4) + (Pdb) continue + 4 + """ + + +def pdb_invoke(method, arg): + """Run pdb.method(arg).""" + getattr(pdb.Pdb(nosigint=True, readrc=False), method)(arg) + + +def test_pdb_run_with_incorrect_argument(): + """Testing run and runeval with incorrect first argument. + + >>> pti = PdbTestInput(['continue',]) + >>> with pti: # TODO: RUSTPYTHON # doctest: +EXPECTED_FAILURE + ... pdb_invoke('run', lambda x: x) + Traceback (most recent call last): + TypeError: exec() arg 1 must be a string, bytes or code object + + >>> with pti: # TODO: RUSTPYTHON # doctest: +EXPECTED_FAILURE + ... pdb_invoke('runeval', lambda x: x) + Traceback (most recent call last): + TypeError: eval() arg 1 must be a string, bytes or code object + """ + + +def test_pdb_run_with_code_object(): + """Testing run and runeval with code object as a first argument. + + >>> with PdbTestInput(['step','x', 'continue']): # doctest: +ELLIPSIS + ... pdb_invoke('run', compile('x=1', '', 'exec')) + > (1)()... + (Pdb) step + --Return-- + > (1)()->None + (Pdb) x + 1 + (Pdb) continue + + >>> with PdbTestInput(['x', 'continue']): + ... x=0 + ... pdb_invoke('runeval', compile('x+1', '', 'eval')) + > (1)()->None + (Pdb) x + 1 + (Pdb) continue + """ + +def test_next_until_return_at_return_event(): + """Test that pdb stops after a next/until/return issued at a return debug event. + + >>> def test_function_2(): + ... x = 1 + ... x = 2 + + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... test_function_2() + ... test_function_2() + ... test_function_2() + ... end = 1 + + >>> with PdbTestInput(['break test_function_2', # TODO: RUSTPYTHON # doctest: +SKIP + ... 'continue', + ... 'return', + ... 'next', + ... 'continue', + ... 'return', + ... 'until', + ... 'continue', + ... 'return', + ... 'return', + ... 'continue']): + ... test_function() + > (2)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) break test_function_2 + Breakpoint 1 at :2 + (Pdb) continue + > (2)test_function_2() + -> x = 1 + (Pdb) return + --Return-- + > (3)test_function_2()->None + -> x = 2 + (Pdb) next + > (4)test_function() + -> test_function_2() + (Pdb) continue + > (2)test_function_2() + -> x = 1 + (Pdb) return + --Return-- + > (3)test_function_2()->None + -> x = 2 + (Pdb) until + > (5)test_function() + -> test_function_2() + (Pdb) continue + > (2)test_function_2() + -> x = 1 + (Pdb) return + --Return-- + > (3)test_function_2()->None + -> x = 2 + (Pdb) return + > (6)test_function() + -> end = 1 + (Pdb) continue + """ + +def test_pdb_next_command_for_generator(): + """Testing skip unwinding stack on yield for generators for "next" command + + >>> def test_gen(): + ... yield 0 + ... return 1 + ... yield 2 + + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... it = test_gen() + ... try: + ... if next(it) != 0: + ... raise AssertionError + ... next(it) + ... except StopIteration as ex: + ... if ex.value != 1: + ... raise AssertionError + ... print("finished") + + >>> with PdbTestInput(['step', # TODO: RUSTPYTHON # doctest: +EXPECTED_FAILURE + ... 'step', + ... 'step', + ... 'step', + ... 'next', + ... 'next', + ... 'step', + ... 'step', + ... 'continue']): + ... test_function() + > (2)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) step + > (3)test_function() + -> it = test_gen() + (Pdb) step + > (4)test_function() + -> try: + (Pdb) step + > (5)test_function() + -> if next(it) != 0: + (Pdb) step + --Call-- + > (1)test_gen() + -> def test_gen(): + (Pdb) next + > (2)test_gen() + -> yield 0 + (Pdb) next + > (3)test_gen() + -> return 1 + (Pdb) step + --Return-- + > (3)test_gen()->1 + -> return 1 + (Pdb) step + StopIteration: 1 + > (7)test_function() + -> next(it) + (Pdb) continue + finished + """ + +if not SKIP_CORO_TESTS: + if has_socket_support: + def test_pdb_asynctask(): + """Testing $_asynctask is accessible in async context + + >>> import asyncio + + >>> async def test(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + + >>> def test_function(): + ... asyncio.run(test(), loop_factory=asyncio.EventLoop) + + >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +ELLIPSIS +EXPECTED_FAILURE + ... '$_asynctask', + ... 'continue', + ... ]): + ... test_function() + > (2)test() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) $_asynctask + :2> ... + (Pdb) continue + """ + + def test_pdb_await_support(): + """Testing await support in pdb + + >>> import asyncio + + >>> async def test(): + ... print("hello") + ... await asyncio.sleep(0) + ... print("world") + ... return 42 + + >>> async def main(): + ... import pdb + ... task = asyncio.create_task(test()) + ... await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + ... pass + + >>> def test_function(): + ... asyncio.run(main(), loop_factory=asyncio.EventLoop) + + >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +ELLIPSIS +EXPECTED_FAILURE + ... 'x = await task', + ... 'p x', + ... 'x = await test()', + ... 'p x', + ... 'new_task = asyncio.create_task(test())', + ... 'await new_task', + ... 'await non_exist()', + ... 's', + ... 'continue', + ... ]): + ... test_function() + > (4)main() + -> await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + (Pdb) x = await task + hello + world + > (4)main() + -> await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + (Pdb) p x + 42 + (Pdb) x = await test() + hello + world + > (4)main() + -> await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + (Pdb) p x + 42 + (Pdb) new_task = asyncio.create_task(test()) + (Pdb) await new_task + hello + world + > (4)main() + -> await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + (Pdb) await non_exist() + *** NameError: name 'non_exist' is not defined + > (4)main() + -> await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + (Pdb) s + > (5)main() + -> pass + (Pdb) continue + """ + + def test_pdb_await_with_breakpoint(): + """Testing await support with breakpoints set in tasks + + >>> import asyncio + + >>> async def test(): + ... x = 2 + ... await asyncio.sleep(0) + ... return 42 + + >>> async def main(): + ... import pdb + ... task = asyncio.create_task(test()) + ... await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + + >>> def test_function(): + ... asyncio.run(main(), loop_factory=asyncio.EventLoop) + + >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +ELLIPSIS +SKIP + ... 'b test', + ... 'k = await task', + ... 'n', + ... 'p x', + ... 'continue', + ... 'p k', + ... 'continue', + ... ]): + ... test_function() + > (4)main() + -> await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + (Pdb) b test + Breakpoint 1 at :2 + (Pdb) k = await task + > (2)test() + -> x = 2 + (Pdb) n + > (3)test() + -> await asyncio.sleep(0) + (Pdb) p x + 2 + (Pdb) continue + > (4)main() + -> await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + (Pdb) p k + 42 + (Pdb) continue + """ + + def test_pdb_await_contextvar(): + """Testing await support context vars + + >>> import asyncio + >>> import contextvars + + >>> var = contextvars.ContextVar('var') + + >>> async def get_var(): + ... return var.get() + + >>> async def set_var(val): + ... var.set(val) + ... return var.get() + + >>> async def main(): + ... var.set(42) + ... import pdb + ... await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + + >>> def test_function(): + ... asyncio.run(main(), loop_factory=asyncio.EventLoop) + + >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +EXPECTED_FAILURE + ... 'p var.get()', + ... 'print(await get_var())', + ... 'print(await asyncio.create_task(set_var(100)))', + ... 'p var.get()', + ... 'print(await set_var(99))', + ... 'p var.get()', + ... 'print(await get_var())', + ... 'continue', + ... ]): + ... test_function() + > (4)main() + -> await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + (Pdb) p var.get() + 42 + (Pdb) print(await get_var()) + 42 + > (4)main() + -> await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + (Pdb) print(await asyncio.create_task(set_var(100))) + 100 + > (4)main() + -> await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + (Pdb) p var.get() + 42 + (Pdb) print(await set_var(99)) + 99 + > (4)main() + -> await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + (Pdb) p var.get() + 99 + (Pdb) print(await get_var()) + 99 + > (4)main() + -> await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + (Pdb) continue + """ + + def test_pdb_next_command_for_coroutine(): + """Testing skip unwinding stack on yield for coroutines for "next" command + + >>> from test.support import run_yielding_async_fn, async_yield + + >>> async def test_coro(): + ... await async_yield(0) + ... await async_yield(0) + ... await async_yield(0) + + >>> async def test_main(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... await test_coro() + + >>> def test_function(): + ... run_yielding_async_fn(test_main) + ... print("finished") + + >>> with PdbTestInput(['step', # TODO: RUSTPYTHON; Flaky # doctest: +SKIP + ... 'step', + ... 'step', + ... 'next', + ... 'next', + ... 'next', + ... 'step', + ... 'continue']): + ... test_function() + > (2)test_main() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) step + > (3)test_main() + -> await test_coro() + (Pdb) step + --Call-- + > (1)test_coro() + -> async def test_coro(): + (Pdb) step + > (2)test_coro() + -> await async_yield(0) + (Pdb) next + > (3)test_coro() + -> await async_yield(0) + (Pdb) next + > (4)test_coro() + -> await async_yield(0) + (Pdb) next + Internal StopIteration + > (3)test_main() + -> await test_coro() + (Pdb) step + --Return-- + > (3)test_main()->None + -> await test_coro() + (Pdb) continue + finished + """ + + def test_pdb_next_command_for_asyncgen(): + """Testing skip unwinding stack on yield for coroutines for "next" command + + >>> from test.support import run_yielding_async_fn, async_yield + + >>> async def agen(): + ... yield 1 + ... await async_yield(0) + ... yield 2 + + >>> async def test_coro(): + ... async for x in agen(): + ... print(x) + + >>> async def test_main(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... await test_coro() + + >>> def test_function(): + ... run_yielding_async_fn(test_main) + ... print("finished") + + >>> with PdbTestInput(['step', + ... 'step', + ... 'step', + ... 'next', + ... 'next', + ... 'step', + ... 'next', + ... 'continue']): + ... test_function() + > (2)test_main() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) step + > (3)test_main() + -> await test_coro() + (Pdb) step + --Call-- + > (1)test_coro() + -> async def test_coro(): + (Pdb) step + > (2)test_coro() + -> async for x in agen(): + (Pdb) next + > (3)test_coro() + -> print(x) + (Pdb) next + 1 + > (2)test_coro() + -> async for x in agen(): + (Pdb) step + --Call-- + > (2)agen() + -> yield 1 + (Pdb) next + > (3)agen() + -> await async_yield(0) + (Pdb) continue + 2 + finished + """ + +def test_pdb_return_command_for_generator(): + """Testing no unwinding stack on yield for generators + for "return" command + + >>> def test_gen(): + ... yield 0 + ... return 1 + ... yield 2 + + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... it = test_gen() + ... try: + ... if next(it) != 0: + ... raise AssertionError + ... next(it) + ... except StopIteration as ex: + ... if ex.value != 1: + ... raise AssertionError + ... print("finished") + + >>> with PdbTestInput(['step', # TODO: RUSTPYTHON # doctest: +EXPECTED_FAILURE + ... 'step', + ... 'step', + ... 'step', + ... 'return', + ... 'step', + ... 'step', + ... 'continue']): + ... test_function() + > (2)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) step + > (3)test_function() + -> it = test_gen() + (Pdb) step + > (4)test_function() + -> try: + (Pdb) step + > (5)test_function() + -> if next(it) != 0: + (Pdb) step + --Call-- + > (1)test_gen() + -> def test_gen(): + (Pdb) return + StopIteration: 1 + > (7)test_function() + -> next(it) + (Pdb) step + > (8)test_function() + -> except StopIteration as ex: + (Pdb) step + > (9)test_function() + -> if ex.value != 1: + (Pdb) continue + finished + """ + +if not SKIP_CORO_TESTS: + def test_pdb_return_command_for_coroutine(): + """Testing no unwinding stack on yield for coroutines for "return" command + + >>> from test.support import run_yielding_async_fn, async_yield + + >>> async def test_coro(): + ... await async_yield(0) + ... await async_yield(0) + ... await async_yield(0) + + >>> async def test_main(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... await test_coro() + + >>> def test_function(): + ... run_yielding_async_fn(test_main) + ... print("finished") + + >>> with PdbTestInput(['step', # TODO: RUSTPYTHON; Flaky # doctest: +SKIP + ... 'step', + ... 'step', + ... 'next', + ... 'continue']): + ... test_function() + > (2)test_main() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) step + > (3)test_main() + -> await test_coro() + (Pdb) step + --Call-- + > (1)test_coro() + -> async def test_coro(): + (Pdb) step + > (2)test_coro() + -> await async_yield(0) + (Pdb) next + > (3)test_coro() + -> await async_yield(0) + (Pdb) continue + finished + """ + +def test_pdb_until_command_for_generator(): + """Testing no unwinding stack on yield for generators + for "until" command if target breakpoint is not reached + + >>> def test_gen(): + ... yield 0 + ... yield 1 + ... yield 2 + + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... for i in test_gen(): + ... print(i) + ... print("finished") + + >>> with PdbTestInput(['step', # TODO: RUSTPYTHON; Flaky # doctest: +SKIP + ... 'step', + ... 'until 4', + ... 'step', + ... 'step', + ... 'continue']): + ... test_function() + > (2)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) step + > (3)test_function() + -> for i in test_gen(): + (Pdb) step + --Call-- + > (1)test_gen() + -> def test_gen(): + (Pdb) until 4 + 0 + 1 + > (4)test_gen() + -> yield 2 + (Pdb) step + --Return-- + > (4)test_gen()->2 + -> yield 2 + (Pdb) step + > (4)test_function() + -> print(i) + (Pdb) continue + 2 + finished + """ + +if not SKIP_CORO_TESTS: + def test_pdb_until_command_for_coroutine(): + """Testing no unwinding stack for coroutines + for "until" command if target breakpoint is not reached + + >>> from test.support import run_yielding_async_fn, async_yield + + >>> async def test_coro(): + ... print(0) + ... await async_yield(0) + ... print(1) + ... await async_yield(0) + ... print(2) + ... await async_yield(0) + ... print(3) + + >>> async def test_main(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... await test_coro() + + >>> def test_function(): + ... run_yielding_async_fn(test_main) + ... print("finished") + + >>> with PdbTestInput(['step', # TODO: RUSTPYTHON; Flaky # doctest: +SKIP + ... 'step', + ... 'until 8', + ... 'continue']): + ... test_function() + > (2)test_main() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) step + > (3)test_main() + -> await test_coro() + (Pdb) step + --Call-- + > (1)test_coro() + -> async def test_coro(): + (Pdb) until 8 + 0 + 1 + 2 + > (8)test_coro() + -> print(3) + (Pdb) continue + 3 + finished + """ + +def test_pdb_next_command_in_generator_for_loop(): + """The next command on returning from a generator controlled by a for loop. + + >>> def test_gen(): + ... yield 0 + ... return 1 + + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... for i in test_gen(): + ... print('value', i) + ... x = 123 + + >>> with PdbTestInput(['break test_gen', # TODO: RUSTPYTHON # doctest: +SKIP + ... 'continue', + ... 'next', + ... 'next', + ... 'next', + ... 'continue']): + ... test_function() + > (2)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) break test_gen + Breakpoint 1 at :2 + (Pdb) continue + > (2)test_gen() + -> yield 0 + (Pdb) next + value 0 + > (3)test_gen() + -> return 1 + (Pdb) next + Internal StopIteration: 1 + > (3)test_function() + -> for i in test_gen(): + (Pdb) next + > (5)test_function() + -> x = 123 + (Pdb) continue + """ + +def test_pdb_next_command_subiterator(): + """The next command in a generator with a subiterator. + + >>> def test_subgenerator(): + ... yield 0 + ... return 1 + + >>> def test_gen(): + ... x = yield from test_subgenerator() + ... return x + + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... for i in test_gen(): + ... print('value', i) + ... x = 123 + + >>> with PdbTestInput(['step', # TODO: RUSTPYTHON; Flaky # doctest: +SKIP + ... 'step', + ... 'step', + ... 'next', + ... 'next', + ... 'next', + ... 'continue']): + ... test_function() + > (2)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) step + > (3)test_function() + -> for i in test_gen(): + (Pdb) step + --Call-- + > (1)test_gen() + -> def test_gen(): + (Pdb) step + > (2)test_gen() + -> x = yield from test_subgenerator() + (Pdb) next + value 0 + > (3)test_gen() + -> return x + (Pdb) next + Internal StopIteration: 1 + > (3)test_function() + -> for i in test_gen(): + (Pdb) next + > (5)test_function() + -> x = 123 + (Pdb) continue + """ + +def test_pdb_breakpoint_with_throw(): + """GH-132536: PY_THROW event should not be turned off + + >>> def gen(): + ... yield 0 + + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... g = gen() + ... try: + ... g.throw(TypeError) + ... except TypeError: + ... pass + + >>> with PdbTestInput([ + ... 'b 7', + ... 'continue', + ... 'clear 1', + ... 'continue', + ... ]): + ... test_function() + > (2)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) b 7 + Breakpoint 1 at :7 + (Pdb) continue + > (7)test_function() + -> pass + (Pdb) clear 1 + Deleted breakpoint 1 at :7 + (Pdb) continue + """ + +def test_pdb_multiline_statement(): + """Test for multiline statement + + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + + >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +NORMALIZE_WHITESPACE +EXPECTED_FAILURE + ... 'def f(x):', + ... ' return x * 2', + ... '', + ... 'val = 2', + ... 'if val > 0:', + ... ' val = f(val)', + ... '', + ... '', # empty line should repeat the multi-line statement + ... 'val', + ... 'c' + ... ]): + ... test_function() + > (2)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) def f(x): + ... return x * 2 + ... + (Pdb) val = 2 + (Pdb) if val > 0: + ... val = f(val) + ... + (Pdb) + (Pdb) val + 8 + (Pdb) c + """ + +def test_pdb_closure(): + """Test for all expressions/statements that involve closure + + >>> k = 0 + >>> g = 1 + >>> def test_function(): + ... x = 2 + ... g = 3 + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + + >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +NORMALIZE_WHITESPACE +EXPECTED_FAILURE + ... 'k', + ... 'g', + ... 'y = y', + ... 'global g; g', + ... 'global g; (lambda: g)()', + ... '(lambda: x)()', + ... '(lambda: g)()', + ... 'lst = [n for n in range(10) if (n % x) == 0]', + ... 'lst', + ... 'sum(n for n in lst if n > x)', + ... 'x = 1; raise Exception()', + ... 'x', + ... 'def f():', + ... ' return x', + ... '', + ... 'f()', + ... 'c' + ... ]): + ... test_function() + > (4)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) k + 0 + (Pdb) g + 3 + (Pdb) y = y + *** NameError: name 'y' is not defined + (Pdb) global g; g + 1 + (Pdb) global g; (lambda: g)() + 1 + (Pdb) (lambda: x)() + 2 + (Pdb) (lambda: g)() + 3 + (Pdb) lst = [n for n in range(10) if (n % x) == 0] + (Pdb) lst + [0, 2, 4, 6, 8] + (Pdb) sum(n for n in lst if n > x) + 18 + (Pdb) x = 1; raise Exception() + *** Exception + (Pdb) x + 1 + (Pdb) def f(): + ... return x + ... + (Pdb) f() + 1 + (Pdb) c + """ + +def test_pdb_show_attribute_and_item(): + """Test for expressions with command prefix + + >>> def test_function(): + ... n = lambda x: x + ... c = {"a": 1} + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + + >>> with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE + ... 'c["a"]', + ... 'c.get("a")', + ... 'n(1)', + ... 'j=1', + ... 'j+1', + ... 'r"a"', + ... 'next(iter([1]))', + ... 'list((0, 1))', + ... 'c' + ... ]): + ... test_function() + > (4)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) c["a"] + 1 + (Pdb) c.get("a") + 1 + (Pdb) n(1) + 1 + (Pdb) j=1 + (Pdb) j+1 + 2 + (Pdb) r"a" + 'a' + (Pdb) next(iter([1])) + 1 + (Pdb) list((0, 1)) + [0, 1] + (Pdb) c + """ + +# doctest will modify pdb.set_trace during the test, so we need to backup +# the original function to use it in the test +original_pdb_settrace = pdb.set_trace + +def test_pdb_with_inline_breakpoint(): + """Hard-coded breakpoint() calls should invoke the same debugger instance + + >>> def test_function(): + ... x = 1 + ... import pdb; pdb.Pdb().set_trace() + ... original_pdb_settrace() + ... x = 2 + + >>> with PdbTestInput(['display x', + ... 'n', + ... 'n', + ... 'n', + ... 'n', + ... 'undisplay', + ... 'c']): + ... test_function() + > (3)test_function() + -> import pdb; pdb.Pdb().set_trace() + (Pdb) display x + display x: 1 + (Pdb) n + > (4)test_function() + -> original_pdb_settrace() + (Pdb) n + > (4)test_function() + -> original_pdb_settrace() + (Pdb) n + > (5)test_function() + -> x = 2 + (Pdb) n + --Return-- + > (5)test_function()->None + -> x = 2 + display x: 2 [old: 1] + (Pdb) undisplay + (Pdb) c + """ + +def test_pdb_issue_20766(): + """Test for reference leaks when the SIGINT handler is set. + + >>> def test_function(): + ... i = 1 + ... while i <= 2: + ... sess = pdb.Pdb() + ... sess.set_trace(sys._getframe()) + ... print('pdb %d: %s' % (i, sess._previous_sigint_handler)) + ... i += 1 + + >>> with PdbTestInput(['continue', # TODO: RUSTPYTHON # doctest: +SKIP + ... 'continue']): + ... test_function() + > (5)test_function() + -> sess.set_trace(sys._getframe()) + (Pdb) continue + pdb 1: + > (5)test_function() + -> sess.set_trace(sys._getframe()) + (Pdb) continue + pdb 2: + """ + +def test_pdb_issue_43318(): + """echo breakpoints cleared with filename:lineno + + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... print(1) + ... print(2) + ... print(3) + ... print(4) + >>> with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE + ... 'break 3', + ... 'clear :3', + ... 'continue' + ... ]): + ... test_function() + > (2)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) break 3 + Breakpoint 1 at :3 + (Pdb) clear :3 + Deleted breakpoint 1 at :3 + (Pdb) continue + 1 + 2 + 3 + 4 + """ + +def test_pdb_issue_gh_91742(): + """See GH-91742 + + >>> def test_function(): + ... __author__ = "pi" + ... __version__ = "3.14" + ... + ... def about(): + ... '''About''' + ... print(f"Author: {__author__!r}", + ... f"Version: {__version__!r}", + ... sep=" ") + ... + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... about() + + + >>> # TODO: RUSTPYTHON; tried to push value onto stack but overflowed max_stackdepth (remove the skip) + >>> with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE +SKIP + ... 'step', + ... 'step', + ... 'next', + ... 'next', + ... 'jump 5', + ... 'continue' + ... ]): + ... test_function() + > (11)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) step + > (12)test_function() + -> about() + (Pdb) step + --Call-- + > (5)about() + -> def about(): + (Pdb) next + > (7)about() + -> print(f"Author: {__author__!r}", + (Pdb) next + > (8)about() + -> f"Version: {__version__!r}", + (Pdb) jump 5 + > (5)about() + -> def about(): + (Pdb) continue + Author: 'pi' Version: '3.14' + """ + +def test_pdb_issue_gh_94215(): + """See GH-94215 + + Check that frame_setlineno() does not leak references. + + >>> def test_function(): + ... def func(): + ... def inner(v): pass + ... inner( + ... 42 + ... ) + ... + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... func() + + >>> # TODO: RUSTPYTHON; tried to push value onto stack but overflowed max_stackdepth (remove the skip) + >>> with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE +SKIP + ... 'step', + ... 'step', + ... 'next', + ... 'next', + ... 'jump 3', + ... 'next', + ... 'next', + ... 'jump 3', + ... 'next', + ... 'next', + ... 'jump 3', + ... 'continue' + ... ]): + ... test_function() + > (8)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) step + > (9)test_function() + -> func() + (Pdb) step + --Call-- + > (2)func() + -> def func(): + (Pdb) next + > (3)func() + -> def inner(v): pass + (Pdb) next + > (4)func() + -> inner( + (Pdb) jump 3 + > (3)func() + -> def inner(v): pass + (Pdb) next + > (4)func() + -> inner( + (Pdb) next + > (5)func() + -> 42 + (Pdb) jump 3 + > (3)func() + -> def inner(v): pass + (Pdb) next + > (4)func() + -> inner( + (Pdb) next + > (5)func() + -> 42 + (Pdb) jump 3 + > (3)func() + -> def inner(v): pass + (Pdb) continue + """ + +def test_pdb_issue_gh_101673(): + """See GH-101673 + + Make sure ll and switching frames won't revert local variable assignment + + >>> def test_function(): + ... a = 1 + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + + >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +NORMALIZE_WHITESPACE +EXPECTED_FAILURE + ... '!a = 2', + ... 'll', + ... 'p a', + ... 'u', + ... 'p a', + ... 'd', + ... 'p a', + ... 'continue' + ... ]): + ... test_function() + > (3)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) !a = 2 + (Pdb) ll + 1 def test_function(): + 2 a = 1 + 3 -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) p a + 2 + (Pdb) u + > (11)() + -> test_function() + (Pdb) p a + *** NameError: name 'a' is not defined + (Pdb) d + > (3)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) p a + 2 + (Pdb) continue + """ + +def test_pdb_issue_gh_103225(): + """See GH-103225 + + Make sure longlist uses 1-based line numbers in frames that correspond to a module + + >>> with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE + ... 'longlist', + ... 'continue' + ... ]): + ... a = 1 + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... b = 2 + > (6)() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) longlist + 1 with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE + 2 'longlist', + 3 'continue' + 4 ]): + 5 a = 1 + 6 -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + 7 b = 2 + (Pdb) continue + """ + +def test_pdb_issue_gh_101517(): + """See GH-101517 + + Make sure pdb doesn't crash when the exception is caught in a try/except* block + + >>> def test_function(): + ... try: + ... raise KeyError + ... except* Exception as e: + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + + >>> with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE + ... 'continue' + ... ]): + ... test_function() + > (5)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) continue + """ + +def test_pdb_issue_gh_108976(): + """See GH-108976 + Make sure setting f_trace_opcodes = True won't crash pdb + >>> def test_function(): + ... import sys + ... sys._getframe().f_trace_opcodes = True + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... a = 1 + >>> with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE + ... 'continue' + ... ]): + ... test_function() + > (4)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) continue + """ + +def test_pdb_issue_gh_127321(): + """See GH-127321 + breakpoint() should stop at a opcode that has a line number + >>> def test_function(): + ... import pdb; pdb_instance = pdb.Pdb(nosigint=True, readrc=False) + ... [1, 2] and pdb_instance.set_trace() + ... a = 1 + >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +NORMALIZE_WHITESPACE +EXPECTED_FAILURE + ... 'continue' + ... ]): + ... test_function() + > (4)test_function() + -> a = 1 + (Pdb) continue + """ + + +def test_pdb_issue_gh_136057(): + """See GH-136057 + "step" and "next" commands should be able to get over list comprehensions + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... lst = [i for i in range(10)] + ... for i in lst: pass + + >>> with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE + ... 'next', + ... 'next', + ... 'step', + ... 'continue', + ... ]): + ... test_function() + > (2)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) next + > (3)test_function() + -> lst = [i for i in range(10)] + (Pdb) next + > (4)test_function() + -> for i in lst: pass + (Pdb) step + --Return-- + > (4)test_function()->None + -> for i in lst: pass + (Pdb) continue + """ + + +def test_pdb_issue_gh_80731(): + """See GH-80731 + + pdb should correctly print exception info if in an except block. + + >>> with PdbTestInput([ # doctest: +ELLIPSIS + ... 'import sys', + ... 'sys.exc_info()', + ... 'continue' + ... ]): + ... try: + ... raise ValueError('Correct') + ... except ValueError: + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + > (9)() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) import sys + (Pdb) sys.exc_info() + (, ValueError('Correct'), ) + (Pdb) continue + """ + + +def test_pdb_ambiguous_statements(): + """See GH-104301 + + Make sure that ambiguous statements prefixed by '!' are properly disambiguated + + >>> with PdbTestInput([ + ... 's', # step to the print line + ... '! n = 42', # disambiguated statement: reassign the name n + ... 'n', # advance the debugger into the print() + ... 'continue' + ... ]): + ... n = -1 + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... print(f"The value of n is {n}") + > (8)() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) s + > (9)() + -> print(f"The value of n is {n}") + (Pdb) ! n = 42 + (Pdb) n + The value of n is 42 + > (1)() + -> with PdbTestInput([ + (Pdb) continue + """ + +def test_pdb_f_trace_lines(): + """GH-80675 + + pdb should work even if f_trace_lines is set to False on some frames. + + >>> def test_function(): + ... import sys + ... frame = sys._getframe() + ... frame.f_trace_lines = False + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... if frame.f_trace_lines != False: + ... print("f_trace_lines is not reset after continue!") + + >>> with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE + ... 'continue' + ... ]): + ... test_function() + > (5)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) continue + """ + +def test_pdb_frame_refleak(): + """ + pdb should not leak reference to frames + + >>> def frame_leaker(container): + ... import sys + ... container.append(sys._getframe()) + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... pass + + >>> def test_function(): + ... import gc + ... container = [] + ... frame_leaker(container) # c + ... print(len(gc.get_referrers(container[0]))) + ... container = [] + ... frame_leaker(container) # n c + ... print(len(gc.get_referrers(container[0]))) + ... container = [] + ... frame_leaker(container) # r c + ... print(len(gc.get_referrers(container[0]))) + + >>> with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE + ... 'continue', + ... 'next', + ... 'continue', + ... 'return', + ... 'continue', + ... ]): + ... test_function() + > (4)frame_leaker() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) continue + 1 + > (4)frame_leaker() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) next + > (5)frame_leaker() + -> pass + (Pdb) continue + 1 + > (4)frame_leaker() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) return + --Return-- + > (5)frame_leaker()->None + -> pass + (Pdb) continue + 1 + """ + +def test_pdb_function_break(): + """Testing the line number of break on function + + >>> def foo(): pass + + >>> def bar(): + ... + ... pass + + >>> def boo(): + ... # comments + ... global x + ... x = 1 + + >>> def gen(): + ... yield 42 + + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + + >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE +SKIP + ... 'break foo', + ... 'break bar', + ... 'break boo', + ... 'break gen', + ... 'continue' + ... ]): + ... test_function() + > (2)test_function() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) break foo + Breakpoint ... at :1 + (Pdb) break bar + Breakpoint ... at :3 + (Pdb) break boo + Breakpoint ... at :4 + (Pdb) break gen + Breakpoint ... at :2 + (Pdb) continue + """ + +def test_pdb_issue_gh_65052(): + """See GH-65052 + + args, retval and display should not crash if the object is not displayable + >>> class A: + ... def __new__(cls): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... return object.__new__(cls) + ... def __init__(self): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... self.a = 1 + ... def __repr__(self): + ... return self.a + + >>> def test_function(): + ... A() + >>> with PdbTestInput([ # TODO: RUSTPYTHON; Flaky # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE +SKIP + ... 's', + ... 's', + ... 'retval', + ... 'continue', + ... 'args', + ... 'display self', + ... 'display', + ... 'continue', + ... ]): + ... test_function() + > (3)__new__() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) s + > (4)__new__() + -> return object.__new__(cls) + (Pdb) s + --Return-- + > (4)__new__()-> + -> return object.__new__(cls) + (Pdb) retval + *** repr(retval) failed: AttributeError: 'A' object has no attribute 'a' *** + (Pdb) continue + > (6)__init__() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) args + self = *** repr(self) failed: AttributeError: 'A' object has no attribute 'a' *** + (Pdb) display self + display self: *** repr(self) failed: AttributeError: 'A' object has no attribute 'a' *** + (Pdb) display + Currently displaying: + self: *** repr(self) failed: AttributeError: 'A' object has no attribute 'a' *** + (Pdb) continue + """ + + +@support.force_not_colorized_test_class +@support.requires_subprocess() +class PdbTestCase(unittest.TestCase): + def tearDown(self): + os_helper.unlink(os_helper.TESTFN) + + @unittest.skipIf(sys.flags.safe_path, + 'PYTHONSAFEPATH changes default sys.path') + def _run_pdb(self, pdb_args, commands, + expected_returncode=0, + extra_env=None): + self.addCleanup(os_helper.rmtree, '__pycache__') + cmd = [sys.executable, '-m', 'pdb'] + pdb_args + if extra_env is not None: + env = os.environ | extra_env + else: + env = os.environ + with subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + stderr=subprocess.PIPE, + env = {**env, 'PYTHONIOENCODING': 'utf-8'} + ) as proc: + stdout, stderr = proc.communicate(str.encode(commands)) + stdout = bytes.decode(stdout) if isinstance(stdout, bytes) else stdout + stderr = bytes.decode(stderr) if isinstance(stderr, bytes) else stderr + self.assertEqual( + proc.returncode, + expected_returncode, + f"Unexpected return code\nstdout: {stdout}\nstderr: {stderr}" + ) + return stdout, stderr + + def run_pdb_script(self, script, commands, + expected_returncode=0, + extra_env=None, + script_args=None, + pdbrc=None, + remove_home=False): + """Run 'script' lines with pdb and the pdb 'commands'.""" + filename = 'main.py' + with open(filename, 'w') as f: + f.write(textwrap.dedent(script)) + + if pdbrc is not None: + with open('.pdbrc', 'w') as f: + f.write(textwrap.dedent(pdbrc)) + self.addCleanup(os_helper.unlink, '.pdbrc') + self.addCleanup(os_helper.unlink, filename) + + with os_helper.EnvironmentVarGuard() as env: + if remove_home: + env.unset('HOME') + if script_args is None: + script_args = [] + stdout, stderr = self._run_pdb([filename] + script_args, commands, expected_returncode, extra_env) + return stdout, stderr + + def run_pdb_module(self, script, commands): + """Runs the script code as part of a module""" + self.module_name = 't_main' + os_helper.rmtree(self.module_name) + main_file = self.module_name + '/__main__.py' + init_file = self.module_name + '/__init__.py' + os.mkdir(self.module_name) + with open(init_file, 'w') as f: + pass + with open(main_file, 'w') as f: + f.write(textwrap.dedent(script)) + self.addCleanup(os_helper.rmtree, self.module_name) + return self._run_pdb(['-m', self.module_name], commands) + + def _assert_find_function(self, file_content, func_name, expected): + with open(os_helper.TESTFN, 'wb') as f: + f.write(file_content) + + expected = None if not expected else ( + expected[0], os_helper.TESTFN, expected[1]) + self.assertEqual( + expected, pdb.find_function(func_name, os_helper.TESTFN)) + + def _fd_dir_for_pipe_targets(self): + """Return a directory exposing live file descriptors, if any.""" + return self._proc_fd_dir() or self._dev_fd_dir() + + def _proc_fd_dir(self): + """Return /proc-backed fd dir when it can be used for pipes.""" + # GH-142836: Opening /proc/self/fd entries for pipes raises EACCES on + # Solaris, so prefer other mechanisms there. + if sys.platform.startswith("sunos"): + return None + + proc_fd = "/proc/self/fd" + if os.path.isdir(proc_fd) and os.path.exists(os.path.join(proc_fd, '0')): + return proc_fd + return None + + def _dev_fd_dir(self): + """Return /dev-backed fd dir when usable.""" + dev_fd = "/dev/fd" + if os.path.isdir(dev_fd) and os.path.exists(os.path.join(dev_fd, '0')): + if sys.platform.startswith("freebsd"): + try: + if os.stat("/dev").st_dev == os.stat(dev_fd).st_dev: + return None + except FileNotFoundError: + return None + return dev_fd + return None + + def test_find_function_empty_file(self): + self._assert_find_function(b'', 'foo', None) + + def test_find_function_found(self): + self._assert_find_function( + """\ +def foo(): + pass + +def bœr(): + pass + +def quux(): + pass +""".encode(), + 'bœr', + ('bœr', 5), + ) + + def test_find_function_found_with_encoding_cookie(self): + self._assert_find_function( + """\ +# coding: iso-8859-15 +def foo(): + pass + +def bœr(): + pass + +def quux(): + pass +""".encode('iso-8859-15'), + 'bœr', + ('bœr', 6), + ) + + def test_find_function_found_with_bom(self): + self._assert_find_function( + codecs.BOM_UTF8 + """\ +def bœr(): + pass +""".encode(), + 'bœr', + ('bœr', 2), + ) + + def test_spec(self): + # Test that __main__.__spec__ is set to None when running a script + script = """ + import __main__ + print(__main__.__spec__) + """ + + commands = "continue" + + stdout, _ = self.run_pdb_script(script, commands) + self.assertIn('None', stdout) + + def test_script_target_anonymous_pipe(self): + """ + _ScriptTarget doesn't fail on an anonymous pipe. + + GH-142315 + """ + fd_dir = self._fd_dir_for_pipe_targets() + if fd_dir is None: + self.skipTest('anonymous pipe targets require /proc/self/fd or /dev/fd') + + read_fd, write_fd = os.pipe() + + def safe_close(fd): + try: + os.close(fd) + except OSError: + pass + + self.addCleanup(safe_close, read_fd) + self.addCleanup(safe_close, write_fd) + + pipe_path = os.path.join(fd_dir, str(read_fd)) + if not os.path.exists(pipe_path): + self.skipTest('fd directory does not expose anonymous pipes') + + script_source = 'marker = "via_pipe"\n' + os.write(write_fd, script_source.encode('utf-8')) + os.close(write_fd) + + original_path0 = sys.path[0] + self.addCleanup(sys.path.__setitem__, 0, original_path0) + + target = pdb._ScriptTarget(pipe_path) + code_text = target.code + namespace = target.namespace + exec(code_text, namespace) + + self.assertEqual(namespace['marker'], 'via_pipe') + self.assertEqual(namespace['__file__'], target.filename) + self.assertIsNone(namespace['__spec__']) + + def test_find_function_first_executable_line(self): + code = textwrap.dedent("""\ + def foo(): pass + + def bar(): + pass # line 4 + + def baz(): + # comment + pass # line 8 + + def mul(): + # code on multiple lines + code = compile( # line 12 + 'def f()', + '', + 'exec', + ) + """).encode() + + self._assert_find_function(code, 'foo', ('foo', 1)) + self._assert_find_function(code, 'bar', ('bar', 4)) + self._assert_find_function(code, 'baz', ('baz', 8)) + self._assert_find_function(code, 'mul', ('mul', 12)) + + def test_issue7964(self): + # open the file as binary so we can force \r\n newline + with open(os_helper.TESTFN, 'wb') as f: + f.write(b'print("testing my pdb")\r\n') + cmd = [sys.executable, '-m', 'pdb', os_helper.TESTFN] + proc = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + self.addCleanup(proc.stdout.close) + stdout, stderr = proc.communicate(b'quit\n') + self.assertNotIn(b'SyntaxError', stdout, + "Got a syntax error running test script under PDB") + + def test_issue46434(self): + # Temporarily patch in an extra help command which doesn't have a + # docstring to emulate what happens in an embeddable distribution + script = """ + def do_testcmdwithnodocs(self, arg): + pass + + import pdb + pdb.Pdb.do_testcmdwithnodocs = do_testcmdwithnodocs + """ + commands = """ + continue + help testcmdwithnodocs + """ + stdout, stderr = self.run_pdb_script(script, commands) + output = (stdout or '') + (stderr or '') + self.assertNotIn('AttributeError', output, + 'Calling help on a command with no docs should be handled gracefully') + self.assertIn("*** No help for 'testcmdwithnodocs'; __doc__ string missing", output, + 'Calling help on a command with no docs should print an error') + + def test_issue13183(self): + script = """ + from bar import bar + + def foo(): + bar() + + def nope(): + pass + + def foobar(): + foo() + nope() + + foobar() + """ + commands = """ + from bar import bar + break bar + continue + step + step + quit + """ + bar = """ + def bar(): + pass + """ + with open('bar.py', 'w') as f: + f.write(textwrap.dedent(bar)) + self.addCleanup(os_helper.unlink, 'bar.py') + stdout, stderr = self.run_pdb_script(script, commands) + self.assertTrue( + any('main.py(5)foo()->None' in l for l in stdout.splitlines()), + 'Fail to step into the caller after a return') + + def test_issue13120(self): + # Invoking "continue" on a non-main thread triggered an exception + # inside signal.signal. + + with open(os_helper.TESTFN, 'wb') as f: + f.write(textwrap.dedent(""" + import threading + import pdb + + def start_pdb(): + pdb.Pdb(readrc=False).set_trace() + x = 1 + y = 1 + + t = threading.Thread(target=start_pdb) + t.start()""").encode('ascii')) + cmd = [sys.executable, '-u', os_helper.TESTFN] + proc = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + stderr=subprocess.PIPE, + env={**os.environ, 'PYTHONIOENCODING': 'utf-8'} + ) + self.addCleanup(proc.stdout.close) + stdout, stderr = proc.communicate(b'cont\n') + self.assertNotIn(b'Error', stdout, + "Got an error running test script under PDB") + + def test_issue36250(self): + + with open(os_helper.TESTFN, 'wb') as f: + f.write(textwrap.dedent(""" + import threading + import pdb + + evt = threading.Event() + + def start_pdb(): + evt.wait() + pdb.Pdb(readrc=False).set_trace() + + t = threading.Thread(target=start_pdb) + t.start() + pdb.Pdb(readrc=False).set_trace() + evt.set() + t.join()""").encode('ascii')) + cmd = [sys.executable, '-u', os_helper.TESTFN] + proc = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + stderr=subprocess.PIPE, + env = {**os.environ, 'PYTHONIOENCODING': 'utf-8'} + ) + self.addCleanup(proc.stdout.close) + stdout, stderr = proc.communicate(b'cont\ncont\n') + self.assertNotIn(b'Error', stdout, + "Got an error running test script under PDB") + + def test_issue16180(self): + # A syntax error in the debuggee. + script = "def f: pass\n" + commands = '' + expected = "SyntaxError:" + stdout, stderr = self.run_pdb_script( + script, commands + ) + self.assertIn(expected, stderr, + '\n\nExpected:\n{}\nGot:\n{}\n' + 'Fail to handle a syntax error in the debuggee.' + .format(expected, stderr)) + + def test_issue84583(self): + # A syntax error from ast.literal_eval should not make pdb exit. + script = "import ast; ast.literal_eval('')\n" + commands = """ + continue + where + quit + """ + stdout, stderr = self.run_pdb_script(script, commands) + # The code should appear 3 times in the stdout/stderr: + # 1. when pdb starts (stdout) + # 2. when the exception is raised, in trackback (stderr) + # 3. in where command (stdout) + self.assertEqual(stdout.count("ast.literal_eval('')"), 2) + self.assertEqual(stderr.count("ast.literal_eval('')"), 1) + + def test_issue26053(self): + # run command of pdb prompt echoes the correct args + script = "print('hello')" + commands = """ + continue + run a b c + run d e f + quit + """ + stdout, stderr = self.run_pdb_script(script, commands) + res = '\n'.join([x.strip() for x in stdout.splitlines()]) + self.assertRegex(res, "Restarting .* with arguments:\na b c") + self.assertRegex(res, "Restarting .* with arguments:\nd e f") + + def test_issue58956(self): + # Set a breakpoint in a function that already exists on the call stack + # should enable the trace function for the frame. + script = """ + import bar + def foo(): + ret = bar.bar() + pass + foo() + """ + commands = """ + b bar.bar + c + b main.py:5 + c + p ret + quit + """ + bar = """ + def bar(): + return 42 + """ + with open('bar.py', 'w') as f: + f.write(textwrap.dedent(bar)) + self.addCleanup(os_helper.unlink, 'bar.py') + stdout, stderr = self.run_pdb_script(script, commands) + lines = stdout.splitlines() + self.assertIn('-> pass', lines) + self.assertIn('(Pdb) 42', lines) + + def test_step_into_botframe(self): + # gh-125422 + # pdb should not be able to step into the botframe (bdb.py) + script = "x = 1" + commands = """ + step + step + step + quit + """ + stdout, _ = self.run_pdb_script(script, commands) + self.assertIn("The program finished", stdout) + self.assertNotIn("bdb.py", stdout) + + def test_pdbrc_basic(self): + script = textwrap.dedent(""" + a = 1 + b = 2 + """) + + pdbrc = textwrap.dedent(""" + # Comments should be fine + n + p f"{a+8=}" + """) + + stdout, stderr = self.run_pdb_script(script, 'q\n', pdbrc=pdbrc, remove_home=True) + self.assertNotIn("SyntaxError", stdout) + self.assertIn("a+8=9", stdout) + self.assertIn("-> b = 2", stdout) + + def test_pdbrc_empty_line(self): + """Test that empty lines in .pdbrc are ignored.""" + + script = textwrap.dedent(""" + a = 1 + b = 2 + c = 3 + """) + + pdbrc = textwrap.dedent(""" + n + + """) + + stdout, stderr = self.run_pdb_script(script, 'q\n', pdbrc=pdbrc, remove_home=True) + self.assertIn("b = 2", stdout) + self.assertNotIn("c = 3", stdout) + + def test_pdbrc_alias(self): + script = textwrap.dedent(""" + class A: + def __init__(self): + self.attr = 1 + a = A() + b = 2 + """) + + pdbrc = textwrap.dedent(""" + alias pi for k in %1.__dict__.keys(): print(f"%1.{k} = {%1.__dict__[k]}") + until 6 + pi a + """) + + stdout, stderr = self.run_pdb_script(script, 'q\n', pdbrc=pdbrc, remove_home=True) + self.assertIn("a.attr = 1", stdout) + + def test_pdbrc_semicolon(self): + script = textwrap.dedent(""" + class A: + def __init__(self): + self.attr = 1 + a = A() + b = 2 + """) + + pdbrc = textwrap.dedent(""" + b 5;;c;;n + """) + + stdout, stderr = self.run_pdb_script(script, 'q\n', pdbrc=pdbrc, remove_home=True) + self.assertIn("-> b = 2", stdout) + + def test_pdbrc_commands(self): + script = textwrap.dedent(""" + class A: + def __init__(self): + self.attr = 1 + a = A() + b = 2 + """) + + pdbrc = textwrap.dedent(""" + b 6 + commands 1 ;; p a;; end + c + """) + + stdout, stderr = self.run_pdb_script(script, 'q\n', pdbrc=pdbrc, remove_home=True) + self.assertIn("<__main__.A object at", stdout) + + def test_readrc_kwarg(self): + script = textwrap.dedent(""" + print('hello') + """) + + stdout, stderr = self.run_pdb_script(script, 'q\n', pdbrc='invalid', remove_home=True) + self.assertIn("NameError: name 'invalid' is not defined", stdout) + + def test_readrc_homedir(self): + with os_helper.EnvironmentVarGuard() as env: + env.unset("HOME") + with os_helper.temp_dir() as temp_dir, patch("os.path.expanduser"): + rc_path = os.path.join(temp_dir, ".pdbrc") + os.path.expanduser.return_value = rc_path + with open(rc_path, "w") as f: + f.write("invalid") + self.assertEqual(pdb.Pdb().rcLines[0], "invalid") + + def test_header(self): + stdout = StringIO() + header = 'Nobody expects... blah, blah, blah' + with ExitStack() as resources: + resources.enter_context(patch('sys.stdout', stdout)) + # patch pdb.Pdb.set_trace() to avoid entering the debugger + resources.enter_context(patch.object(pdb.Pdb, 'set_trace')) + # We need to manually clear pdb.Pdb._last_pdb_instance so a + # new instance with stdout redirected could be created when + # pdb.set_trace() is called. + pdb.Pdb._last_pdb_instance = None + pdb.set_trace(header=header) + self.assertEqual(stdout.getvalue(), header + '\n') + + def test_run_module(self): + script = """print("SUCCESS")""" + commands = """ + continue + quit + """ + stdout, stderr = self.run_pdb_module(script, commands) + self.assertTrue(any("SUCCESS" in l for l in stdout.splitlines()), stdout) + + def test_module_is_run_as_main(self): + script = """ + if __name__ == '__main__': + print("SUCCESS") + """ + commands = """ + continue + quit + """ + stdout, stderr = self.run_pdb_module(script, commands) + self.assertTrue(any("SUCCESS" in l for l in stdout.splitlines()), stdout) + + def test_run_module_with_args(self): + commands = """ + continue + """ + self._run_pdb(["calendar", "-m"], commands, expected_returncode=1) + + _, stderr = self._run_pdb(["-m", "calendar", "-p", "1"], commands) + self.assertIn("unrecognized arguments: -p", stderr) + + stdout, _ = self._run_pdb(["-m", "calendar", "1"], commands) + self.assertIn("December", stdout) + + stdout, _ = self._run_pdb(["-m", "calendar", "--type", "text"], commands) + self.assertIn("December", stdout) + + def test_run_script_with_args(self): + script = """ + import sys + print(sys.argv[1:]) + """ + commands = """ + continue + quit + """ + + stdout, stderr = self.run_pdb_script(script, commands, script_args=["--bar", "foo"]) + self.assertIn("['--bar', 'foo']", stdout) + + def test_breakpoint(self): + script = """ + if __name__ == '__main__': + pass + print("SUCCESS") + pass + """ + commands = """ + b 3 + quit + """ + stdout, stderr = self.run_pdb_module(script, commands) + self.assertTrue(any("Breakpoint 1 at" in l for l in stdout.splitlines()), stdout) + self.assertTrue(all("SUCCESS" not in l for l in stdout.splitlines()), stdout) + + def test_run_pdb_with_pdb(self): + commands = """ + c + quit + """ + stdout, stderr = self._run_pdb(["-m", "pdb"], commands) + self.assertIn( + pdb._usage, + stdout.replace('\r', '') # remove \r for windows + ) + + def test_module_without_a_main(self): + module_name = 't_main' + os_helper.rmtree(module_name) + init_file = module_name + '/__init__.py' + os.mkdir(module_name) + with open(init_file, 'w'): + pass + self.addCleanup(os_helper.rmtree, module_name) + stdout, stderr = self._run_pdb( + ['-m', module_name], "", expected_returncode=1 + ) + self.assertIn("ImportError: No module named t_main.__main__;", stdout) + + def test_package_without_a_main(self): + pkg_name = 't_pkg' + module_name = 't_main' + os_helper.rmtree(pkg_name) + modpath = pkg_name + '/' + module_name + os.makedirs(modpath) + with open(modpath + '/__init__.py', 'w'): + pass + self.addCleanup(os_helper.rmtree, pkg_name) + stdout, stderr = self._run_pdb( + ['-m', modpath.replace('/', '.')], "", expected_returncode=1 + ) + self.assertIn( + "'t_pkg.t_main' is a package and cannot be directly executed", + stdout) + + def test_nonexistent_module(self): + assert not os.path.exists(os_helper.TESTFN) + stdout, stderr = self._run_pdb(["-m", os_helper.TESTFN], "", expected_returncode=1) + self.assertIn(f"ImportError: No module named {os_helper.TESTFN}", stdout) + + def test_dir_as_script(self): + with os_helper.temp_dir() as temp_dir: + stdout, stderr = self._run_pdb([temp_dir], "", expected_returncode=1) + self.assertIn(f"Error: {temp_dir} is a directory", stdout) + + def test_invalid_cmd_line_options(self): + stdout, stderr = self._run_pdb(["-c"], "", expected_returncode=2) + self.assertIn(f"pdb: error: argument -c/--command: expected one argument", stderr.split('\n')[1]) + stdout, stderr = self._run_pdb(["--spam", "-m", "pdb"], "", expected_returncode=2) + self.assertIn(f"pdb: error: unrecognized arguments: --spam", stderr.split('\n')[1]) + + def test_blocks_at_first_code_line(self): + script = """ + #This is a comment, on line 2 + + print("SUCCESS") + """ + commands = """ + quit + """ + stdout, stderr = self.run_pdb_module(script, commands) + self.assertTrue(any("__main__.py(4)()" + in l for l in stdout.splitlines()), stdout) + + @unittest.expectedFailure # TODO: RUSTPYTHON + def test_file_modified_after_execution(self): + script = """ + print("hello") + """ + + # the time.sleep is needed for low-resolution filesystems like HFS+ + commands = """ + filename = $_frame.f_code.co_filename + f = open(filename, "w") + f.write("print('goodbye')") + import time; time.sleep(1) + f.close() + ll + """ + + stdout, stderr = self.run_pdb_script(script, commands) + self.assertIn("WARNING:", stdout) + self.assertIn("was edited", stdout) + + def test_file_modified_and_immediately_restarted(self): + script = """ + print("hello") + """ + + # the time.sleep is needed for low-resolution filesystems like HFS+ + commands = """ + filename = $_frame.f_code.co_filename + f = open(filename, "w") + f.write("print('goodbye')") + import time; time.sleep(1) + f.close() + restart + """ + + stdout, stderr = self.run_pdb_script(script, commands) + self.assertNotIn("WARNING:", stdout) + self.assertNotIn("was edited", stdout) + + def test_file_modified_after_execution_with_multiple_instances(self): + # the time.sleep is needed for low-resolution filesystems like HFS+ + script = """ + import pdb; pdb.Pdb().set_trace() + with open(__file__, "w") as f: + f.write("print('goodbye')\\n" * 5) + import time; time.sleep(1) + import pdb; pdb.Pdb().set_trace() + """ + + commands = """ + continue + continue + """ + + filename = 'main.py' + with open(filename, 'w') as f: + f.write(textwrap.dedent(script)) + self.addCleanup(os_helper.unlink, filename) + self.addCleanup(os_helper.rmtree, '__pycache__') + cmd = [sys.executable, filename] + with subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + stderr=subprocess.PIPE, + env = {**os.environ, 'PYTHONIOENCODING': 'utf-8'}, + ) as proc: + stdout, _ = proc.communicate(str.encode(commands)) + stdout = stdout and bytes.decode(stdout) + + self.assertEqual(proc.returncode, 0) + self.assertIn("WARNING:", stdout) + self.assertIn("was edited", stdout) + + @unittest.expectedFailure # TODO: RUSTPYTHON + def test_file_modified_after_execution_with_restart(self): + script = """ + import random + # Any code with a source to step into so this script is not checked + # for changes when it's being changed + random.randint(1, 4) + print("hello") + """ + + commands = """ + ll + n + s + filename = $_frame.f_back.f_code.co_filename + def change_file(content, filename): + with open(filename, "w") as f: + f.write(f"print({content})") + + change_file('world', filename) + restart + ll + """ + + stdout, stderr = self.run_pdb_script(script, commands) + # Make sure the code is running correctly and the file is edited + self.assertIn("hello", stdout) + self.assertIn("world", stdout) + # The file was edited, but restart should clear the state and consider + # the file as up to date + self.assertNotIn("WARNING:", stdout) + + def test_post_mortem_restart(self): + script = """ + def foo(): + raise ValueError("foo") + foo() + """ + + commands = """ + continue + restart + continue + quit + """ + + stdout, stderr = self.run_pdb_script(script, commands) + self.assertIn("Restarting", stdout) + + def test_relative_imports(self): + self.module_name = 't_main' + os_helper.rmtree(self.module_name) + main_file = self.module_name + '/__main__.py' + init_file = self.module_name + '/__init__.py' + module_file = self.module_name + '/module.py' + self.addCleanup(os_helper.rmtree, self.module_name) + os.mkdir(self.module_name) + with open(init_file, 'w') as f: + f.write(textwrap.dedent(""" + top_var = "VAR from top" + """)) + with open(main_file, 'w') as f: + f.write(textwrap.dedent(""" + from . import top_var + from .module import var + from . import module + pass # We'll stop here and print the vars + """)) + with open(module_file, 'w') as f: + f.write(textwrap.dedent(""" + var = "VAR from module" + var2 = "second var" + """)) + commands = """ + b 5 + c + p top_var + p var + p module.var2 + quit + """ + stdout, _ = self._run_pdb(['-m', self.module_name], commands) + self.assertTrue(any("VAR from module" in l for l in stdout.splitlines()), stdout) + self.assertTrue(any("VAR from top" in l for l in stdout.splitlines())) + self.assertTrue(any("second var" in l for l in stdout.splitlines())) + + def test_relative_imports_on_plain_module(self): + # Validates running a plain module. See bpo32691 + self.module_name = 't_main' + os_helper.rmtree(self.module_name) + main_file = self.module_name + '/runme.py' + init_file = self.module_name + '/__init__.py' + module_file = self.module_name + '/module.py' + self.addCleanup(os_helper.rmtree, self.module_name) + os.mkdir(self.module_name) + with open(init_file, 'w') as f: + f.write(textwrap.dedent(""" + top_var = "VAR from top" + """)) + with open(main_file, 'w') as f: + f.write(textwrap.dedent(""" + from . import module + pass # We'll stop here and print the vars + """)) + with open(module_file, 'w') as f: + f.write(textwrap.dedent(""" + var = "VAR from module" + """)) + commands = """ + b 3 + c + p module.var + quit + """ + stdout, _ = self._run_pdb(['-m', self.module_name + '.runme'], commands) + self.assertTrue(any("VAR from module" in l for l in stdout.splitlines()), stdout) + + @unittest.expectedFailure # TODO: RUSTPYTHON + def test_errors_in_command(self): + commands = "\n".join([ + 'print(]', + 'debug print(', + 'debug doesnotexist', + 'c', + ]) + stdout, _ = self.run_pdb_script('pass', commands + '\n') + + self.assertEqual(stdout.splitlines()[1:], [ + '-> pass', + "(Pdb) *** SyntaxError: closing parenthesis ']' does not match opening " + "parenthesis '('", + + '(Pdb) ENTERING RECURSIVE DEBUGGER', + '*** SyntaxError: \'(\' was never closed', + 'LEAVING RECURSIVE DEBUGGER', + + '(Pdb) ENTERING RECURSIVE DEBUGGER', + '> (1)()', + "((Pdb)) *** NameError: name 'doesnotexist' is not defined", + 'LEAVING RECURSIVE DEBUGGER', + '(Pdb) ', + ]) + + def test_issue34266(self): + '''do_run handles exceptions from parsing its arg''' + def check(bad_arg, msg): + commands = "\n".join([ + f'run {bad_arg}', + 'q', + ]) + stdout, _ = self.run_pdb_script('pass', commands + '\n') + self.assertEqual(stdout.splitlines()[1:], [ + '-> pass', + f'(Pdb) *** Cannot run {bad_arg}: {msg}', + '(Pdb) ', + ]) + check('\\', 'No escaped character') + check('"', 'No closing quotation') + + def test_issue42384(self): + '''When running `python foo.py` sys.path[0] is an absolute path. `python -m pdb foo.py` should behave the same''' + script = textwrap.dedent(""" + import sys + print('sys.path[0] is', sys.path[0]) + """) + commands = 'c\nq' + + with os_helper.temp_cwd() as cwd: + expected = f'(Pdb) sys.path[0] is {os.path.realpath(cwd)}' + + stdout, stderr = self.run_pdb_script(script, commands) + + self.assertEqual(stdout.split('\n')[2].rstrip('\r'), expected) + + @os_helper.skip_unless_symlink + def test_issue42384_symlink(self): + '''When running `python foo.py` sys.path[0] resolves symlinks. `python -m pdb foo.py` should behave the same''' + script = textwrap.dedent(""" + import sys + print('sys.path[0] is', sys.path[0]) + """) + commands = 'c\nq' + + with os_helper.temp_cwd() as cwd: + cwd = os.path.realpath(cwd) + dir_one = os.path.join(cwd, 'dir_one') + dir_two = os.path.join(cwd, 'dir_two') + expected = f'(Pdb) sys.path[0] is {dir_one}' + + os.mkdir(dir_one) + with open(os.path.join(dir_one, 'foo.py'), 'w') as f: + f.write(script) + os.mkdir(dir_two) + os.symlink(os.path.join(dir_one, 'foo.py'), os.path.join(dir_two, 'foo.py')) + + stdout, stderr = self._run_pdb([os.path.join('dir_two', 'foo.py')], commands) + + self.assertEqual(stdout.split('\n')[2].rstrip('\r'), expected) + + def test_safe_path(self): + """ With safe_path set, pdb should not mangle sys.path[0]""" + + script = textwrap.dedent(""" + import sys + import random + print('sys.path[0] is', sys.path[0]) + """) + commands = 'c\n' + + + with os_helper.temp_cwd() as cwd: + stdout, _ = self.run_pdb_script(script, commands, extra_env={'PYTHONSAFEPATH': '1'}) + + unexpected = f'sys.path[0] is {os.path.realpath(cwd)}' + self.assertNotIn(unexpected, stdout) + + def test_issue42383(self): + with os_helper.temp_cwd() as cwd: + with open('foo.py', 'w') as f: + s = textwrap.dedent(""" + print('The correct file was executed') + + import os + os.chdir("subdir") + """) + f.write(s) + + subdir = os.path.join(cwd, 'subdir') + os.mkdir(subdir) + os.mkdir(os.path.join(subdir, 'subdir')) + wrong_file = os.path.join(subdir, 'foo.py') + + with open(wrong_file, 'w') as f: + f.write('print("The wrong file was executed")') + + stdout, stderr = self._run_pdb(['foo.py'], 'c\nc\nq') + expected = '(Pdb) The correct file was executed' + self.assertEqual(stdout.split('\n')[6].rstrip('\r'), expected) + + def test_gh_94215_crash(self): + script = """\ + def func(): + def inner(v): pass + inner( + 42 + ) + func() + """ + commands = textwrap.dedent(""" + break func + continue + next + next + jump 2 + """) + stdout, stderr = self.run_pdb_script(script, commands) + self.assertFalse(stderr) + + def test_gh_93696_frozen_list(self): + frozen_src = """ + def func(): + x = "Sentinel string for gh-93696" + print(x) + """ + host_program = """ + import os + import sys + + def _create_fake_frozen_module(): + with open('gh93696.py') as f: + src = f.read() + + # this function has a co_filename as if it were in a frozen module + dummy_mod = compile(src, "", "exec") + func_code = dummy_mod.co_consts[0] + + mod = type(sys)("gh93696") + mod.func = type(lambda: None)(func_code, mod.__dict__) + mod.__file__ = 'gh93696.py' + + return mod + + mod = _create_fake_frozen_module() + mod.func() + """ + commands_list = """ + break 20 + continue + step + break 4 + list + quit + """ + commands_longlist = """ + break 20 + continue + step + break 4 + longlist + quit + """ + with open('gh93696.py', 'w') as f: + f.write(textwrap.dedent(frozen_src)) + + with open('gh93696_host.py', 'w') as f: + f.write(textwrap.dedent(host_program)) + + self.addCleanup(os_helper.unlink, 'gh93696.py') + self.addCleanup(os_helper.unlink, 'gh93696_host.py') + + # verify that pdb found the source of the "frozen" function and it + # shows the breakpoint at the correct line for both list and longlist + for commands in (commands_list, commands_longlist): + stdout, _ = self._run_pdb(["gh93696_host.py"], commands) + self.assertIn('x = "Sentinel string for gh-93696"', stdout, "Sentinel statement not found") + self.assertIn('4 B', stdout, "breakpoint not found") + self.assertIn('-> def func():', stdout, "stack entry not found") + + @unittest.expectedFailure # TODO: RUSTPYTHON + def test_empty_file(self): + script = '' + commands = 'q\n' + # We check that pdb stopped at line 0, but anything reasonable + # is acceptable here, as long as it does not halt + stdout, _ = self.run_pdb_script(script, commands) + self.assertIn('main.py(0)', stdout) + stdout, _ = self.run_pdb_module(script, commands) + self.assertIn('__main__.py(0)', stdout) + + def test_non_utf8_encoding(self): + script_dir = os.path.join(os.path.dirname(__file__), 'encoded_modules') + for filename in os.listdir(script_dir): + if filename.endswith(".py"): + self._run_pdb([os.path.join(script_dir, filename)], 'q') + + def test_zipapp(self): + with os_helper.temp_dir() as temp_dir: + os.mkdir(os.path.join(temp_dir, 'source')) + script = textwrap.dedent( + """ + def f(x): + return x + 1 + f(21 + 21) + """ + ) + with open(os.path.join(temp_dir, 'source', '__main__.py'), 'w') as f: + f.write(script) + zipapp.create_archive(os.path.join(temp_dir, 'source'), + os.path.join(temp_dir, 'zipapp.pyz')) + stdout, _ = self._run_pdb([os.path.join(temp_dir, 'zipapp.pyz')], '\n'.join([ + 'b f', + 'c', + 'p x', + 'q' + ])) + self.assertIn('42', stdout) + self.assertIn('return x + 1', stdout) + + @unittest.expectedFailure # TODO: RUSTPYTHON + def test_zipimport(self): + with os_helper.temp_dir() as temp_dir: + os.mkdir(os.path.join(temp_dir, 'source')) + zipmodule = textwrap.dedent( + """ + def bar(): + pass + """ + ) + script = textwrap.dedent( + f""" + import sys; sys.path.insert(0, {repr(os.path.join(temp_dir, 'zipmodule.zip'))}) + import foo + foo.bar() + """ + ) + + with zipfile.ZipFile(os.path.join(temp_dir, 'zipmodule.zip'), 'w') as zf: + zf.writestr('foo.py', zipmodule) + with open(os.path.join(temp_dir, 'script.py'), 'w') as f: + f.write(script) + + stdout, _ = self._run_pdb([os.path.join(temp_dir, 'script.py')], '\n'.join([ + 'n', + 'n', + 'b foo.bar', + 'c', + 'p f"break in {$_frame.f_code.co_name}"', + 'q' + ])) + self.assertIn('break in bar', stdout) + + def test_end_of_options_separator(self): + # gh-148615: Test parsing when '--' separator is used + script = "import sys; print(f'ARGS: {sys.argv[1:]}')" + with open(os_helper.TESTFN, 'w', encoding='utf-8') as f: + f.write(script) + stdout, _ = self._run_pdb(['--', os_helper.TESTFN, '-foo'], 'c\nq') + self.assertIn("ARGS: ['-foo']", stdout) + stdout, _ = self._run_pdb(['-c', 'continue', '--', os_helper.TESTFN, '-c', 'foo'], 'q') + self.assertIn("ARGS: ['-c', 'foo']", stdout) + stdout, stderr = self._run_pdb(['--'], 'q', expected_returncode=2) + self.assertIn("missing script or module to run", stderr) + stdout, stderr = self._run_pdb(['-x', '--', os_helper.TESTFN], 'q', expected_returncode=2) + self.assertIn("unrecognized arguments: -x", stderr) + stdout, _ = self._run_pdb([os_helper.TESTFN, '--', 'arg'], 'c\nq') + self.assertIn("ARGS: ['--', 'arg']", stdout) + with os_helper.temp_cwd(): + with open('mymod.py', 'w', encoding='utf-8') as f: + f.write(script) + stdout, _ = self._run_pdb(['-m', 'mymod', '--', 'arg'], 'c\nq') + self.assertIn("ARGS: ['--', 'arg']", stdout) + + def test_issue_59000(self): + script = """ + def foo(): + pass + + class C: + def foo(self): + pass + """ + commands = """ + break C.foo + quit + """ + stdout, stderr = self.run_pdb_script(script, commands) + self.assertIn("The specified object 'C.foo' is not a function", stdout) + + +class ChecklineTests(unittest.TestCase): + def setUp(self): + linecache.clearcache() # Pdb.checkline() uses linecache.getline() + + def tearDown(self): + os_helper.unlink(os_helper.TESTFN) + + def test_checkline_before_debugging(self): + with open(os_helper.TESTFN, "w") as f: + f.write("print(123)") + db = pdb.Pdb() + self.assertEqual(db.checkline(os_helper.TESTFN, 1), 1) + + def test_checkline_after_reset(self): + with open(os_helper.TESTFN, "w") as f: + f.write("print(123)") + db = pdb.Pdb() + db.reset() + self.assertEqual(db.checkline(os_helper.TESTFN, 1), 1) + + def test_checkline_is_not_executable(self): + # Test for comments, docstrings and empty lines + s = textwrap.dedent(""" + # Comment + \"\"\" docstring \"\"\" + ''' docstring ''' + + """) + with open(os_helper.TESTFN, "w") as f: + f.write(s) + num_lines = len(s.splitlines()) + 2 # Test for EOF + with redirect_stdout(StringIO()): + db = pdb.Pdb() + for lineno in range(num_lines): + self.assertFalse(db.checkline(os_helper.TESTFN, lineno)) + + +@support.requires_subprocess() +class PdbTestInline(unittest.TestCase): + @unittest.skipIf(sys.flags.safe_path, + 'PYTHONSAFEPATH changes default sys.path') + def _run_script(self, script, commands, + expected_returncode=0, + extra_env=None): + self.addCleanup(os_helper.rmtree, '__pycache__') + filename = 'main.py' + with open(filename, 'w') as f: + f.write(textwrap.dedent(script)) + self.addCleanup(os_helper.unlink, filename) + + commands = textwrap.dedent(commands) + + cmd = [sys.executable, 'main.py'] + if extra_env is not None: + env = os.environ | extra_env + else: + env = os.environ + with subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + stderr=subprocess.PIPE, + env = {**env, 'PYTHONIOENCODING': 'utf-8'} + ) as proc: + stdout, stderr = proc.communicate(str.encode(commands)) + stdout = bytes.decode(stdout) if isinstance(stdout, bytes) else stdout + stderr = bytes.decode(stderr) if isinstance(stderr, bytes) else stderr + self.assertEqual( + proc.returncode, + expected_returncode, + f"Unexpected return code\nstdout: {stdout}\nstderr: {stderr}" + ) + return stdout, stderr + + @unittest.expectedFailure # TODO: RUSTPYTHON + def test_quit(self): + script = """ + x = 1 + breakpoint() + """ + + commands = """ + quit + n + p x + 1 + quit + y + """ + + stdout, stderr = self._run_script(script, commands, expected_returncode=1) + self.assertIn("2", stdout) + self.assertIn("Quit anyway", stdout) + # Closing stdin will quit the debugger anyway so we need to confirm + # it's the quit command that does the job + # call/return event will print --Call-- and --Return-- + self.assertNotIn("--", stdout) + # Normal exit should not print anything to stderr + self.assertEqual(stderr, "") + # The quit prompt should be printed exactly twice + self.assertEqual(stdout.count("Quit anyway"), 2) + + @unittest.expectedFailure # TODO: RUSTPYTHON + def test_quit_after_interact(self): + """ + interact command will set sys.ps1 temporarily, we need to make sure + that it's restored and pdb does not believe it's in interactive mode + after interact is done. + """ + script = """ + x = 1 + breakpoint() + """ + + commands = """ + interact + quit() + q + y + """ + + stdout, stderr = self._run_script(script, commands, expected_returncode=1) + # Normal exit should not print anything to stderr + self.assertEqual(stderr, "") + # The quit prompt should be printed exactly once + self.assertEqual(stdout.count("Quit anyway"), 1) + # BdbQuit should not be printed + self.assertNotIn("BdbQuit", stdout) + + def test_set_trace_with_skip(self): + """GH-82897 + Inline set_trace() should break unconditionally. This example is a + bit oversimplified, but as `pdb.set_trace()` uses the previous Pdb + instance, it's possible that we had a previous pdb instance with + skip values when we use `pdb.set_trace()` - it would be confusing + to users when such inline breakpoints won't break immediately. + """ + script = textwrap.dedent(""" + import pdb + def foo(): + x = 40 + 2 + pdb.Pdb(skip=['__main__']).set_trace() + foo() + """) + commands = """ + p x + c + """ + stdout, _ = self._run_script(script, commands) + self.assertIn("42", stdout) + + def test_readline_not_imported(self): + """GH-138860 + Directly or indirectly importing readline might deadlock a subprocess + if it's launched with process_group=0 or preexec_fn=setpgrp + + It's also a pattern that readline is never imported with just import pdb. + + This test is to ensure that readline is not imported for import pdb. + It's possible that we have a good reason to do that in the future. + """ + + script = textwrap.dedent(""" + import sys + import pdb + if "readline" in sys.modules: + print("readline imported") + """) + commands = "" + stdout, stderr = self._run_script(script, commands) + self.assertNotIn("readline imported", stdout) + self.assertEqual(stderr, "") + + +@support.force_colorized_test_class +class PdbTestColorize(unittest.TestCase): + def setUp(self): + self._original_can_colorize = _colorize.can_colorize + # Force colorize to be enabled because we are sending data + # to a StringIO + _colorize.can_colorize = lambda *args, **kwargs: True + + def tearDown(self): + _colorize.can_colorize = self._original_can_colorize + + def test_code_display(self): + output = io.StringIO() + p = pdb.Pdb(stdout=output, colorize=True) + p.set_trace(commands=['ll', 'c']) + self.assertIn("\x1b", output.getvalue()) + + output = io.StringIO() + p = pdb.Pdb(stdout=output, colorize=False) + p.set_trace(commands=['ll', 'c']) + self.assertNotIn("\x1b", output.getvalue()) + + output = io.StringIO() + p = pdb.Pdb(stdout=output) + p.set_trace(commands=['ll', 'c']) + self.assertNotIn("\x1b", output.getvalue()) + + def test_stack_entry(self): + output = io.StringIO() + p = pdb.Pdb(stdout=output, colorize=True) + p.set_trace(commands=['w', 'c']) + self.assertIn("\x1b", output.getvalue()) + + +@support.force_not_colorized_test_class +@support.requires_subprocess() +class TestREPLSession(unittest.TestCase): + @unittest.expectedFailureIf(sys.platform == "android", "TODO: RUSTPYTHON") + def test_return_from_inline_mode_to_REPL(self): + # GH-124703: Raise BdbQuit when exiting pdb in REPL session. + # This allows the REPL session to continue. + from test.test_repl import spawn_repl + p = spawn_repl() + user_input = """ + x = 'Spam' + import pdb + pdb.set_trace(commands=['x + "During"', 'q']) + x + 'After' + """ + p.stdin.write(textwrap.dedent(user_input)) + output = kill_python(p) + self.assertIn('SpamDuring', output) + self.assertNotIn("Quit anyway", output) + self.assertIn('BdbQuit', output) + self.assertIn('SpamAfter', output) + self.assertEqual(p.returncode, 0) + + +@support.force_not_colorized_test_class +@support.requires_subprocess() +class PdbTestReadline(unittest.TestCase): + + @classmethod + def setUpClass(cls): + # Ensure that the readline module is loaded + # If this fails, the test is skipped because SkipTest will be raised + readline = import_module('readline') + if readline.backend == "editline": + raise unittest.SkipTest("libedit readline is not supported for pdb") + + def test_basic_completion(self): + script = textwrap.dedent(""" + import pdb; pdb.Pdb().set_trace() + # Concatenate strings so that the output doesn't appear in the source + print('hello' + '!') + """) + + # List everything starting with 'co', there should be multiple matches + # then add ntin and complete 'contin' to 'continue' + input = b"co\t\tntin\t\n" + + output = run_pty(script, input) + + self.assertIn(b'commands', output) + self.assertIn(b'condition', output) + self.assertIn(b'continue', output) + self.assertIn(b'hello!', output) + + def test_expression_completion(self): + script = textwrap.dedent(""" + value = "speci" + import pdb; pdb.Pdb().set_trace() + """) + + # Complete: value + 'al' + input = b"val\t + 'al'\n" + # Complete: p value + 'es' + input += b"p val\t + 'es'\n" + # Complete: $_frame + input += b"$_fra\t\n" + # Continue + input += b"c\n" + + output = run_pty(script, input) + + self.assertIn(b'special', output) + self.assertIn(b'species', output) + self.assertIn(b'$_frame', output) + + def test_builtin_completion(self): + script = textwrap.dedent(""" + value = "speci" + import pdb; pdb.Pdb().set_trace() + """) + + # Complete: print(value + 'al') + input = b"pri\tval\t + 'al')\n" + + # Continue + input += b"c\n" + + output = run_pty(script, input) + + self.assertIn(b'special', output) + + def test_convvar_completion(self): + script = textwrap.dedent(""" + import pdb; pdb.Pdb().set_trace() + """) + + # Complete: $_frame + input = b"$_fram\t\n" + + # Complete: $_frame.f_lineno + 100 + input += b"$_frame.f_line\t + 100\n" + + # Continue + input += b"c\n" + + output = run_pty(script, input) + + self.assertIn(b' 0:\n" + input += b"x += 1\n" + input += b"return x\n" + # We need to do backspaces to remove the auto-indentation + input += b"\x08\x08\x08\x08else:\n" + input += b"return -x\n" + input += b"\n" + input += b"f(-21-21)\n" + input += b"c\n" + + output = run_pty(script, input) + + self.assertIn(b'42', output) + + def test_multiline_completion(self): + script = textwrap.dedent(""" + import pdb; pdb.Pdb().set_trace() + """) + + input = b"def func():\n" + # Auto-indent + # Complete: return 40 + 2 + input += b"ret\t 40 + 2\n" + input += b"\n" + # Complete: func() + input += b"fun\t()\n" + input += b"c\n" + + output = run_pty(script, input) + + self.assertIn(b'42', output) + + @unittest.skipIf(sys.platform.startswith('freebsd'), + '\\x08 is not interpreted as backspace on FreeBSD') + def test_multiline_indent_completion(self): + script = textwrap.dedent(""" + import pdb; pdb.Pdb().set_trace() + """) + + # \t should always complete a 4-space indent + # This piece of code will raise an IndentationError or a SyntaxError + # if the completion is not working as expected + input = textwrap.dedent("""\ + def func(): + a = 1 + \x08\ta += 1 + \x08\x08\ta += 1 + \x08\x08\x08\ta += 1 + \x08\x08\x08\x08\tif a > 0: + a += 1 + \x08\x08\x08\x08return a + + func() + c + """).encode() + + output = run_pty(script, input) + + self.assertIn(b'5', output) + self.assertNotIn(b'Error', output) + + def test_interact_completion(self): + script = textwrap.dedent(""" + value = "speci" + import pdb; pdb.Pdb().set_trace() + """) + + # Enter interact mode + input = b"interact\n" + # Should fail to complete 'display' because that's a pdb command + input += b"disp\t\n" + # 'value' should still work + input += b"val\t + 'al'\n" + # Let's define a function to test + input += b"def f():\n" + input += b"\treturn 42\n" + input += b"\n" + input += b"f() * 2\n" + # Exit interact mode + input += b"exit()\n" + # continue + input += b"c\n" + + output = run_pty(script, input) + + self.assertIn(b"'disp' is not defined", output) + self.assertIn(b'special', output) + self.assertIn(b'84', output) + + +def load_tests(loader, tests, pattern): + from test import test_pdb + + def setUpPdbBackend(backend): + def setUp(test): + import pdb + pdb.set_default_backend(backend) + return setUp + + def tearDown(test): + # Ensure that asyncio state has been cleared at the end of the test. + # This prevents a "test altered the execution environment" warning if + # asyncio features are used. + _set_event_loop_policy(None) + + # A doctest of pdb could have residues. For example, pdb could still + # be running, or breakpoints might be left uncleared. These residues + # could potentially interfere with the following test, especially + # when we switch backends. Here we clear all the residues to restore + # to its pre-test state. + + # clear all the breakpoints left + import bdb + bdb.Breakpoint.clearBreakpoints() + + # Stop tracing and clear the pdb instance cache + if pdb.Pdb._last_pdb_instance: + pdb.Pdb._last_pdb_instance.stop_trace() + pdb.Pdb._last_pdb_instance = None + + # If garbage objects are collected right after we start tracing, we + # could stop at __del__ of the object which would fail the test. + gc.collect() + + from test.support.rustpython import DocTestChecker # TODO: RUSTPYTHON + tests.addTest( + doctest.DocTestSuite( + test_pdb, + setUp=setUpPdbBackend('monitoring'), + tearDown=tearDown, + checker=DocTestChecker(), # TODO: RUSTPYTHON + ) + ) + tests.addTest( + doctest.DocTestSuite( + test_pdb, + setUp=setUpPdbBackend('settrace'), + tearDown=tearDown, + checker=DocTestChecker(), # TODO: RUSTPYTHON + ) + ) + return tests + + +if __name__ == '__main__': + unittest.main()