GitHub Viewer
"""Extract, format and print information about Python stack traces."""
import collections.abc
import functools
import itertools
import linecache
import os
import re
import sys
import textwrap
import types
import warnings
import codeop
import keyword
import tokenize
import io
import importlib.util
import pathlib
from contextlib import suppress
lazy import _colorize
try:
from _missing_stdlib_info import _MISSING_STDLIB_MODULE_MESSAGES
except ImportError:
_MISSING_STDLIB_MODULE_MESSAGES = {}
__all__ = ['extract_stack', 'extract_tb', 'format_exception',
'format_exception_only', 'format_list', 'format_stack',
'format_tb', 'print_exc', 'format_exc', 'print_exception',
'print_last', 'print_stack', 'print_tb', 'clear_frames',
'FrameSummary', 'StackSummary', 'TracebackException',
'walk_stack', 'walk_tb', 'print_list']
class _ShutdownTheme:
"""Empty stand-in if `_colorize` cannot be imported during late shutdown."""
def __getattr__(self, _): return self
def __getitem__(self, _): return ""
def __format__(self, _): return ""
def __str__(self): return ""
def __add__(self, other): return other
__radd__ = __add__
_shutdown_theme = _ShutdownTheme()
def _safe_get_theme(*, force_color=False, force_no_color=False):
try:
return _colorize.get_theme(
force_color=force_color, force_no_color=force_no_color
)
except ImportError:
return _shutdown_theme
def _safe_can_colorize(*, file=None):
try:
return _colorize.can_colorize(file=file)
except ImportError:
return False
#
# Formatting and printing lists of traceback lines.
#
def print_list(extracted_list, file=None):
"""Print the list of tuples as returned by extract_tb() or
extract_stack() as a formatted stack trace to the given file."""
if file is None:
file = sys.stderr
for item in StackSummary.from_list(extracted_list).format():
print(item, file=file, end="")
def format_list(extracted_list):
"""Format a list of tuples or FrameSummary objects for printing.
Given a list of tuples or FrameSummary objects as returned by
extract_tb() or extract_stack(), return a list of strings ready
for printing.
Each string in the resulting list corresponds to the item with the
same index in the argument list. Each string ends in a newline;
the strings may contain internal newlines as well, for those items
whose source text line is not None.
"""
return StackSummary.from_list(extracted_list).format()
#
# Printing and Extracting Tracebacks.
#
def print_tb(tb, limit=None, file=None):
"""Print up to 'limit' stack trace entries from the traceback 'tb'.
If 'limit' is omitted or None, all entries are printed. If 'file'
is omitted or None, the output goes to sys.stderr; otherwise
'file' should be an open file or file-like object with a write()
method.
"""
print_list(extract_tb(tb, limit=limit), file=file)
def format_tb(tb, limit=None):
"""A shorthand for 'format_list(extract_tb(tb, limit))'."""
return extract_tb(tb, limit=limit).format()
def extract_tb(tb, limit=None):
"""
Return a StackSummary object representing a list of
pre-processed entries from traceback.
This is useful for alternate formatting of stack traces. If
'limit' is omitted or None, all entries are extracted. A
pre-processed stack trace entry is a FrameSummary object
representing the information that is usually printed for a
stack trace. The line attribute is a string with
leading and trailing whitespace stripped; if the source is not
available the corresponding attribute is None.
"""
return StackSummary._extract_from_extended_frame_gen(
_walk_tb_with_full_positions(tb), limit=limit)
#
# Exception formatting and output.
#
_cause_message = (
"\nThe above exception was the direct cause "
"of the following exception:\n\n")
_context_message = (
"\nDuring handling of the above exception, "
"another exception occurred:\n\n")
class _Sentinel:
def __repr__(self):
return ""
_sentinel = _Sentinel()
def _parse_value_tb(exc, value, tb):
if (value is _sentinel) != (tb is _sentinel):
raise ValueError("Both or neither of value and tb must be given")
if value is tb is _sentinel:
if exc is not None:
if isinstance(exc, BaseException):
return exc, exc.__traceback__
raise TypeError(f'Exception expected for value, '
f'{type(exc).__name__} found')
else:
return None, None
return value, tb
def print_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \
file=None, chain=True, **kwargs):
"""Print exception up to 'limit' stack trace entries from 'tb' to 'file'.
This differs from print_tb() in the following ways: (1) if
traceback is not None, it prints a header "Traceback (most recent
call last):"; (2) it prints the exception type and value after the
stack trace; (3) if type is SyntaxError and value has the
appropriate format, it prints the line where the syntax error
occurred with a caret on the next line indicating the approximate
position of the error.
"""
colorize = kwargs.get("colorize", False)
value, tb = _parse_value_tb(exc, value, tb)
te = TracebackException(type(value), value, tb, limit=limit, compact=True)
te.print(file=file, chain=chain, colorize=colorize)
BUILTIN_EXCEPTION_LIMIT = object()
def _print_exception_bltin(exc, file=None, /):
if file is None:
file = sys.stderr if sys.stderr is not None else sys.__stderr__
colorize = _safe_can_colorize(file=file)
return print_exception(exc, limit=BUILTIN_EXCEPTION_LIMIT, file=file, colorize=colorize)
def format_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \
chain=True, **kwargs):
"""Format a stack trace and the exception information.
The arguments have the same meaning as the corresponding arguments
to print_exception(). The return value is a list of strings, each
ending in a newline and some containing internal newlines. When
these lines are concatenated and printed, exactly the same text is
printed as does print_exception().
"""
colorize = kwargs.get("colorize", False)
value, tb = _parse_value_tb(exc, value, tb)
te = TracebackException(type(value), value, tb, limit=limit, compact=True)
return list(te.format(chain=chain, colorize=colorize))
def format_exception_only(exc, /, value=_sentinel, *, show_group=False, **kwargs):
"""Format the exception part of a traceback.
The return value is a list of strings, each ending in a newline.
The list contains the exception's message, which is
normally a single string; however, for :exc:`SyntaxError` exceptions, it
contains several lines that (when printed) display detailed information
about where the syntax error occurred. Following the message, the list
contains the exception's ``__notes__``.
When *show_group* is ``True``, and the exception is an instance of
:exc:`BaseExceptionGroup`, the nested exceptions are included as
well, recursively, with indentation relative to their nesting depth.
"""
colorize = kwargs.get("colorize", False)
if value is _sentinel:
value = exc
te = TracebackException(type(value), value, None, compact=True)
return list(te.format_exception_only(show_group=show_group, colorize=colorize))
# -- not official API but folk probably use these two functions.
def _format_final_exc_line(etype, value, *, insert_final_newline=True, colorize=False):
valuestr = _safe_string(value, 'exception')
end_char = "\n" if insert_final_newline else ""
if colorize:
theme = _safe_get_theme(force_color=True).traceback
else:
theme = _safe_get_theme(force_no_color=True).traceback
if value is None or not valuestr:
line = f"{theme.type}{etype}{theme.reset}{end_char}"
else:
line = f"{theme.type}{etype}{theme.reset}: {theme.message}{valuestr}{theme.reset}{end_char}"
return line
def _safe_string(value, what, func=str):
try:
return func(value)
except:
return f''
# --
def print_exc(limit=None, file=None, chain=True, **kwargs):
"""Shorthand for 'print_exception(sys.exception(), limit=limit, file=file, chain=chain)'."""
print_exception(sys.exception(), limit=limit, file=file, chain=chain, **kwargs)
def format_exc(limit=None, chain=True):
"""Like print_exc() but return a string."""
return "".join(format_exception(sys.exception(), limit=limit, chain=chain))
def print_last(limit=None, file=None, chain=True):
"""This is a shorthand for 'print_exception(sys.last_exc, limit=limit, file=file, chain=chain)'."""
if not hasattr(sys, "last_exc") and not hasattr(sys, "last_type"):
raise ValueError("no last exception")
if hasattr(sys, "last_exc"):
print_exception(sys.last_exc, limit=limit, file=file, chain=chain)
else:
print_exception(sys.last_type, sys.last_value, sys.last_traceback,
limit=limit, file=file, chain=chain)
#
# Printing and Extracting Stacks.
#
def print_stack(f=None, limit=None, file=None):
"""Print a stack trace from its invocation point.
The optional 'f' argument can be used to specify an alternate
stack frame at which to start. The optional 'limit' and 'file'
arguments have the same meaning as for print_exception().
"""
if f is None:
f = sys._getframe().f_back
print_list(extract_stack(f, limit=limit), file=file)
def format_stack(f=None, limit=None):
"""Shorthand for 'format_list(extract_stack(f, limit))'."""
if f is None:
f = sys._getframe().f_back
return format_list(extract_stack(f, limit=limit))
def extract_stack(f=None, limit=None):
"""Extract the raw traceback from the current stack frame.
The return value has the same format as for extract_tb(). The
optional 'f' and 'limit' arguments have the same meaning as for
print_stack(). Each item in the list is a FrameSummary object,
and the entries are in order from oldest to newest stack frame.
"""
if f is None:
f = sys._getframe().f_back
stack = StackSummary.extract(walk_stack(f), limit=limit)
stack.reverse()
return stack
def clear_frames(tb):
"Clear all references to local variables in the frames of a traceback."
while tb is not None:
try:
tb.tb_frame.clear()
except RuntimeError:
# Ignore the exception raised if the frame is still executing.
pass
tb = tb.tb_next
class FrameSummary:
"""Information about a single frame from a traceback.
- :attr:`filename` The filename for the frame.
- :attr:`lineno` The line within filename for the frame that was
active when the frame was captured.
- :attr:`name` The name of the function or method that was executing
when the frame was captured.
- :attr:`line` The text from the linecache module for the line
of code that was running when the frame was captured.
- :attr:`locals` Either None if locals were not supplied, or a dict
mapping the name to the repr() of the variable.
- :attr:`end_lineno` The last line number of the source code for this frame.
By default, it is set to lineno and indexation starts from 1.
- :attr:`colno` The column number of the source code for this frame.
By default, it is None and indexation starts from 0.
- :attr:`end_colno` The last column number of the source code for this frame.
By default, it is None and indexation starts from 0.
"""
__slots__ = ('filename', 'lineno', 'end_lineno', 'colno', 'end_colno',
'name', '_lines', '_lines_dedented', 'locals', '_code')
def __init__(self, filename, lineno, name, *, lookup_line=True,
locals=None, line=None,
end_lineno=None, colno=None, end_colno=None, **kwargs):
"""Construct a FrameSummary.
:param lookup_line: If True, `linecache` is consulted for the source
code line. Otherwise, the line will be looked up when first needed.
:param locals: If supplied the frame locals, which will be captured as
object representations.
:param line: If provided, use this instead of looking up the line in
the linecache.
"""
self.filename = filename
self.lineno = lineno
self.end_lineno = lineno if end_lineno is None else end_lineno
self.colno = colno
self.end_colno = end_colno
self.name = name
self._code = kwargs.get("_code")
self._lines = line
self._lines_dedented = None
if lookup_line:
self.line
self.locals = {k: _safe_string(v, 'local', func=repr)
for k, v in locals.items()} if locals else None
def __eq__(self, other):
if isinstance(other, FrameSummary):
return (self.filename == other.filename and
self.lineno == other.lineno and
self.name == other.name and
self.locals == other.locals)
if isinstance(other, tuple):
return (self.filename, self.lineno, self.name, self.line) == other
return NotImplemented
def __getitem__(self, pos):
return (self.filename, self.lineno, self.name, self.line)[pos]
def __iter__(self):
return iter([self.filename, self.lineno, self.name, self.line])
def __repr__(self):
return "".format(
filename=self.filename, lineno=self.lineno, name=self.name)
def __len__(self):
return 4
def _set_lines(self):
if (
self._lines is None
and self.lineno is not None
and self.end_lineno is not None
):
lines = []
for lineno in range(self.lineno, self.end_lineno + 1):
# treat errors (empty string) and empty lines (newline) as the same
line = linecache.getline(self.filename, lineno).rstrip()
if not line and self._code is not None and self.filename.startswith(" 0:
# Nested exceptions needs correct handling of multiline messages.
formatted = _format_final_exc_line(
stype, self._str, insert_final_newline=False, colorize=colorize
).split('\n')
yield from [
indent + l + '\n'
for l in formatted
]
else:
yield _format_final_exc_line(stype, self._str, colorize=colorize)
else:
yield from [indent + l for l in self._format_syntax_error(stype, colorize=colorize)]
if (
isinstance(self.__notes__, collections.abc.Sequence)
and not isinstance(self.__notes__, (str, bytes))
):
for note in self.__notes__:
note = _safe_string(note, 'note')
yield from _format_note(note, indent, theme)
elif self.__notes__ is not None:
note = _safe_string(self.__notes__, '__notes__', func=repr)
yield from _format_note(note, indent, theme)
if self.exceptions and show_group:
for ex in self.exceptions:
yield from ex.format_exception_only(show_group=show_group, _depth=_depth+1, colorize=colorize)
def _find_keyword_typos(self):
assert self._is_syntax_error
try:
import _suggestions
except ImportError:
_suggestions = None
# Only try to find keyword typos if there is no custom message
if self.msg != "invalid syntax" and "Perhaps you forgot a comma" not in self.msg:
return
if not self._exc_metadata:
return
line, offset, source = self._exc_metadata
end_line = int(self.lineno) if self.lineno is not None else 0
lines = None
from_filename = False
if source is None:
if self.filename:
try:
with open(self.filename) as f:
lines = f.read().splitlines()
except Exception:
line, end_line, offset = 0,1,0
else:
from_filename = True
lines = lines if lines is not None else self.text.splitlines()
else:
lines = source.splitlines()
error_code = lines[line -1 if line > 0 else 0:end_line]
error_code = textwrap.dedent('\n'.join(error_code))
# Do not continue if the source is too large
if len(error_code) > 1024:
return
# If the original code doesn't raise SyntaxError, we can't validate
# that a keyword replacement actually fixes anything
try:
codeop.compile_command(error_code, symbol="exec", flags=codeop.PyCF_ONLY_AST)
except SyntaxError:
pass # Good - the original code has a syntax error we might fix
else:
return # Original code compiles or is incomplete - can't validate fixes
error_lines = error_code.splitlines()
tokens = tokenize.generate_tokens(io.StringIO(error_code).readline)
tokens_left_to_process = 10
import difflib
for token in tokens:
start, end = token.start, token.end
if token.type != tokenize.NAME:
continue
# Only consider NAME tokens on the same line as the error
the_end = end_line if line == 0 else end_line + 1
if from_filename and token.start[0]+line != the_end:
continue
wrong_name = token.string
if wrong_name in keyword.kwlist:
continue
# Limit the number of valid tokens to consider to not spend
# to much time in this function
tokens_left_to_process -= 1
if tokens_left_to_process < 0:
break
# Limit the number of possible matches to try
max_matches = 3
matches = []
hint = _get_cross_language_keyword_hint(wrong_name)
if hint:
matches.append(hint)
if _suggestions is not None:
suggestion = _suggestions._generate_suggestions(keyword.kwlist + keyword.softkwlist, wrong_name)
if suggestion:
matches.append(suggestion)
matches.extend(
difflib.get_close_matches(
wrong_name,
keyword.kwlist + keyword.softkwlist,
n=max_matches,
cutoff=0.5
)
)
matches = matches[:max_matches]
for suggestion in matches:
if not suggestion or suggestion == wrong_name:
continue
# Try to replace the token with the keyword
the_lines = error_lines.copy()
the_line = the_lines[start[0] - 1][:]
chars = list(the_line)
chars[token.start[1]:token.end[1]] = suggestion
the_lines[start[0] - 1] = ''.join(chars)
code = '\n'.join(the_lines)
# Check if it works
try:
codeop.compile_command(code, symbol="exec", flags=codeop.PyCF_ONLY_AST)
except SyntaxError:
continue
# Keep token.line but handle offsets correctly
self.text = token.line
self.offset = token.start[1] + 1
self.end_offset = token.end[1] + 1
self.lineno = start[0]
self.end_lineno = end[0]
self.msg = f"invalid syntax. Did you mean '{suggestion}'?"
return
def _format_syntax_error(self, stype, **kwargs):
"""Format SyntaxError exceptions (internal helper)."""
# Show exactly where the problem was found.
colorize = kwargs.get("colorize", False)
if colorize:
theme = _safe_get_theme(force_color=True).traceback
else:
theme = _safe_get_theme(force_no_color=True).traceback
filename_suffix = ''
if self.lineno is not None:
yield ' File {}"{}"{}, line {}{}{}\n'.format(
theme.filename,
self.filename or "",
theme.reset,
theme.line_no,
self.lineno,
theme.reset,
)
elif self.filename is not None:
filename_suffix = ' ({})'.format(self.filename)
text = self.text
if isinstance(text, str):
# text = " foo\n"
# rtext = " foo"
# ltext = "foo"
with suppress(Exception):
self._find_keyword_typos()
text = self.text
rtext = text.rstrip('\n')
ltext = rtext.lstrip(' \n\f')
spaces = len(rtext) - len(ltext)
if self.offset is None:
yield ' {}\n'.format(ltext)
elif isinstance(self.offset, int):
offset = self.offset
if self.lineno == self.end_lineno:
end_offset = (
self.end_offset
if (
isinstance(self.end_offset, int)
and self.end_offset != 0
)
else offset
)
else:
end_offset = len(rtext) + 1
if self.text and offset > len(self.text):
offset = len(rtext) + 1
if self.text and end_offset > len(self.text):
end_offset = len(rtext) + 1
if offset >= end_offset or end_offset < 0:
end_offset = offset + 1
# Convert 1-based column offset to 0-based index into stripped text
colno = offset - 1 - spaces
end_colno = end_offset - 1 - spaces
if colno >= 0:
# Calculate display width to account for wide characters
dp_colno = _display_width(ltext, colno)
highlighted = ltext[colno:end_colno]
caret_count = _display_width(highlighted) if highlighted else (end_colno - colno)
start_color = end_color = ""
if colorize:
# colorize from colno to end_colno
ltext = (
ltext[:colno] +
theme.error_highlight + ltext[colno:end_colno] + theme.reset +
ltext[end_colno:]
)
start_color = theme.error_highlight
end_color = theme.reset
yield ' {}\n'.format(ltext)
yield ' {}{}{}{}\n'.format(
' ' * dp_colno,
start_color,
'^' * caret_count,
end_color,
)
else:
yield ' {}\n'.format(ltext)
try:
if not (msg := self.msg):
msg = ""
except TypeError:
pass
yield "{}{}{}: {}{}{}{}\n".format(
theme.type,
stype,
theme.reset,
theme.message,
msg,
theme.reset,
filename_suffix,
)
def format(self, *, chain=True, _ctx=None, **kwargs):
"""Format the exception.
If chain is not *True*, *__cause__* and *__context__* will not be formatted.
The return value is a generator of strings, each ending in a newline and
some containing internal newlines. `print_exception` is a wrapper around
this method which just prints the lines to a file.
The message indicating which exception occurred is always the last
string in the output.
"""
colorize = kwargs.get("colorize", False)
if _ctx is None:
_ctx = _ExceptionPrintContext()
output = []
exc = self
if chain:
while exc:
if exc.__cause__ is not None:
chained_msg = _cause_message
chained_exc = exc.__cause__
elif (exc.__context__ is not None and
not exc.__suppress_context__):
chained_msg = _context_message
chained_exc = exc.__context__
else:
chained_msg = None
chained_exc = None
output.append((chained_msg, exc))
exc = chained_exc
else:
output.append((None, exc))
for msg, exc in reversed(output):
if msg is not None:
yield from _ctx.emit(msg)
if exc.exceptions is None:
if exc.stack:
yield from _ctx.emit('Traceback (most recent call last):\n')
yield from _ctx.emit(exc.stack.format(colorize=colorize))
yield from _ctx.emit(exc.format_exception_only(colorize=colorize))
elif _ctx.exception_group_depth > self.max_group_depth:
# exception group, but depth exceeds limit
yield from _ctx.emit(
f"... (max_group_depth is {self.max_group_depth})\n")
else:
# format exception group
is_toplevel = (_ctx.exception_group_depth == 0)
if is_toplevel:
_ctx.exception_group_depth += 1
if exc.stack:
yield from _ctx.emit(
'Exception Group Traceback (most recent call last):\n',
margin_char = '+' if is_toplevel else None)
yield from _ctx.emit(exc.stack.format(colorize=colorize))
yield from _ctx.emit(exc.format_exception_only(colorize=colorize))
num_excs = len(exc.exceptions)
if num_excs = self.max_group_width)
else:
truncated = False
title = f'{i+1}' if not truncated else '...'
yield (_ctx.indent() +
('+-' if i==0 else ' ') +
f'+---------------- {title} ----------------\n')
_ctx.exception_group_depth += 1
if not truncated:
yield from exc.exceptions[i].format(chain=chain, _ctx=_ctx, colorize=colorize)
else:
remaining = num_excs - self.max_group_width
plural = 's' if remaining > 1 else ''
yield from _ctx.emit(
f"and {remaining} more exception{plural}\n")
if last_exc and _ctx.need_close:
yield (_ctx.indent() +
"+------------------------------------\n")
_ctx.need_close = False
_ctx.exception_group_depth -= 1
if is_toplevel:
assert _ctx.exception_group_depth == 1
_ctx.exception_group_depth = 0
def print(self, *, file=None, chain=True, **kwargs):
"""Print the result of self.format(chain=chain) to 'file'."""
colorize = kwargs.get("colorize", False)
if file is None:
file = sys.stderr
for line in self.format(chain=chain, colorize=colorize):
print(line, file=file, end="")
_MAX_CANDIDATE_ITEMS = 750
_MAX_STRING_SIZE = 40
_MOVE_COST = 2
_CASE_COST = 1
# Cross-language method suggestions for builtin types.
# Consulted as a fallback when Levenshtein-based suggestions find no match.
#
# Inclusion criteria:
#
# 1. Must have evidence of real cross-language confusion (Stack Overflow
# traffic, bug reports in production repos, developer survey data).
# 2. Must not be catchable by Levenshtein distance (too different from
# the correct Python method name).
#
# Each entry maps a wrong method name to a list of (type, suggestion, is_raw)
# tuples. The lookup checks isinstance() so subclasses are also matched.
# If is_raw is False, the suggestion is wrapped in "Did you mean '.X'?".
# If is_raw is True, the suggestion is rendered as-is.
#
# See https://github.com/python/cpython/issues/146406.
_CROSS_LANGUAGE_HINTS = frozendict({
# list -- JavaScript/Ruby equivalents
"push": ((list, "append", False),),
"concat": ((list, "extend", False),),
# list -- Java/C# equivalents
"addAll": ((list, "extend", False),),
"contains": ((list, "Use 'x in list'.", True),),
# list -- wrong-type suggestion (user expected a set)
"add": ((list, "Did you mean to use a 'set' object?", True),
(frozenset, "Did you mean to use a 'set' object?", True)),
# str -- JavaScript equivalents
"toUpperCase": ((str, "upper", False),),
"toLowerCase": ((str, "lower", False),),
"trimStart": ((str, "lstrip", False),),
"trimEnd": ((str, "rstrip", False),),
# dict -- Java/JavaScript equivalents
"keySet": ((dict, "keys", False),),
"entrySet": ((dict, "items", False),),
"entries": ((dict, "items", False),),
"putAll": ((dict, "update", False),),
"put": ((dict, "Use d[k] = v.", True),),
# tuple -- mutable method on immutable type (user expected a list)
"append": ((tuple, "Did you mean to use a 'list' object?", True),),
"extend": ((tuple, "Did you mean to use a 'list' object?", True),),
"insert": ((tuple, "Did you mean to use a 'list' object?", True),),
"remove": ((tuple, "Did you mean to use a 'list' object?", True),
(frozenset, "Did you mean to use a 'set' object?", True)),
# frozenset -- mutable method on immutable type (user expected a set)
"discard": ((frozenset, "Did you mean to use a 'set' object?", True),),
# frozendict -- mutable method on immutable type (user expected a dict)
"update": ((frozenset, "Did you mean to use a 'set' object?", True),
(frozendict, "Did you mean to use a 'dict' object?", True)),
# clear() -- shared across immutable container types (user expected the mutable counterpart)
"clear": ((tuple, "Did you mean to use a 'list' object?", True),
(frozenset, "Did you mean to use a 'set' object?", True),
(frozendict, "Did you mean to use a 'dict' object?", True)),
# float -- bitwise operators belong to int
"__or__": ((float, "Did you mean to use an 'int' object? Bitwise operators are not supported by 'float'.", True),),
"__and__": ((float, "Did you mean to use an 'int' object? Bitwise operators are not supported by 'float'.", True),),
"__xor__": ((float, "Did you mean to use an 'int' object? Bitwise operators are not supported by 'float'.", True),),
"__lshift__": ((float, "Did you mean to use an 'int' object? Bitwise operators are not supported by 'float'.", True),),
"__rshift__": ((float, "Did you mean to use an 'int' object? Bitwise operators are not supported by 'float'.", True),),
})
# Cross-language keyword suggestions.
_CROSS_LANGUAGE_KEYWORD_HINTS = frozendict({
# C/C++ equivalents
'switch': 'match',
'delete': 'del',
# function define equivalents
'function': 'def',
'func': 'def',
'void': 'def',
})
def _substitution_cost(ch_a, ch_b):
if ch_a == ch_b:
return 0
if ch_a.lower() == ch_b.lower():
return _CASE_COST
return _MOVE_COST
def _is_lazy_import(obj, attr_name):
"""Check if attr_name in obj's __dict__ is a lazy import.
Returns True if obj is a module and the attribute is a LazyImportType,
False otherwise. This avoids triggering module loading when computing
suggestions for AttributeError.
"""
if not isinstance(obj, types.ModuleType):
return False
obj_dict = getattr(obj, '__dict__', None)
if obj_dict is None:
return False
attr_value = obj_dict.get(attr_name)
return isinstance(attr_value, types.LazyImportType)
def _check_for_nested_attribute(obj, wrong_name, attrs):
"""Check if any attribute of obj has the wrong_name as a nested attribute.
Returns the first nested attribute suggestion found, or None.
Limited to checking 20 attributes.
Only considers non-descriptor outer attributes to avoid executing
arbitrary code. Checks nested attributes statically so descriptors such
as properties can still be suggested without invoking them.
Skips lazy imports to avoid triggering module loading.
"""
from inspect import getattr_static
# Check for nested attributes (only one level deep)
attrs_to_check = [x for x in attrs if not x.startswith('_')][:20] # Limit number of attributes to check
for attr_name in attrs_to_check:
with suppress(Exception):
# Check if attr_name is a descriptor - if so, skip it
attr_from_class = getattr_static(type(obj), attr_name, _sentinel)
if attr_from_class is not _sentinel and hasattr(attr_from_class, '__get__'):
continue # Skip descriptors to avoid executing arbitrary code
# Skip lazy imports to avoid triggering module loading
if _is_lazy_import(obj, attr_name):
continue
# Safe to get the attribute since it's not a descriptor
attr_obj = getattr(obj, attr_name)
if _is_lazy_import(attr_obj, wrong_name):
continue
if getattr_static(attr_obj, wrong_name, _sentinel) is not _sentinel:
return f"{attr_name}.{wrong_name}"
return None
def _get_cross_language_hint(obj, wrong_name):
"""Check if wrong_name is a common method name from another language,
a mutable method on an immutable type, or a method tried on None.
Uses isinstance() so subclasses of builtin types also get hints.
Returns a formatted hint string, or None.
"""
entries = _CROSS_LANGUAGE_HINTS.get(wrong_name)
if entries is None:
return None
for check_type, hint, is_raw in entries:
if isinstance(obj, check_type):
if is_raw:
return hint
return f"Did you mean '.{hint}'?"
return None
def _get_cross_language_keyword_hint(wrong_name):
"""Check if wrong_name is a common keyword from another language
"""
return _CROSS_LANGUAGE_KEYWORD_HINTS.get(wrong_name)
def _get_safe___dir__(obj):
# Use obj.__dir__() to avoid a TypeError when calling dir(obj).
# See gh-131001 and gh-139933.
# Also filters out lazy imports to avoid triggering module loading.
try:
d = obj.__dir__()
except TypeError: # when obj is a class
d = type(obj).__dir__(obj)
return sorted(
x for x in d if isinstance(x, str) and not _is_lazy_import(obj, x)
)
def _compute_suggestion_error(exc_value, tb, wrong_name):
if wrong_name is None or not isinstance(wrong_name, str):
return None
not_normalized = False
if not wrong_name.isascii():
from unicodedata import normalize
normalized_name = normalize('NFKC', wrong_name)
if normalized_name != wrong_name:
not_normalized = True
wrong_name = normalized_name
if isinstance(exc_value, AttributeError):
obj = exc_value.obj
try:
d = _get_safe___dir__(obj)
hide_underscored = (wrong_name[:1] != '_')
if hide_underscored and tb is not None:
while tb.tb_next is not None:
tb = tb.tb_next
frame = tb.tb_frame
if 'self' in frame.f_locals and frame.f_locals['self'] is obj:
hide_underscored = False
if hide_underscored:
d = [x for x in d if x[:1] != '_']
except Exception:
return None
elif isinstance(exc_value, ModuleNotFoundError):
try:
if parent_name := wrong_name.rpartition('.')[0]:
parent = importlib.util.find_spec(parent_name)
else:
parent = None
d = []
for finder in sys.meta_path:
if discover := getattr(finder, 'discover', None):
d += [spec.name for spec in discover(parent)]
except Exception:
return None
elif isinstance(exc_value, ImportError):
try:
mod = __import__(exc_value.name)
d = _get_safe___dir__(mod)
if wrong_name[:1] != '_':
d = [x for x in d if x[:1] != '_']
except Exception:
return None
else:
assert isinstance(exc_value, NameError)
try:
# find most recent frame
if tb is None:
return None
while tb.tb_next is not None:
tb = tb.tb_next
frame = tb.tb_frame
d = (
list(frame.f_locals)
+ list(frame.f_globals)
+ list(frame.f_builtins)
)
d = [x for x in d if isinstance(x, str)]
if not_normalized and wrong_name in d:
return wrong_name
# Check first if we are in a method and the instance
# has the wrong name as attribute
if 'self' in frame.f_locals:
self = frame.f_locals['self']
try:
has_wrong_name = hasattr(self, wrong_name)
except Exception:
has_wrong_name = False
if has_wrong_name:
return f"self.{wrong_name}"
except Exception:
return None
if not_normalized and wrong_name in d:
return wrong_name
try:
import _suggestions
except ImportError:
pass
else:
suggestion = _suggestions._generate_suggestions(d, wrong_name)
if suggestion:
return suggestion
# Compute closest match
if len(d) > _MAX_CANDIDATE_ITEMS:
return None
wrong_name_len = len(wrong_name)
if wrong_name_len > _MAX_STRING_SIZE:
return None
best_distance = wrong_name_len
suggestion = None
for possible_name in d:
if possible_name == wrong_name:
# A missing attribute is "found". Don't suggest it (see GH-88821).
continue
# No more than 1/3 of the involved characters should need changed.
max_distance = (len(possible_name) + wrong_name_len + 3) * _MOVE_COST // 6
# Don't take matches we've already beaten.
max_distance = min(max_distance, best_distance - 1)
current_distance = _levenshtein_distance(wrong_name, possible_name, max_distance)
if current_distance > max_distance:
continue
if not suggestion or current_distance < best_distance:
suggestion = possible_name
best_distance = current_distance
# If no direct attribute match found, check for nested attributes
if not suggestion and isinstance(exc_value, AttributeError):
with suppress(Exception):
nested_suggestion = _check_for_nested_attribute(exc_value.obj, wrong_name, d)
if nested_suggestion:
return nested_suggestion
return suggestion
def _levenshtein_distance(a, b, max_cost):
# A Python implementation of Python/suggestions.c:levenshtein_distance.
# Both strings are the same
if a == b:
return 0
# Trim away common affixes
pre = 0
while a[pre:] and b[pre:] and a[pre] == b[pre]:
pre += 1
a = a[pre:]
b = b[pre:]
post = 0
while a[:post or None] and b[:post or None] and a[post-1] == b[post-1]:
post -= 1
a = a[:post or None]
b = b[:post or None]
if not a or not b:
return _MOVE_COST * (len(a) + len(b))
if len(a) > _MAX_STRING_SIZE or len(b) > _MAX_STRING_SIZE:
return max_cost + 1
# Prefer shorter buffer
if len(b) < len(a):
a, b = b, a
# Quick fail when a match is impossible
if (len(b) - len(a)) * _MOVE_COST > max_cost:
return max_cost + 1
# Instead of producing the whole traditional len(a)-by-len(b)
# matrix, we can update just one row in place.
# Initialize the buffer row
row = list(range(_MOVE_COST, _MOVE_COST * (len(a) + 1), _MOVE_COST))
result = 0
for bindex in range(len(b)):
bchar = b[bindex]
distance = result = bindex * _MOVE_COST
minimum = sys.maxsize
for index in range(len(a)):
# 1) Previous distance in this row is cost(b[:b_index], a[:index])
substitute = distance + _substitution_cost(bchar, a[index])
# 2) cost(b[:b_index], a[:index+1]) from previous row
distance = row[index]
# 3) existing result is cost(b[:b_index+1], a[index])
insert_delete = min(result, distance) + _MOVE_COST
result = min(insert_delete, substitute)
# cost(b[:b_index+1], a[:index+1])
row[index] = result
if result < minimum:
minimum = result
if minimum > max_cost:
# Everything in this row is too big, so bail early.
return max_cost + 1
return result
def _find_incompatible_extension_module(module_name):
import importlib.machinery
import importlib.resources.readers
if not module_name or not importlib.machinery.EXTENSION_SUFFIXES:
return
# We assume the last extension is untagged (eg. .so, .pyd)!
# tests.test_traceback.MiscTest.test_find_incompatible_extension_modules
# tests that assumption.
untagged_suffix = importlib.machinery.EXTENSION_SUFFIXES[-1]
# On Windows the debug tag is part of the module file stem, instead of the
# extension (eg. foo_d.pyd), so let's remove it and just look for .pyd.
if os.name == 'nt':
untagged_suffix = untagged_suffix.removeprefix('_d')
parent, _, child = module_name.rpartition('.')
if parent:
traversable = importlib.resources.files(parent)
else:
traversable = importlib.resources.readers.MultiplexedPath(
*map(pathlib.Path, filter(os.path.isdir, sys.path))
)
for entry in traversable.iterdir():
if entry.name.startswith(child + '.') and entry.name.endswith(untagged_suffix):
return entry.name