| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573 |
- import builtins
- import inspect
- import io
- import keyword
- import linecache
- import os
- import re
- import sys
- import sysconfig
- import tokenize
- import traceback
- if sys.version_info >= (3, 11):
- def is_exception_group(exc):
- return isinstance(exc, ExceptionGroup)
- else:
- try:
- from exceptiongroup import ExceptionGroup
- except ImportError:
- def is_exception_group(exc):
- return False
- else:
- def is_exception_group(exc):
- return isinstance(exc, ExceptionGroup)
- class SyntaxHighlighter:
- _default_style = frozenset(
- {
- "comment": "\x1b[30m\x1b[1m{}\x1b[0m",
- "keyword": "\x1b[35m\x1b[1m{}\x1b[0m",
- "builtin": "\x1b[1m{}\x1b[0m",
- "string": "\x1b[36m{}\x1b[0m",
- "number": "\x1b[34m\x1b[1m{}\x1b[0m",
- "operator": "\x1b[35m\x1b[1m{}\x1b[0m",
- "punctuation": "\x1b[1m{}\x1b[0m",
- "constant": "\x1b[36m\x1b[1m{}\x1b[0m",
- "identifier": "\x1b[1m{}\x1b[0m",
- "other": "{}",
- }.items()
- )
- _builtins = frozenset(dir(builtins))
- _constants = frozenset({"True", "False", "None"})
- _punctuation = frozenset({"(", ")", "[", "]", "{", "}", ":", ",", ";"})
- if sys.version_info >= (3, 12):
- _strings = frozenset(
- {tokenize.STRING, tokenize.FSTRING_START, tokenize.FSTRING_MIDDLE, tokenize.FSTRING_END}
- )
- _fstring_middle = tokenize.FSTRING_MIDDLE
- else:
- _strings = frozenset({tokenize.STRING})
- _fstring_middle = None
- def __init__(self, style=None):
- self._style = style or dict(self._default_style)
- def highlight(self, source):
- style = self._style
- row, column = 0, 0
- output = ""
- for token in self.tokenize(source):
- type_, string, (start_row, start_column), (_, end_column), line = token
- if type_ == self._fstring_middle:
- # When an f-string contains "{{" or "}}", they appear as "{" or "}" in the "string"
- # attribute of the token. However, they do not count in the column position.
- end_column += string.count("{") + string.count("}")
- if type_ == tokenize.NAME:
- if string in self._constants:
- color = style["constant"]
- elif keyword.iskeyword(string):
- color = style["keyword"]
- elif string in self._builtins:
- color = style["builtin"]
- else:
- color = style["identifier"]
- elif type_ == tokenize.OP:
- if string in self._punctuation:
- color = style["punctuation"]
- else:
- color = style["operator"]
- elif type_ == tokenize.NUMBER:
- color = style["number"]
- elif type_ in self._strings:
- color = style["string"]
- elif type_ == tokenize.COMMENT:
- color = style["comment"]
- else:
- color = style["other"]
- if start_row != row:
- source = source[column:]
- row, column = start_row, 0
- if type_ != tokenize.ENCODING:
- output += line[column:start_column]
- output += color.format(line[start_column:end_column])
- column = end_column
- output += source[column:]
- return output
- @staticmethod
- def tokenize(source):
- # Worth reading: https://www.asmeurer.com/brown-water-python/
- source = source.encode("utf-8")
- source = io.BytesIO(source)
- try:
- yield from tokenize.tokenize(source.readline)
- except tokenize.TokenError:
- return
- class ExceptionFormatter:
- _default_theme = frozenset(
- {
- "introduction": "\x1b[33m\x1b[1m{}\x1b[0m",
- "cause": "\x1b[1m{}\x1b[0m",
- "context": "\x1b[1m{}\x1b[0m",
- "dirname": "\x1b[32m{}\x1b[0m",
- "basename": "\x1b[32m\x1b[1m{}\x1b[0m",
- "line": "\x1b[33m{}\x1b[0m",
- "function": "\x1b[35m{}\x1b[0m",
- "exception_type": "\x1b[31m\x1b[1m{}\x1b[0m",
- "exception_value": "\x1b[1m{}\x1b[0m",
- "arrows": "\x1b[36m{}\x1b[0m",
- "value": "\x1b[36m\x1b[1m{}\x1b[0m",
- }.items()
- )
- def __init__(
- self,
- colorize=False,
- backtrace=False,
- diagnose=True,
- theme=None,
- style=None,
- max_length=128,
- encoding="ascii",
- hidden_frames_filename=None,
- prefix="",
- ):
- self._colorize = colorize
- self._diagnose = diagnose
- self._theme = theme or dict(self._default_theme)
- self._backtrace = backtrace
- self._syntax_highlighter = SyntaxHighlighter(style)
- self._max_length = max_length
- self._encoding = encoding
- self._hidden_frames_filename = hidden_frames_filename
- self._prefix = prefix
- self._lib_dirs = self._get_lib_dirs()
- self._pipe_char = self._get_char("\u2502", "|")
- self._cap_char = self._get_char("\u2514", "->")
- self._catch_point_identifier = " <Loguru catch point here>"
- @staticmethod
- def _get_lib_dirs():
- schemes = sysconfig.get_scheme_names()
- names = ["stdlib", "platstdlib", "platlib", "purelib"]
- paths = {sysconfig.get_path(name, scheme) for scheme in schemes for name in names}
- return [os.path.abspath(path).lower() + os.sep for path in paths if path in sys.path]
- @staticmethod
- def _indent(text, count, *, prefix="| "):
- if count == 0:
- yield text
- return
- for line in text.splitlines(True):
- indented = " " * count + prefix + line
- yield indented.rstrip() + "\n"
- def _get_char(self, char, default):
- try:
- char.encode(self._encoding)
- except (UnicodeEncodeError, LookupError):
- return default
- else:
- return char
- def _is_file_mine(self, file):
- filepath = os.path.abspath(file).lower()
- if not filepath.endswith(".py"):
- return False
- return not any(filepath.startswith(d) for d in self._lib_dirs)
- def _extract_frames(self, tb, is_first, *, limit=None, from_decorator=False):
- frames, final_source = [], None
- if tb is None or (limit is not None and limit <= 0):
- return frames, final_source
- def is_valid(frame):
- return frame.f_code.co_filename != self._hidden_frames_filename
- def get_info(frame, lineno):
- filename = frame.f_code.co_filename
- function = frame.f_code.co_name
- source = linecache.getline(filename, lineno).strip()
- return filename, lineno, function, source
- infos = []
- if is_valid(tb.tb_frame):
- infos.append((get_info(tb.tb_frame, tb.tb_lineno), tb.tb_frame))
- get_parent_only = from_decorator and not self._backtrace
- if (self._backtrace and is_first) or get_parent_only:
- frame = tb.tb_frame.f_back
- while frame:
- if is_valid(frame):
- infos.insert(0, (get_info(frame, frame.f_lineno), frame))
- if get_parent_only:
- break
- frame = frame.f_back
- if infos and not get_parent_only:
- (filename, lineno, function, source), frame = infos[-1]
- function += self._catch_point_identifier
- infos[-1] = ((filename, lineno, function, source), frame)
- tb = tb.tb_next
- while tb:
- if is_valid(tb.tb_frame):
- infos.append((get_info(tb.tb_frame, tb.tb_lineno), tb.tb_frame))
- tb = tb.tb_next
- if limit is not None:
- infos = infos[-limit:]
- for (filename, lineno, function, source), frame in infos:
- final_source = source
- if source:
- colorize = self._colorize and self._is_file_mine(filename)
- lines = []
- if colorize:
- lines.append(self._syntax_highlighter.highlight(source))
- else:
- lines.append(source)
- if self._diagnose:
- relevant_values = self._get_relevant_values(source, frame)
- values = self._format_relevant_values(list(relevant_values), colorize)
- lines += list(values)
- source = "\n ".join(lines)
- frames.append((filename, lineno, function, source))
- return frames, final_source
- def _get_relevant_values(self, source, frame):
- value = None
- pending = None
- is_attribute = False
- is_valid_value = False
- is_assignment = True
- for token in self._syntax_highlighter.tokenize(source):
- type_, string, (_, col), *_ = token
- if pending is not None:
- # Keyword arguments are ignored
- if type_ != tokenize.OP or string != "=" or is_assignment:
- yield pending
- pending = None
- if type_ == tokenize.NAME and not keyword.iskeyword(string):
- if not is_attribute:
- for variables in (frame.f_locals, frame.f_globals):
- try:
- value = variables[string]
- except KeyError:
- continue
- else:
- is_valid_value = True
- pending = (col, self._format_value(value))
- break
- elif is_valid_value:
- try:
- value = inspect.getattr_static(value, string)
- except AttributeError:
- is_valid_value = False
- else:
- yield (col, self._format_value(value))
- elif type_ == tokenize.OP and string == ".":
- is_attribute = True
- is_assignment = False
- elif type_ == tokenize.OP and string == ";":
- is_assignment = True
- is_attribute = False
- is_valid_value = False
- else:
- is_attribute = False
- is_valid_value = False
- is_assignment = False
- if pending is not None:
- yield pending
- def _format_relevant_values(self, relevant_values, colorize):
- for i in reversed(range(len(relevant_values))):
- col, value = relevant_values[i]
- pipe_cols = [pcol for pcol, _ in relevant_values[:i]]
- pre_line = ""
- index = 0
- for pc in pipe_cols:
- pre_line += (" " * (pc - index)) + self._pipe_char
- index = pc + 1
- pre_line += " " * (col - index)
- value_lines = value.split("\n")
- for n, value_line in enumerate(value_lines):
- if n == 0:
- arrows = pre_line + self._cap_char + " "
- else:
- arrows = pre_line + " " * (len(self._cap_char) + 1)
- if colorize:
- arrows = self._theme["arrows"].format(arrows)
- value_line = self._theme["value"].format(value_line)
- yield arrows + value_line
- def _format_value(self, v):
- try:
- v = repr(v)
- except Exception:
- v = "<unprintable %s object>" % type(v).__name__
- max_length = self._max_length
- if max_length is not None and len(v) > max_length:
- v = v[: max_length - 3] + "..."
- return v
- def _format_locations(self, frames_lines, *, has_introduction):
- prepend_with_new_line = has_introduction
- regex = r'^ File "(?P<file>.*?)", line (?P<line>[^,]+)(?:, in (?P<function>.*))?\n'
- for frame in frames_lines:
- match = re.match(regex, frame)
- if match:
- file, line, function = match.group("file", "line", "function")
- is_mine = self._is_file_mine(file)
- if function is not None:
- pattern = ' File "{}", line {}, in {}\n'
- else:
- pattern = ' File "{}", line {}\n'
- if self._backtrace and function and function.endswith(self._catch_point_identifier):
- function = function[: -len(self._catch_point_identifier)]
- pattern = ">" + pattern[1:]
- if self._colorize and is_mine:
- dirname, basename = os.path.split(file)
- if dirname:
- dirname += os.sep
- dirname = self._theme["dirname"].format(dirname)
- basename = self._theme["basename"].format(basename)
- file = dirname + basename
- line = self._theme["line"].format(line)
- function = self._theme["function"].format(function)
- if self._diagnose and (is_mine or prepend_with_new_line):
- pattern = "\n" + pattern
- location = pattern.format(file, line, function)
- frame = location + frame[match.end() :]
- prepend_with_new_line = is_mine
- yield frame
- def _format_exception(
- self, value, tb, *, seen=None, is_first=False, from_decorator=False, group_nesting=0
- ):
- # Implemented from built-in traceback module:
- # https://github.com/python/cpython/blob/a5b76167/Lib/traceback.py#L468
- exc_type, exc_value, exc_traceback = type(value), value, tb
- if seen is None:
- seen = set()
- seen.add(id(exc_value))
- if exc_value:
- if exc_value.__cause__ is not None and id(exc_value.__cause__) not in seen:
- yield from self._format_exception(
- exc_value.__cause__,
- exc_value.__cause__.__traceback__,
- seen=seen,
- group_nesting=group_nesting,
- )
- cause = "The above exception was the direct cause of the following exception:"
- if self._colorize:
- cause = self._theme["cause"].format(cause)
- if self._diagnose:
- yield from self._indent("\n\n" + cause + "\n\n\n", group_nesting)
- else:
- yield from self._indent("\n" + cause + "\n\n", group_nesting)
- elif (
- exc_value.__context__ is not None
- and id(exc_value.__context__) not in seen
- and not exc_value.__suppress_context__
- ):
- yield from self._format_exception(
- exc_value.__context__,
- exc_value.__context__.__traceback__,
- seen=seen,
- group_nesting=group_nesting,
- )
- context = "During handling of the above exception, another exception occurred:"
- if self._colorize:
- context = self._theme["context"].format(context)
- if self._diagnose:
- yield from self._indent("\n\n" + context + "\n\n\n", group_nesting)
- else:
- yield from self._indent("\n" + context + "\n\n", group_nesting)
- is_grouped = is_exception_group(value)
- if is_grouped and group_nesting == 0:
- yield from self._format_exception(
- value,
- tb,
- seen=seen,
- group_nesting=1,
- is_first=is_first,
- from_decorator=from_decorator,
- )
- return
- try:
- traceback_limit = sys.tracebacklimit
- except AttributeError:
- traceback_limit = None
- frames, final_source = self._extract_frames(
- exc_traceback, is_first, limit=traceback_limit, from_decorator=from_decorator
- )
- exception_only = traceback.format_exception_only(exc_type, exc_value)
- # Determining the correct index for the "Exception: message" part in the formatted exception
- # is challenging. This is because it might be preceded by multiple lines specific to
- # "SyntaxError" or followed by various notes. However, we can make an educated guess based
- # on the indentation; the preliminary context for "SyntaxError" is always indented, while
- # the Exception itself is not. This allows us to identify the correct index for the
- # exception message.
- no_indented_indexes = (i for i, p in enumerate(exception_only) if not p.startswith(" "))
- error_message_index = next(no_indented_indexes, None)
- if error_message_index is not None:
- # Remove final new line temporarily.
- error_message = exception_only[error_message_index][:-1]
- if self._colorize:
- if ":" in error_message:
- exception_type, exception_value = error_message.split(":", 1)
- exception_type = self._theme["exception_type"].format(exception_type)
- exception_value = self._theme["exception_value"].format(exception_value)
- error_message = exception_type + ":" + exception_value
- else:
- error_message = self._theme["exception_type"].format(error_message)
- if self._diagnose and frames:
- if issubclass(exc_type, AssertionError) and not str(exc_value) and final_source:
- if self._colorize:
- final_source = self._syntax_highlighter.highlight(final_source)
- error_message += ": " + final_source
- error_message = "\n" + error_message
- exception_only[error_message_index] = error_message + "\n"
- if is_first:
- yield self._prefix
- has_introduction = bool(frames)
- if has_introduction:
- if is_grouped:
- introduction = "Exception Group Traceback (most recent call last):"
- else:
- introduction = "Traceback (most recent call last):"
- if self._colorize:
- introduction = self._theme["introduction"].format(introduction)
- if group_nesting == 1: # Implies we're processing the root ExceptionGroup.
- yield from self._indent(introduction + "\n", group_nesting, prefix="+ ")
- else:
- yield from self._indent(introduction + "\n", group_nesting)
- frames_lines = self._format_list(frames) + exception_only
- if self._colorize or self._backtrace or self._diagnose:
- frames_lines = self._format_locations(frames_lines, has_introduction=has_introduction)
- yield from self._indent("".join(frames_lines), group_nesting)
- if is_grouped:
- exc = None
- for n, exc in enumerate(value.exceptions, start=1):
- ruler = "+" + (" %s " % ("..." if n > 15 else n)).center(35, "-")
- yield from self._indent(ruler, group_nesting, prefix="+-" if n == 1 else " ")
- if n > 15:
- message = "and %d more exceptions\n" % (len(value.exceptions) - 15)
- yield from self._indent(message, group_nesting + 1)
- break
- elif group_nesting == 10 and is_exception_group(exc):
- message = "... (max_group_depth is 10)\n"
- yield from self._indent(message, group_nesting + 1)
- else:
- yield from self._format_exception(
- exc,
- exc.__traceback__,
- seen=seen,
- group_nesting=group_nesting + 1,
- )
- if not is_exception_group(exc) or group_nesting == 10:
- yield from self._indent("-" * 35, group_nesting + 1, prefix="+-")
- def _format_list(self, frames):
- def source_message(filename, lineno, name, line):
- message = ' File "%s", line %d, in %s\n' % (filename, lineno, name)
- if line:
- message += " %s\n" % line.strip()
- return message
- def skip_message(count):
- plural = "s" if count > 1 else ""
- return " [Previous line repeated %d more time%s]\n" % (count, plural)
- result = []
- count = 0
- last_source = None
- for *source, line in frames:
- if source != last_source and count > 3:
- result.append(skip_message(count - 3))
- if source == last_source:
- count += 1
- if count > 3:
- continue
- else:
- count = 1
- result.append(source_message(*source, line))
- last_source = source
- # Add a final skip message if the iteration of frames ended mid-repetition.
- if count > 3:
- result.append(skip_message(count - 3))
- return result
- def format_exception(self, type_, value, tb, *, from_decorator=False):
- yield from self._format_exception(value, tb, is_first=True, from_decorator=from_decorator)
|