import os
import io
import re
import sys
import cmd
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):
pass
__all__ = ["run", "pm", "Pdb", "runeval", "runctx", "runcall", "set_trace",
"post_mortem", "set_default_backend", "get_default_backend", "help"]
def find_first_executable_line(code):
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*\[.+\])?\s*[(]' % re.escape(funcname))
try:
fp = tokenize.open(filename)
except OSError:
lines = linecache.getlines(filename)
if not lines:
return None
fp = io.StringIO(''.join(lines))
funcdef = ""
funcstart = 0
with fp:
for lineno, line in enumerate(fp, start=1):
if cre.match(line):
funcstart, funcdef = lineno, line
elif funcdef:
funcdef += line
if funcdef:
try:
code = compile(funcdef, filename, 'exec')
except SyntaxError:
continue
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))
linestarts.reverse()
for i, lineno in linestarts:
if lasti >= i:
return lineno
return 0
class _rstr(str):
def __repr__(self):
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 not sys.flags.safe_path:
sys.path[0] = os.path.dirname(self._target)
@staticmethod
def _check(target):
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):
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):
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='')
line_prefix = '\n-> '
_default_backend = 'settrace'
def set_default_backend(backend):
global _default_backend
if backend not in ('settrace', 'monitoring'):
raise ValueError("Invalid backend: %s" % backend)
_default_backend = backend
def get_default_backend():
return _default_backend
class Pdb(bdb.Bdb, cmd.Cmd):
_previous_sigint_handler = None
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, 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:
self.use_rawinput = 0
self.prompt = '(Pdb) '
self.aliases = {}
self.displaying = {}
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:
import readline
readline.set_completer_delims(' \t\n`@#%^&*()=+[{]}\\|;:\'",<>?')
except ImportError:
pass
self.allow_kbdint = False
self.nosigint = nosigint
self.identchars = cmd.Cmd.identchars + '=.[](),"\'+-*/%@&|<>~^'
self.rcLines = []
if readrc:
try:
with open(os.path.expanduser('~/.pdbrc'), encoding='utf-8') as rcFile:
self.rcLines.extend(rcFile)
except OSError:
pass
try:
with open(".pdbrc", encoding='utf-8') as rcFile:
self.rcLines.extend(rcFile)
except OSError:
pass
self.commands = {} self.commands_defining = False self.commands_bnum = None
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
if commands is not None:
self.rcLines.extend(commands)
super().set_trace(frame)
async def set_trace_async(self, frame=None, *, commands=None):
if self.async_awaitable is not None:
return
if frame is None:
frame = sys._getframe().f_back
self.set_trace(frame, commands=commands)
self.stopframe = frame
self.stop_trace()
self.async_shim_frame = sys._getframe()
self.async_awaitable = None
while True:
self.async_awaitable = None
self.trace_dispatch(frame, "opcode", None)
if self.async_awaitable is not None:
try:
if self.breaks:
with self.set_enterframe(frame):
self.set_continue()
self.start_trace()
await self.async_awaitable
except Exception:
self._error_exc()
else:
break
self.async_shim_frame = None
if self.returnframe is None and self.stoplineno == -1 and not self.breaks:
return
self.start_trace()
def sigint_handler(self, signum, frame):
if self.allow_kbdint:
raise KeyboardInterrupt
self.message("\nProgram interrupted. (Use 'cont' to resume).")
self.set_step()
self.set_trace(frame)
def reset(self):
bdb.Bdb.reset(self)
self.forget()
def forget(self):
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()
def setup(self, f, tb):
self.forget()
self.stack, self.curindex = self.get_stack(f, tb)
while tb:
lineno = lasti2lineno(tb.tb_frame.f_code, tb.tb_lasti)
self.tb_lineno[tb.tb_frame] = lineno
tb = tb.tb_next
self.curframe = self.stack[self.curindex][0]
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)
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
def user_call(self, frame, argument_list):
if self._wait_for_mainpyfile:
return
if self.stop_here(frame):
self.message('--Call--')
self.interaction(frame, None)
def user_line(self, frame):
if self._wait_for_mainpyfile:
if (self.mainpyfile != self.canonic(frame.f_code.co_filename)):
return
self._wait_for_mainpyfile = False
if self.trace_opcodes:
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):
if getattr(self, "currentbp", False) and \
self.currentbp in self.commands:
currentbp = self.currentbp
self.currentbp = 0
for line in self.commands[currentbp]:
self.cmdqueue.append(line)
self.cmdqueue.append(f'_pdbcmd_restore_lastcmd {self.lastcmd}')
def user_return(self, frame, return_value):
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)
def user_exception(self, frame, exc_info):
if self._wait_for_mainpyfile:
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)
prefix = 'Internal ' if (not exc_traceback
and exc_type is StopIteration) else ''
self.message('%s%s' % (prefix, self._format_exc(exc_value)))
self.interaction(frame, exc_traceback)
def _cmdloop(self):
while True:
try:
self.allow_kbdint = True
self.cmdloop()
self.allow_kbdint = False
break
except KeyboardInterrupt:
self.message('--KeyboardInterrupt--')
def _save_initial_file_mtime(self, frame):
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):
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
def _show_display(self):
displaying = self.displaying.get(self.curframe)
if displaying:
for expr, oldvalue in displaying.items():
newvalue = self._getval_except(expr)
if newvalue is not oldvalue and newvalue != oldvalue:
displaying[expr] = newvalue
self.message('display %s: %s [old: %s]' %
(expr, self._safe_repr(newvalue, expr),
self._safe_repr(oldvalue, expr)))
def _get_tb_and_exceptions(self, tb_or_exc):
_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):
try:
self._chained_exceptions = exceptions
self._chained_exception_index = len(exceptions) - 1
yield
finally:
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):
if Pdb._previous_sigint_handler:
try:
signal.signal(signal.SIGINT, Pdb._previous_sigint_handler)
except ValueError: pass
else:
Pdb._previous_sigint_handler = None
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)
self.cmdqueue.append('_pdbcmd_print_frame_status')
self._cmdloop()
if self.cmdqueue and self.cmdqueue[-1] == '_pdbcmd_print_frame_status':
self.cmdqueue.pop()
self.forget()
def displayhook(self, obj):
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():
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):
code = compile(source, "<string>", "exec")
if not any(isinstance(const, CodeType) for const in code.co_consts):
return False
locals_copy = dict(locals)
locals_copy["__pdb_eval__"] = {
"result": None,
"write_back": {}
}
try:
compile(source, "<string>", "eval")
except SyntaxError:
pass
else:
source = "__pdb_eval__['result'] = " + source
source = ("try:\n" +
textwrap.indent(source, " ") + "\n" +
"finally:\n" +
" __pdb_eval__['write_back'] = locals()")
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__"
)
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
pdb_eval = locals_copy["__pdb_eval__"]
pdb_eval["write_back"].pop("__pdb_eval__")
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):
source_async = (
"async def __pdb_await():\n" +
textwrap.indent(source, " ") + '\n' +
" __pdb_locals.update(locals())"
)
ns = globals | locals
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', '<stdin>', 'single')) is None:
with self._enable_multiline_input():
buffer = line
continue_prompt = "... "
while (code := codeop.compile_command(buffer, '<stdin>', '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():
buffer += '\n'
else:
buffer += '\n' + line
self.lastcmd = buffer
except SyntaxError as e:
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:].strip()
locals = self.curframe.f_locals
globals = self.curframe.f_globals
try:
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
try:
sys.stdin = self.stdin
sys.stdout = self.stdout
sys.displayhook = self.displayhook
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:
self._error_exc()
def _replace_convenience_variables(self, line):
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:
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):
if not line.strip():
return line
args = line.split()
while args[0] in self.aliases:
line = self.aliases[args[0]]
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]}'")
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]}'")
return "!"
break
line = line.replace("%*", ' '.join(args[1:]))
args = line.split()
if args[0] != 'alias':
marker = line.find(';;')
if marker >= 0:
next = line[marker+2:].lstrip()
self.cmdqueue.insert(0, next)
line = line[:marker].rstrip()
line = self._replace_convenience_variables(line)
return line
def onecmd(self, line):
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)
def handle_command_def(self, line):
cmd, arg, line = self.parseline(line)
if not cmd:
return False
if cmd == 'end':
return True elif cmd == 'EOF':
self.message('')
return True cmdlist = self.commands[self.commands_bnum]
if cmd == 'silent':
cmdlist.append('_pdbcmd_silence_frame_status')
return False if arg:
cmdlist.append(cmd+' '+arg)
else:
cmdlist.append(cmd)
try:
func = getattr(self, 'do_' + cmd)
except AttributeError:
func = self.default
if func.__name__ in self.commands_resuming:
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
def message(self, msg, end='\n'):
print(msg, end=end, file=self.stdout)
def error(self, msg):
print('***', msg, file=self.stdout)
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
@property
def rlcompleter(self):
if not hasattr(self, "_rlcompleter"):
try:
import readline
except ImportError:
from rlcompleter import Completer
self._rlcompleter = Completer
else:
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):
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):
if line.strip().endswith((':', ',')):
return []
try:
ret = self._complete_expression(text, line, begidx, endidx)
except Exception:
ret = []
globs = glob.glob(glob.escape(text) + '*')
for fn in globs:
if os.path.isdir(fn):
ret.append(fn + '/')
elif os.path.isfile(fn) and fn.lower().endswith(('.py', '.pyw')):
ret.append(fn + ':')
return ret
def _complete_bpnumber(self, text, line, begidx, endidx):
return [str(i) for i, bp in enumerate(bdb.Breakpoint.bpbynumber)
if bp is not None and str(i).startswith(text)]
def _complete_expression(self, text, line, begidx, endidx):
if not self.curframe:
return []
ns = {**self.curframe.f_globals, **self.curframe.f_locals}
if '.' in text:
dotted = text.split('.')
try:
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):
return []
prefix = '.'.join(dotted[:-1]) + '.'
return [prefix + n for n in dir(obj) if n.startswith(dotted[-1])]
else:
if text.startswith("$"):
conv_vars = self.curframe.f_globals.get('__pdb_convenience_variables', {})
return [f"${name}" for name in conv_vars if name.startswith(text[1:])]
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 []
return [' ' * (4 - readline.get_begidx() % 4)]
def complete_multiline_names(self, text, line, begidx, endidx):
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("$"):
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)
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
def do_commands(self, arg):
if not arg:
bnum = len(bdb.Breakpoint.bpbynumber) - 1
else:
try:
bnum = int(arg)
except:
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
if bnum in self.commands:
old_commands = self.commands[bnum]
else:
old_commands = None
self.commands[bnum] = []
prompt_back = self.prompt
self.prompt = '(com) '
self.commands_defining = True
try:
self.cmdloop()
except KeyboardInterrupt:
if old_commands:
self.commands[bnum] = old_commands
else:
del self.commands[bnum]
self.error('command definition aborted, old commands restored')
finally:
self.commands_defining = False
self.prompt = prompt_back
complete_commands = _complete_bpnumber
def do_break(self, arg, temporary=False):
if not arg:
if self.breaks: self.message("Num Type Disp Enb Where")
for bp in bdb.Breakpoint.bpbynumber:
if bp:
self.message(bp.bpformat())
return
filename = None
lineno = None
cond = None
module_globals = None
comma = arg.find(',')
if comma > 0:
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()
colon = arg.rfind(':')
funcname = None
if colon >= 0:
filename = arg[:colon].rstrip()
f = self.lookupmodule(filename)
if not f:
self.error('%r not found from sys.path' % filename)
return
else:
filename = f
arg = arg[colon+1:].lstrip()
try:
lineno = int(arg)
except ValueError:
self.error('Bad lineno: %s' % arg)
return
else:
try:
lineno = int(arg)
except ValueError:
try:
func = eval(arg,
self.curframe.f_globals,
self.curframe.f_locals)
except:
func = arg
try:
if hasattr(func, '__func__'):
func = func.__func__
code = func.__code__
funcname = code.co_name
lineno = find_first_executable_line(code)
filename = code.co_filename
module_globals = func.__globals__
except:
(ok, filename, ln) = self.lineinfo(arg)
if not ok:
self.error('The specified object %r is not a function '
'or was not found along sys.path.' % arg)
return
funcname = ok lineno = int(ln)
if not filename:
filename = self.defaultFile()
filename = self.canonic(filename)
line = self.checkline(filename, lineno, module_globals)
if line:
err = self.set_break(filename, line, temporary, cond, funcname)
if err:
self.error(err)
else:
bp = self.get_breaks(filename, line)[-1]
self.message("Breakpoint %d at %s:%d" %
(bp.number, bp.file, bp.line))
def defaultFile(self):
filename = self.curframe.f_code.co_filename
if filename == '<string>' and self.mainpyfile:
filename = self.mainpyfile
return filename
do_b = do_break
complete_break = _complete_location
complete_b = _complete_location
def do_tbreak(self, arg):
self.do_break(arg, True)
complete_tbreak = _complete_location
def lineinfo(self, identifier):
failed = (None, None, None)
idstring = identifier.split("'")
if len(idstring) == 1:
id = idstring[0].strip()
elif len(idstring) == 3:
id = idstring[1].strip()
else:
return failed
if id == '': return failed
parts = id.split('.')
if parts[0] == 'self':
del parts[0]
if len(parts) == 0:
return failed
fname = self.defaultFile()
if len(parts) == 1:
item = parts[0]
else:
f = self.lookupmodule(parts[0])
if f:
fname = f
item = parts[1]
else:
return failed
answer = find_function(item, self.canonic(fname))
return answer or failed
def checkline(self, filename, lineno, module_globals=None):
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
line = line.strip()
if (not line or (line[0] == '#') or
(line[:3] == '"""') or line[:3] == "'''"):
self.error('Blank or comment')
return 0
return lineno
def do_enable(self, arg):
if not arg:
self._print_invalid_arg(arg)
return
args = arg.split()
for i in args:
try:
bp = self.get_bpbynumber(i)
except ValueError as err:
self.error(err)
else:
bp.enable()
self.message('Enabled %s' % bp)
complete_enable = _complete_bpnumber
def do_disable(self, arg):
if not arg:
self._print_invalid_arg(arg)
return
args = arg.split()
for i in args:
try:
bp = self.get_bpbynumber(i)
except ValueError as err:
self.error(err)
else:
bp.disable()
self.message('Disabled %s' % bp)
complete_disable = _complete_bpnumber
def do_condition(self, arg):
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:
bp = self.get_bpbynumber(args[0].strip())
except IndexError:
self.error('Breakpoint number expected')
except ValueError as err:
self.error(err)
else:
bp.cond = cond
if not cond:
self.message('Breakpoint %d is now unconditional.' % bp.number)
else:
self.message('New condition set for breakpoint %d.' % bp.number)
complete_condition = _complete_bpnumber
def do_ignore(self, arg):
if not arg:
self._print_invalid_arg(arg)
return
args = arg.split()
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 ValueError as err:
self.error(err)
else:
bp.ignore = count
if count > 0:
if count > 1:
countstr = '%d crossings' % count
else:
countstr = '1 crossing'
self.message('Will ignore next %s of breakpoint %d.' %
(countstr, bp.number))
else:
self.message('Will stop next time breakpoint %d is reached.'
% bp.number)
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):
if not arg:
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()
for bp in bplist:
self.message('Deleted %s' % bp)
return
if ':' in arg:
i = arg.rfind(':')
filename = arg[:i]
arg = arg[i+1:]
try:
lineno = int(arg)
except ValueError:
err = "Invalid line number (%s)" % arg
else:
bplist = self.get_breaks(filename, lineno)[:]
err = self.clear_break(filename, lineno)
if err:
self.error(err)
else:
for bp in bplist:
self.message('Deleted %s' % bp)
return
numberlist = arg.split()
for i in numberlist:
try:
bp = self.get_bpbynumber(i)
except ValueError as err:
self.error(err)
else:
self.clear_bpbynumber(i)
self.message('Deleted %s' % bp)
do_cl = do_clear
complete_clear = _complete_location
complete_cl = _complete_location
def do_where(self, arg):
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
def _select_frame(self, number):
assert 0 <= number < len(self.stack)
self.curindex = number
self.curframe = self.stack[self.curindex][0]
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):
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):
if self.curindex == 0:
self.error('Oldest frame')
return
try:
count = int(arg or 1)
except ValueError:
self.error('Invalid frame count (%s)' % arg)
return
if count < 0:
newframe = 0
else:
newframe = max(0, self.curindex - count)
self._select_frame(newframe)
do_u = do_up
def do_down(self, arg):
if self.curindex + 1 == len(self.stack):
self.error('Newest frame')
return
try:
count = int(arg or 1)
except ValueError:
self.error('Invalid frame count (%s)' % arg)
return
if count < 0:
newframe = len(self.stack) - 1
else:
newframe = min(len(self.stack) - 1, self.curindex + count)
self._select_frame(newframe)
do_d = do_down
def do_until(self, arg):
if arg:
try:
lineno = int(arg)
except ValueError:
self.error('Error in argument: %r' % arg)
return
if lineno <= self.curframe.f_lineno:
self.error('"until" line number is smaller than current '
'line number')
return
else:
lineno = None
self.set_until(self.curframe, lineno)
return 1
do_unt = do_until
def do_step(self, arg):
if arg:
self._print_invalid_arg(arg)
return
self.set_step()
return 1
do_s = do_step
def do_next(self, arg):
if arg:
self._print_invalid_arg(arg)
return
self.set_next(self.curframe)
return 1
do_n = do_next
def do_run(self, arg):
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]
try:
sys.argv = shlex.split(arg)
except ValueError as e:
self.error('Cannot run %s: %s' % (arg, e))
return
sys.argv[:0] = argv0
raise Restart
do_restart = do_run
def do_return(self, arg):
if arg:
self._print_invalid_arg(arg)
return
self.set_return(self.curframe)
return 1
do_r = do_return
def do_continue(self, arg):
if arg:
self._print_invalid_arg(arg)
return
if not self.nosigint:
try:
Pdb._previous_sigint_handler = \
signal.signal(signal.SIGINT, self.sigint_handler)
except ValueError:
pass
self.set_continue()
return 1
do_c = do_cont = do_continue
def do_jump(self, arg):
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
try:
arg = int(arg)
except ValueError:
self.error("The 'jump' command requires a line number")
else:
try:
self.curframe.f_lineno = arg
self.stack[self.curindex] = self.stack[self.curindex][0], arg
self.print_stack_entry(self.stack[self.curindex])
except ValueError as e:
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):
if not arg:
self._print_invalid_arg(arg)
return
self.stop_trace()
globals = self.curframe.f_globals
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:
self._error_exc()
self.message("LEAVING RECURSIVE DEBUGGER")
self.start_trace()
self.lastcmd = p.lastcmd
complete_debug = _complete_expression
def do_quit(self, arg):
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
do_q = do_quit
do_exit = do_quit
def do_EOF(self, arg):
self.message('')
return self.do_quit(arg)
def do_args(self, arg):
if arg:
self._print_invalid_arg(arg)
return
co = self.curframe.f_code
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 = %s' % (name, self._safe_repr(dict[name], name)))
else:
self.message('%s = *** undefined ***' % (name,))
do_a = do_args
def do_retval(self, arg):
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.f_locals)
except:
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.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:
return 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):
if not arg:
self._print_invalid_arg(arg)
return
self._msg_val_func(arg, repr)
def do_pp(self, arg):
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):
self.lastcmd = 'list'
last = None
if arg and arg != '.':
try:
if ',' in arg:
first, last = arg.split(',')
first = int(first.strip())
last = int(last.strip())
if last < first:
last = first + last
else:
first = int(arg.strip())
first = max(1, first - 5)
except ValueError:
self.error('Error in argument: %r' % arg)
return
elif self.lineno is None or arg == '.':
first = max(1, self.curframe.f_lineno - 5)
else:
first = self.lineno + 1
if last is None:
last = first + 10
filename = self.curframe.f_code.co_filename
breaklist = self.get_file_breaks(filename)
try:
lines = linecache.getlines(filename, self.curframe.f_globals)
self._print_lines(lines[first-1:last], first, breaklist,
self.curframe)
self.lineno = min(last, len(lines))
if len(lines) < last:
self.message('[EOF]')
except KeyboardInterrupt:
pass
self._validate_file_mtime()
do_l = do_list
def do_longlist(self, arg):
if arg:
self._print_invalid_arg(arg)
return
filename = self.curframe.f_code.co_filename
breaklist = self.get_file_breaks(filename)
try:
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):
if not arg:
self._print_invalid_arg(arg)
return
try:
obj = self._getval(arg)
except:
return
try:
lines, lineno = self._getsourcelines(obj)
except (OSError, TypeError) as err:
self.error(err)
return
self._print_lines(lines, lineno)
complete_source = _complete_expression
def _print_lines(self, lines, start, breaks=(), frame=None):
if frame:
current_lineno = frame.f_lineno
exc_lineno = self.tb_lineno.get(frame, -1)
else:
current_lineno = exc_lineno = -1
for lineno, line in enumerate(lines, start):
s = str(lineno).rjust(3)
if len(s) < 4:
s += ' '
if lineno in breaks:
s += 'B'
else:
s += ' '
if lineno == current_lineno:
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):
if not arg:
self._print_invalid_arg(arg)
return
try:
value = self._getval(arg)
except:
return
code = None
try:
code = value.__func__.__code__
except Exception:
pass
if code:
self.message('Method %s' % code.co_name)
return
try:
code = value.__code__
except Exception:
pass
if code:
self.message('Function %s' % code.co_name)
return
if value.__class__ is type:
self.message('Class %s.%s' % (value.__module__, value.__qualname__))
return
self.message(type(value))
complete_whatis = _complete_expression
def do_display(self, arg):
if not arg:
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:
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
def do_undisplay(self, arg):
if arg:
try:
del self.displaying.get(self.curframe, {})[arg]
except KeyError:
self.error('not displaying %s' % arg)
else:
self.displaying.pop(self.curframe, None)
def complete_undisplay(self, text, line, begidx, endidx):
return [e for e in self.displaying.get(self.curframe, {})
if e.startswith(text)]
def do_interact(self, arg):
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):
args = arg.split()
if len(args) == 0:
keys = sorted(self.aliases.keys())
for alias in keys:
self.message("%s = %s" % (alias, self.aliases[alias]))
return
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:
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):
args = arg.split()
if len(args) == 0:
self._print_invalid_arg(arg)
return
if args[0] in self.aliases:
del self.aliases[args[0]]
def complete_unalias(self, text, line, begidx, endidx):
return [a for a in self.aliases if a.startswith(text)]
commands_resuming = ['do_continue', 'do_step', 'do_next', 'do_return',
'do_until', 'do_quit', 'do_jump']
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 stack_to_print:
self.print_stack_entry(frame_lineno)
except KeyboardInterrupt:
pass
def print_stack_entry(self, frame_lineno, prompt_prefix=line_prefix):
frame, lineno = frame_lineno
if frame is self.curframe:
prefix = '> '
else:
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:
lines[1] = self._colorize_code(lines[1])
stack_entry = prompt_prefix.join(lines)
self.message(prefix + stack_entry)
def do_help(self, arg):
if not arg:
return cmd.Cmd.do_help(self, arg)
try:
try:
topic = getattr(self, 'help_' + arg)
return topic()
except AttributeError:
command = getattr(self, 'do_' + arg)
except AttributeError:
self.error('No help for %r' % arg)
else:
if sys.flags.optimize >= 2:
self.error('No help for %r; please do not run Python with -OO '
'if you need command help' % arg)
return
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):
self.message((self.help_exec.__doc__ or '').strip())
def help_pdb(self):
help()
def lookupmodule(self, filename):
if not filename.endswith('.py'):
filename = filename.replace('.', os.sep) + '.py'
if os.path.isabs(filename):
if os.path.exists(filename):
return filename
return None
for dirname in sys.path:
while os.path.islink(dirname):
dirname = os.readlink(dirname)
fullname = os.path.join(dirname, filename)
if os.path.exists(fullname):
return fullname
return None
def _run(self, target: _ExecutableTarget):
self._wait_for_mainpyfile = True
self._user_requested_quit = False
self.mainpyfile = self.canonic(target.filename)
import __main__
__main__.__dict__.clear()
__main__.__dict__.update(target.namespace)
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):
try:
compile(expr, "<stdin>", "eval")
except SyntaxError as exc:
return _rstr(self._format_exc(exc))
return ""
def _getsourcelines(self, obj):
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):
if not arg:
self.error("Argument is required for this command")
else:
self.error(f"Invalid argument: {arg}")
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))
if __doc__ is not None:
_help_order = [
'help', 'where', 'down', 'up', 'break', 'tbreak', 'clear', 'disable',
'enable', 'ignore', 'condition', 'commands', 'step', 'next', 'until',
'jump', 'return', 'retval', 'run', 'continue', 'list', 'longlist',
'args', 'p', 'pp', 'whatis', 'source', 'display', 'undisplay',
'interact', 'alias', 'unalias', 'debug', 'quit',
]
for _command in _help_order:
__doc__ += getattr(Pdb, 'do_' + _command).__doc__.strip() + '\n\n'
__doc__ += Pdb.help_exec.__doc__
del _help_order, _command
def run(statement, globals=None, locals=None):
Pdb().run(statement, globals, locals)
def runeval(expression, globals=None, locals=None):
return Pdb().runeval(expression, globals, locals)
def runctx(statement, globals, locals):
run(statement, globals, locals)
def runcall(*args, **kwds):
return Pdb().runcall(*args, **kwds)
def set_trace(*, header=None, commands=None):
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, commands=commands)
async def set_trace_async(*, header=None, commands=None):
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)
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:
self._start_signal_listener(signal_server)
super().__init__(colorize=False, **kwargs)
self.colorize = colorize
@staticmethod
def protocol_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):
match msg:
case {"message": str(), "type": str()}:
pass
case {"help": str()}:
pass
case {"prompt": str(), "state": str()}:
pass
case {
"completions": list(completions)
} if all(isinstance(c, str) for c in completions):
pass
case {
"command_list": list(command_list)
} if all(isinstance(c, str) for c in command_list):
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):
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 signal.raise_signal(signal.SIGINT)
def stop_thread():
shut_down.set()
thread.join()
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):
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:
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):
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}"
)
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
self.error(f"Ignoring invalid message from client: {msg}")
def _complete_any(self, text, line, begidx, endidx):
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:
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):
self.quitting = False
if self._owns_sockfile:
Pdb._last_pdb_instance = None
try:
self._sockfile.close()
except OSError:
pass
def do_debug(self, arg):
self._command_name_cache = []
return super().do_debug(arg)
def do_alias(self, arg):
self._command_name_cache = []
return super().do_alias(arg)
def do_unalias(self, arg):
self._command_name_cache = []
return super().do_unalias(arg)
def do_help(self, arg):
self._send(help=arg)
do_h = do_help
def _interact_displayhook(self, obj):
if obj is not None:
self.message(repr(obj))
builtins._ = obj
def _run_in_python_repl(self, lines):
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):
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):
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):
try:
candidate = line.removeprefix("!") + "\n"
if codeop.compile_command(candidate, "<stdin>", "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):
match msg:
case {"reply": str()}:
pass
case {"signal": "EOF"}:
pass
case {"signal": "INT"}:
pass
case {
"complete": {
"text": str(),
"line": str(),
"begidx": int(),
"endidx": int(),
}
}:
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:
self.write_failed = True
def _readline(self):
if self.sigint_received:
self.sigint_received = False
raise KeyboardInterrupt
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:
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:
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":
return reply
prefix = ""
if self.state == "pdb":
cmd = self.pdb_instance.parseline(reply)[0]
if cmd in self.pdb_commands or reply.strip() == "":
return reply
if reply.startswith("!"):
prefix = "!"
reply = reply.removeprefix(prefix).lstrip()
if codeop.compile_command(reply + "\n", "<stdin>", "single") is not None:
return prefix + reply
more_prompt = "...".ljust(len(prompt))
while codeop.compile_command(reply, "<stdin>", "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":
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):
def handler(signum, frame):
self.sigint_received = True
if self.raise_on_sigint:
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:
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:
signal.signal(signal.SIGINT, old_handler)
@contextmanager
def _sigint_raises_keyboard_interrupt(self):
if self.sigint_received:
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:
self.interrupt_sock.sendall(signal.SIGINT.to_bytes())
else:
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:
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")
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=()):
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)
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()
def post_mortem(t=None):
return _post_mortem(t, Pdb())
def _post_mortem(t, pdb_instance):
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")
pdb_instance.reset()
pdb_instance.interaction(None, t)
def pm():
post_mortem(sys.last_exc)
TESTCMD = 'import x; x.main()'
def test():
run(TESTCMD)
def help():
import pydoc
pydoc.pager(__doc__)
_usage = """\
Debug the Python program given by pyfile. Alternatively,
an executable module or package to debug can be specified using
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
-c are executed after commands from .pdbrc files.
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 exit_with_permission_help_text():
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():
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,
)
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:
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':
parser.add_argument('-p', '--pid', type=int, help="attach to the specified PID", default=None)
opts, args = parser.parse_known_args()
if args:
parser.error(f"unrecognized arguments: {' '.join(args)}")
elif args[0] == '-m':
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_args = list(itertools.takewhile(lambda a: a.startswith('-'), args))
parser.error(f"unrecognized arguments: {' '.join(invalid_args)}")
return opts, args
def main():
opts, args = parse_args()
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
pdb = Pdb(mode='cli', backend='monitoring', colorize=True)
pdb.rcLines.extend(opts.commands)
while True:
try:
pdb._run(target)
except Restart:
print("Restarting", target, "with arguments:")
print("\t" + " ".join(sys.argv[1:]))
except SystemExit as e:
print("The program exited via sys.exit(). Exit status:", end=' ')
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")
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")
if __name__ == '__main__':
import pdb
pdb.main()