| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555 |
- import functools
- import inspect
- import pydoc
- import sys
- import types
- import warnings
- from types import TracebackType
- from typing import Any, Optional, Tuple
- from collections.abc import Callable
- import stack_data
- from pygments.token import Token
- from IPython import get_ipython
- from IPython.core import debugger
- from IPython.utils import path as util_path
- from IPython.utils import py3compat
- from IPython.utils.PyColorize import Theme, TokenStream, theme_table
- _sentinel = object()
- INDENT_SIZE = 8
- @functools.lru_cache
- def count_lines_in_py_file(filename: str) -> int:
- """
- Given a filename, returns the number of lines in the file
- if it ends with the extension ".py". Otherwise, returns 0.
- """
- if not filename.endswith(".py"):
- return 0
- else:
- try:
- with open(filename, "r") as file:
- s = sum(1 for line in file)
- except UnicodeError:
- return 0
- return s
- def get_line_number_of_frame(frame: types.FrameType) -> int:
- """
- Given a frame object, returns the total number of lines in the file
- containing the frame's code object, or the number of lines in the
- frame's source code if the file is not available.
- Parameters
- ----------
- frame : FrameType
- The frame object whose line number is to be determined.
- Returns
- -------
- int
- The total number of lines in the file containing the frame's
- code object, or the number of lines in the frame's source code
- if the file is not available.
- """
- filename = frame.f_code.co_filename
- if filename is None:
- print("No file....")
- lines, first = inspect.getsourcelines(frame)
- return first + len(lines)
- return count_lines_in_py_file(filename)
- def _safe_string(value: Any, what: Any, func: Any = str) -> str:
- # Copied from cpython/Lib/traceback.py
- try:
- return func(value)
- except:
- return f"<{what} {func.__name__}() failed>"
- def _format_traceback_lines(
- lines: list[stack_data.Line],
- theme: Theme,
- has_colors: bool,
- lvals_toks: list[TokenStream],
- ) -> TokenStream:
- """
- Format tracebacks lines with pointing arrow, leading numbers,
- this assumes the stack have been extracted using stackdata.
- Parameters
- ----------
- lines : list[Line]
- """
- numbers_width = INDENT_SIZE - 1
- tokens: TokenStream = []
- for stack_line in lines:
- if stack_line is stack_data.LINE_GAP:
- toks = [(Token.LinenoEm, " (...)")]
- tokens.extend(toks)
- continue
- lineno = stack_line.lineno
- line = stack_line.render(pygmented=has_colors).rstrip("\n") + "\n"
- if stack_line.is_current:
- # This is the line with the error
- pad = numbers_width - len(str(lineno))
- toks = [
- (Token.LinenoEm, theme.make_arrow(pad)),
- (Token.LinenoEm, str(lineno)),
- (Token, " "),
- (Token, line),
- ]
- else:
- num = "%*s" % (numbers_width, lineno)
- toks = [
- (Token.LinenoEm, str(num)),
- (Token, " "),
- (Token, line),
- ]
- tokens.extend(toks)
- if lvals_toks and stack_line.is_current:
- for lv in lvals_toks:
- tokens.append((Token, " " * INDENT_SIZE))
- tokens.extend(lv)
- tokens.append((Token, "\n"))
- # strip the last newline
- tokens = tokens[:-1]
- return tokens
- # some internal-use functions
- def text_repr(value: Any) -> str:
- """Hopefully pretty robust repr equivalent."""
- # this is pretty horrible but should always return *something*
- try:
- return pydoc.text.repr(value) # type: ignore[call-arg]
- except KeyboardInterrupt:
- raise
- except:
- try:
- return repr(value)
- except KeyboardInterrupt:
- raise
- except:
- try:
- # all still in an except block so we catch
- # getattr raising
- name = getattr(value, "__name__", None)
- if name:
- # ick, recursion
- return text_repr(name)
- klass = getattr(value, "__class__", None)
- if klass:
- return "%s instance" % text_repr(klass)
- return "UNRECOVERABLE REPR FAILURE"
- except KeyboardInterrupt:
- raise
- except:
- return "UNRECOVERABLE REPR FAILURE"
- def eqrepr(value: Any, repr: Callable[[Any], str] = text_repr) -> str:
- return "=%s" % repr(value)
- def nullrepr(value: Any, repr: Callable[[Any], str] = text_repr) -> str:
- return ""
- def _tokens_filename(
- em: bool,
- file: str | None,
- *,
- lineno: int | None = None,
- ) -> TokenStream:
- """
- Format filename lines with custom formatting from caching compiler or `File *.py` by default
- Parameters
- ----------
- em: wether bold or not
- file : str
- """
- Normal = Token.NormalEm if em else Token.Normal
- Filename = Token.FilenameEm if em else Token.Filename
- ipinst = get_ipython()
- if (
- ipinst is not None
- and (data := ipinst.compile.format_code_name(file)) is not None
- ):
- label, name = data
- if lineno is None:
- return [
- (Normal, label),
- (Normal, " "),
- (Filename, name),
- ]
- else:
- return [
- (Normal, label),
- (Normal, " "),
- (Filename, name),
- (Filename, f", line {lineno}"),
- ]
- else:
- name = util_path.compress_user(
- py3compat.cast_unicode(file or "", util_path.fs_encoding)
- )
- if lineno is None:
- return [
- (Normal, "File "),
- (Filename, name),
- ]
- else:
- return [
- (Normal, "File "),
- (Filename, f"{name}:{lineno}"),
- ]
- def _simple_format_traceback_lines(
- lnum: int,
- index: int,
- lines: list[tuple[str, tuple[str, bool]]],
- lvals_toks: list[TokenStream],
- theme: Theme,
- ) -> TokenStream:
- """
- Format tracebacks lines with pointing arrow, leading numbers
- This should be equivalent to _format_traceback_lines, but does not rely on stackdata
- to format the lines
- This is due to the fact that stackdata may be slow on super long and complex files.
- Parameters
- ==========
- lnum: int
- number of the target line of code.
- index: int
- which line in the list should be highlighted.
- lines: list[string]
- lvals_toks: pairs of token type and str
- Values of local variables, already colored, to inject just after the error line.
- """
- for item in lvals_toks:
- assert isinstance(item, list)
- for subit in item:
- assert isinstance(subit[1], str)
- numbers_width = INDENT_SIZE - 1
- res_toks: TokenStream = []
- for i, (line, (new_line, err)) in enumerate(lines, lnum - index):
- if not err:
- line = new_line
- colored_line = line
- if i == lnum:
- # This is the line with the error
- pad = numbers_width - len(str(i))
- line_toks = [
- (Token.LinenoEm, theme.make_arrow(pad)),
- (Token.LinenoEm, str(lnum)),
- (Token, " "),
- (Token, colored_line),
- ]
- else:
- padding_num = "%*s" % (numbers_width, i)
- line_toks = [
- (Token.LinenoEm, padding_num),
- (Token, " "),
- (Token, colored_line),
- ]
- res_toks.extend(line_toks)
- if lvals_toks and i == lnum:
- for lv in lvals_toks:
- res_toks.extend(lv)
- # res_toks.extend(lvals_toks)
- return res_toks
- class FrameInfo:
- """
- Mirror of stack data's FrameInfo, but so that we can bypass highlighting on
- really long frames.
- """
- description: Optional[str]
- filename: Optional[str]
- lineno: int
- # number of context lines to use
- context: Optional[int]
- raw_lines: list[str]
- _sd: stack_data.core.FrameInfo
- frame: Any
- @classmethod
- def _from_stack_data_FrameInfo(
- cls, frame_info: stack_data.core.FrameInfo | stack_data.core.RepeatedFrames
- ) -> "FrameInfo":
- return cls(
- getattr(frame_info, "description", None),
- getattr(frame_info, "filename", None), # type: ignore[arg-type]
- getattr(frame_info, "lineno", None), # type: ignore[arg-type]
- getattr(frame_info, "frame", None),
- getattr(frame_info, "code", None),
- sd=frame_info,
- context=None,
- )
- def __init__(
- self,
- description: Optional[str],
- filename: str,
- lineno: int,
- frame: Any,
- code: Optional[types.CodeType],
- *,
- sd: Any = None,
- context: int | None = None,
- ):
- assert isinstance(lineno, (int, type(None))), lineno
- self.description = description
- self.filename = filename
- self.lineno = lineno
- self.frame = frame
- self.code = code
- self._sd = sd
- self.context = context
- # self.lines = []
- if sd is None:
- try:
- # return a list of source lines and a starting line number
- self.raw_lines = inspect.getsourcelines(frame)[0]
- except OSError:
- self.raw_lines = [
- "'Could not get source, probably due dynamically evaluated source code.'"
- ]
- @property
- def variables_in_executing_piece(self) -> list[Any]:
- if self._sd is not None:
- return self._sd.variables_in_executing_piece # type:ignore[misc]
- else:
- return []
- @property
- def lines(self) -> list[Any]:
- from executing.executing import NotOneValueFound
- assert self._sd is not None
- try:
- return self._sd.lines # type: ignore[misc]
- except NotOneValueFound:
- class Dummy:
- lineno = 0
- is_current = False
- def render(self, *, pygmented: bool) -> str:
- return "<Error retrieving source code with stack_data see ipython/ipython#13598>"
- return [Dummy()]
- @property
- def executing(self) -> Any:
- if self._sd is not None:
- return self._sd.executing
- else:
- return None
- class TBTools:
- """Basic tools used by all traceback printer classes."""
- # Number of frames to skip when reporting tracebacks
- tb_offset = 0
- _theme_name: str
- _old_theme_name: str
- call_pdb: bool
- ostream: Any
- debugger_cls: Any
- pdb: Any
- def __init__(
- self,
- color_scheme: Any = _sentinel,
- call_pdb: bool = False,
- ostream: Any = None,
- *,
- debugger_cls: type | None = None,
- theme_name: str = "nocolor",
- ):
- if color_scheme is not _sentinel:
- assert isinstance(color_scheme, str), color_scheme
- warnings.warn(
- "color_scheme is deprecated since IPython 9.0, use theme_name instead, all lowercase",
- DeprecationWarning,
- stacklevel=2,
- )
- theme_name = color_scheme
- if theme_name in ["Linux", "LightBG", "Neutral", "NoColor"]:
- warnings.warn(
- f"Theme names and color schemes are lowercase in IPython 9.0 use {theme_name.lower()} instead",
- DeprecationWarning,
- stacklevel=2,
- )
- theme_name = theme_name.lower()
- # Whether to call the interactive pdb debugger after printing
- # tracebacks or not
- super().__init__()
- self.call_pdb = call_pdb
- # Output stream to write to. Note that we store the original value in
- # a private attribute and then make the public ostream a property, so
- # that we can delay accessing sys.stdout until runtime. The way
- # things are written now, the sys.stdout object is dynamically managed
- # so a reference to it should NEVER be stored statically. This
- # property approach confines this detail to a single location, and all
- # subclasses can simply access self.ostream for writing.
- self._ostream = ostream
- # Create color table
- self.set_theme_name(theme_name)
- self.debugger_cls = debugger_cls or debugger.Pdb
- if call_pdb:
- self.pdb = self.debugger_cls()
- else:
- self.pdb = None
- def _get_ostream(self) -> Any:
- """Output stream that exceptions are written to.
- Valid values are:
- - None: the default, which means that IPython will dynamically resolve
- to sys.stdout. This ensures compatibility with most tools, including
- Windows (where plain stdout doesn't recognize ANSI escapes).
- - Any object with 'write' and 'flush' attributes.
- """
- return sys.stdout if self._ostream is None else self._ostream
- def _set_ostream(self, val) -> None: # type:ignore[no-untyped-def]
- assert val is None or (hasattr(val, "write") and hasattr(val, "flush"))
- self._ostream = val
- ostream = property(_get_ostream, _set_ostream)
- @staticmethod
- def _get_chained_exception(exception_value: Any) -> Any:
- cause = getattr(exception_value, "__cause__", None)
- if cause:
- return cause
- if getattr(exception_value, "__suppress_context__", False):
- return None
- return getattr(exception_value, "__context__", None)
- def get_parts_of_chained_exception(
- self, evalue: BaseException | None
- ) -> Optional[Tuple[type, BaseException, TracebackType]]:
- chained_evalue = self._get_chained_exception(evalue)
- if chained_evalue:
- return (
- chained_evalue.__class__,
- chained_evalue,
- chained_evalue.__traceback__,
- )
- return None
- def prepare_chained_exception_message(
- self, cause: BaseException | None
- ) -> list[list[str]]:
- direct_cause = (
- "\nThe above exception was the direct cause of the following exception:\n"
- )
- exception_during_handling = (
- "\nDuring handling of the above exception, another exception occurred:\n"
- )
- if cause:
- message = [[direct_cause]]
- else:
- message = [[exception_during_handling]]
- return message
- @property
- def has_colors(self) -> bool:
- assert self._theme_name == self._theme_name.lower()
- return self._theme_name != "nocolor"
- def set_theme_name(self, name: str) -> None:
- assert name in theme_table
- assert name.lower() == name
- self._theme_name = name
- # Also set colors of debugger
- if hasattr(self, "pdb") and self.pdb is not None:
- self.pdb.set_theme_name(name)
- def set_colors(self, name: str) -> None:
- """Shorthand access to the color table scheme selector method."""
- # todo emit deprecation
- warnings.warn(
- "set_colors is deprecated since IPython 9.0, use set_theme_name instead",
- DeprecationWarning,
- stacklevel=2,
- )
- self.set_theme_name(name)
- def color_toggle(self) -> None:
- """Toggle between the currently active color scheme and nocolor."""
- if self._theme_name == "nocolor":
- self._theme_name = self._old_theme_name
- else:
- self._old_theme_name = self._theme_name
- self._theme_name = "nocolor"
- def stb2text(self, stb: list[str]) -> str:
- """Convert a structured traceback (a list) to a string."""
- return "\n".join(stb)
- def text(
- self,
- etype: type,
- value: BaseException | None,
- tb: TracebackType | None,
- tb_offset: Optional[int] = None,
- context: int = 5,
- ) -> str:
- """Return formatted traceback.
- Subclasses may override this if they add extra arguments.
- """
- tb_list = self.structured_traceback(etype, value, tb, tb_offset, context)
- return self.stb2text(tb_list)
- def structured_traceback(
- self,
- etype: type,
- evalue: BaseException | None,
- etb: Optional[TracebackType] = None,
- tb_offset: Optional[int] = None,
- context: int = 5,
- ) -> list[str]:
- """Return a list of traceback frames.
- Must be implemented by each class.
- """
- raise NotImplementedError()
|