Commit b96519f0 authored by Lucas Felix's avatar Lucas Felix
Browse files

Use a pygments lexer to format errors in repl

parent 30395cf2
import os
import sys
import importlib
import traceback
from datetime import datetime
from textwrap import indent
from prompt_toolkit.formatted_text import PygmentsTokens
from prompt_toolkit import print_formatted_text
from pygments import lex
from pygments.lexer import RegexLexer, bygroups, default
from pygments.token import Token
class BlissTracebackLexer(RegexLexer):
"""Inspired by pygments.PythonTracebackLexer to colorize tracebacks, but for BlissTraceback
as well (it handles locals and timestamp)"""
name = "BlissTraceback"
tokens = {
"root": [
(r"\n", Token.Text),
(r"\*[^\n]+\n", Token.Comment.Preproc),
(r"^[0-9]+\/[0-9]+\/[0-9]+ [0-9]+:[0-9]+:[0-9]+\n", Token.Name.Property),
(r"^Traceback \(most recent call last\):\n", Token.Text, "intb"),
(
r"^During handling of the above exception, another exception occurred:\n\n",
Token.Heading,
),
(
r"^The above exception was the direct cause of the following exception:\n\n",
Token.Heading,
),
(r'^(?= File "[^"]+", line \d+)', Token.Generic.Traceback, "intb"),
(r"^.*\n", Token.Other),
],
"intb": [
(
r'^( File )("[^"]+")(, line )(\d+)(, in )(.+)(\n)',
bygroups(
Token.Text,
Token.Name.Builtin,
Token.Text,
Token.Number,
Token.Text,
Token.Name,
Token.Text,
),
),
(
r'^( File )("[^"]+")(, line )(\d+)(\n)',
bygroups(
Token.Text, Token.Name.Builtin, Token.Text, Token.Number, Token.Text
),
),
(r"^(?= @.+\n)", Token.Generic.Traceback, "markers"),
(
r"^( )(.+)(\n)",
bygroups(Token.Text, Token.Other, Token.Text),
"markers",
),
(
r"^([^:]+)(: )(.+)(\n)",
bygroups(
Token.Generic.Error, Token.Text, Token.Name.Exception, Token.Text
),
"#pop",
),
(
r"^([a-zA-Z_][\w.]*)(:?\n)",
bygroups(Token.Generic.Error, Token.Text),
"#pop",
),
],
"markers": [
(r"^( )\.\.\. \(truncated\)\n", Token.Comment.Preproc),
(
r"^( )(@[^:]+)(: )(\.\.\. \(truncated\)\n)",
bygroups(
Token.Text,
Token.Name.Variable,
Token.Punctuation,
Token.Comment.Preproc,
),
),
(
r"^( )(@[^:]+)(:)(.+)(\n)",
bygroups(
Token.Text,
Token.Name.Variable,
Token.Punctuation,
Token.Literal.String.Other,
Token.Text,
),
),
(r"^[\w]*\n", Token.Text, "#pop"),
default("#pop"),
],
}
def pprint_traceback(formatted_traceback, style):
"""Print a formatted traceback (generic Python traceback or BlissTraceback) with colors,
using BlissTracebackLexer.
"""
tokens = list(lex(formatted_traceback, lexer=BlissTracebackLexer()))
print_formatted_text(PygmentsTokens(tokens), end="", style=style)
class BlissTraceback:
"""Extract traceback content for later formatting without keeping any reference on
objects to avoid memory leaks. Then the format method can be used to produce various
formatting of the same traceback.
"""
_blacklist = [
# gevent & aiogevent module paths
os.path.dirname(importlib.util.find_spec("gevent").origin),
os.path.dirname(importlib.util.find_spec("aiogevent").origin),
# gevent compiled functions root path
"src/gevent",
]
def __init__(self, exc_type, exc_value, tb):
self._datetime = datetime.now()
# convert traceback to StackSummary to stringify references and avoid memory leak
self._locals_capture_exc = ""
try:
traceback_exc = traceback.TracebackException(
exc_type, exc_value, tb, capture_locals=True
)
except Exception as e:
# Capture_locals option fails as soon as one local's __repr__ fails.
# This will be fixed in python 3.11 with the addition of format_locals option,
# see https://github.com/python/cpython/pull/29299
# For the moment we can only disable capture_locals
self._locals_capture_exc = (
traceback.format_tb(sys.exc_info()[2], -1)[0] + f" {e}"
)
traceback_exc = traceback.TracebackException(
exc_type, exc_value, tb, capture_locals=False
)
self._exc_info = []
iter = traceback_exc
while iter is not None:
exc_type = iter.exc_type
exc_value = iter._str
stack = iter.stack
msg = ""
if iter.__cause__ is not None:
msg = "\nThe above exception was the direct cause of the following exception:\n\n"
iter = iter.__cause__
elif iter.__context__ is not None:
msg = "\nDuring handling of the above exception, another exception occurred:\n\n"
iter = iter.__context__
else:
iter = None
self._exc_info.insert(0, (exc_type, exc_value, stack, msg))
def _is_file_blacklisted(self, filename):
for black_path in BlissTraceback._blacklist:
if filename.startswith(black_path):
return True
return False
def _format_stack(
self,
exc_type,
exc_value,
stack,
msg,
disable_blacklist,
max_nb_locals,
max_local_len,
):
text = ""
for frame in stack:
if not disable_blacklist and self._is_file_blacklisted(frame.filename):
continue
# skip bottom calls from ptpython
if frame.filename == "<stdin>":
text = ""
text += f' File "{frame.filename}", line {frame.lineno}, in {frame.name}\n'
if frame._line:
text += f" {frame._line}\n"
if frame.locals is not None:
for i, (key, val) in enumerate(sorted(frame.locals.items())):
if not (max_nb_locals < 0) and i + 1 > max_nb_locals:
text += " ... (truncated)\n"
break
if len(val) <= max_local_len or max_local_len < 0:
text += f" @{key}: {val}\n"
else:
text += f" @{key}: ... (truncated)\n"
text += f"{exc_type.__name__}: {exc_value}\n"
return msg + "Traceback (most recent call last):\n" + text
def format(self, disable_blacklist=False, max_nb_locals=-1, max_local_len=-1):
timestamp = self._datetime.strftime("%d/%m/%Y %H:%M:%S")
text = timestamp + "\n"
if self._locals_capture_exc:
msg = "Can't display local variables along stack trace, "
msg += "error occured during recovery:\n"
msg += self._locals_capture_exc + "\n\n"
text += indent(msg, "* ")
# Stack traces formatting
for exc in self._exc_info:
text += self._format_stack(
*exc, disable_blacklist, max_nb_locals, max_local_len
)
return text
......@@ -9,8 +9,6 @@
import asyncio
from typing import Optional
from prompt_toolkit import print_formatted_text
from prompt_toolkit.formatted_text import FormattedText
from prompt_toolkit.styles.pygments import style_from_pygments_cls
from pygments.styles import get_style_by_name
from pygments.util import ClassNotFound
......@@ -26,10 +24,8 @@ import gevent
import signal
import logging
import platform
import importlib
from collections import deque
from datetime import datetime
from textwrap import indent
import ptpython.layout
from prompt_toolkit.patch_stdout import patch_stdout as patch_stdout_context
......@@ -49,6 +45,7 @@ from bliss.shell.cli.ptpython_statusbar_patch import NEWstatus_bar, TMUXstatus_b
from bliss.shell.bliss_banners import print_rainbow_banner
from bliss.shell.cli.protected_dict import ProtectedDict
from bliss.shell.cli.no_thread_repl import NoThreadPythonRepl
from bliss.shell.cli.formatted_traceback import BlissTraceback, pprint_traceback
from bliss.shell import standard
from bliss import set_bliss_shell_mode
......@@ -86,118 +83,6 @@ session_mdl.set_current_session = functools.partial(
# =================== ERROR REPORTING ============================
class PrettyTraceback:
_blacklist = [
# gevent & aiogevent module paths
os.path.dirname(importlib.util.find_spec("gevent").origin),
os.path.dirname(importlib.util.find_spec("aiogevent").origin),
# gevent compiled functions root path
"src/gevent",
]
def __init__(self, exc_type, exc_value, tb):
self._datetime = datetime.now()
self._locals_capture_exc = ""
# convert traceback to StackSummary to stringify references and avoid memory leak
try:
traceback_exc = traceback.TracebackException(
exc_type, exc_value, tb, capture_locals=True
)
except Exception as e:
# Capture_locals option fails as soon as one local's __repr__ fails.
# This will be fixed in python 3.11 with the addition of format_locals option,
# see https://github.com/python/cpython/pull/29299
# For the moment we can only disable capture_locals
self._locals_capture_exc = (
traceback.format_tb(sys.exc_info()[2], -1)[0] + f" {e}"
)
traceback_exc = traceback.TracebackException(
exc_type, exc_value, tb, capture_locals=False
)
self._exc_info = []
iter = traceback_exc
while iter is not None:
self._exc_info.insert(0, (iter.exc_type, iter._str, iter.stack))
iter = iter.__cause__
def _is_blacklisted(self, filename):
for black_path in PrettyTraceback._blacklist:
if filename.startswith(black_path):
return True
return False
def __str__(self):
return "".join([t[-1] for t in self._format()])
def print_formatted(self, disable_blacklist=False):
print_formatted_text(
self._format(disable_blacklist),
style=BlissRepl()._current_style,
file=sys.stderr,
)
def _format_stack(self, exc_type, exc_value, stack, disable_blacklist=False):
tokens = []
for frame in stack:
if not disable_blacklist and self._is_blacklisted(frame.filename):
continue
# skip bottom calls from ptpython
if frame.filename == "<stdin>":
tokens.clear()
tokens += [
("class:pygments.literal", frame.filename),
("class:pygments.punctuation", ":"),
("class:pygments.literal.number", str(frame.lineno) + " "),
("class:pygments.name.function", frame.name + "\n"),
]
if frame._line:
tokens += [
("class:pygments.generic.error", " > "),
("class:pygments.text", frame._line + "\n"),
]
# locals are not printed when frame._line is empty, this happens with traces coming
# from the interpreter or compiled files and locals dict is full of obscure symbols.
if frame.locals is not None:
for key, val in frame.locals.items():
tokens += [
("class:pygments.name.attribute", key),
("class:pygments.punctuation", ": "),
("class:pygments.text", val + "\n"),
]
tokens.append(("class:pygments.generic.strong", exc_type.__name__ + ":\n"))
tokens.append(("class:pygments.name.exception", str(exc_value) + "\n"))
return tokens
def _format(self, disable_blacklist=False):
timestamp = self._datetime.strftime("%d/%m/%Y %H:%M:%S")
tokens = [("class:pygments.generic.heading", f"--- {timestamp} ---\n")]
if self._locals_capture_exc:
msg = "Can't display local variables along stack trace, "
msg += "error occured during recovery:\n"
msg += self._locals_capture_exc + "\n\n"
tokens.append(("class:pygments.comment.preproc", indent(msg, "* ")))
# Stack trace formatting
for exc in self._exc_info[:-1]:
tokens += self._format_stack(*exc)
tokens.append(
(
"class:pygments.generic.traceback",
"\nThe above exception was the direct cause of the following exception:\n\n",
)
)
tokens += self._format_stack(*self._exc_info[-1])
return FormattedText(tokens)
class ErrorReport(ErrorReportInterface):
"""
Manage the behavior of the error reporting in the shell.
......@@ -242,15 +127,26 @@ def install_excepthook():
# filter exceptions from aiogevent(?) with no traceback, no value
return
error_report.history.append(PrettyTraceback(exc_type, exc_value, tb))
# BlissTraceback captures traceback information without holding any reference on its content
fmt_tb = BlissTraceback(exc_type, exc_value, tb)
# python generic formatting for the exception logger
exc_text = "".join(traceback.format_exception(exc_type, exc_value, tb))
exc_logger.error(exc_text)
# store BlissTraceback for later formatting
error_report.history.append(fmt_tb)
# publish full error to logger
exc_logger.error(fmt_tb.format(disable_blacklist=True))
# Adapt the error message depending on the expert_mode
if error_report._expert_mode:
error_report.history[-1].print_formatted(disable_blacklist=True)
fmt_tb = error_report.history[-1].format(disable_blacklist=True)
try:
style = BlissRepl()._current_style
except Exception:
# BlissRepl singleton is not instantiated yet
# falling back to monochrome
print(fmt_tb)
else:
pprint_traceback(fmt_tb, style)
elif current_session:
if current_session.is_loading_config:
print(f"{exc_type.__name__}: {exc_value}", file=sys.stderr)
......@@ -798,9 +694,12 @@ def cli(
hist = user_ns["ERROR_REPORT"].history
try:
idx = -1 if index is None else index
hist[idx].print_formatted(
disable_blacklist=user_ns["ERROR_REPORT"].expert_mode
fmt_tb = hist[idx].format(
disable_blacklist=user_ns["ERROR_REPORT"].expert_mode,
max_nb_locals=15,
max_local_len=200,
)
pprint_traceback(fmt_tb, BlissRepl()._current_style)
except IndexError:
if index is None:
print("None")
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment