FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

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

Back | FazBrowse Home | New Git URL