235 lines
8.3 KiB
Python
235 lines
8.3 KiB
Python
|
import inspect
|
||
|
import sys
|
||
|
import traceback
|
||
|
from types import FrameType, TracebackType
|
||
|
from typing import Union, Iterable
|
||
|
|
||
|
from stack_data import (style_with_executing_node, Options, Line, FrameInfo, LINE_GAP,
|
||
|
Variable, RepeatedFrames, BlankLineRange, BlankLines)
|
||
|
from stack_data.utils import assert_
|
||
|
|
||
|
|
||
|
class Formatter:
|
||
|
def __init__(
|
||
|
self, *,
|
||
|
options=None,
|
||
|
pygmented=False,
|
||
|
show_executing_node=True,
|
||
|
pygments_formatter_cls=None,
|
||
|
pygments_formatter_kwargs=None,
|
||
|
pygments_style="monokai",
|
||
|
executing_node_modifier="bg:#005080",
|
||
|
executing_node_underline="^",
|
||
|
current_line_indicator="-->",
|
||
|
line_gap_string="(...)",
|
||
|
line_number_gap_string=":",
|
||
|
line_number_format_string="{:4} | ",
|
||
|
show_variables=False,
|
||
|
use_code_qualname=True,
|
||
|
show_linenos=True,
|
||
|
strip_leading_indent=True,
|
||
|
html=False,
|
||
|
chain=True,
|
||
|
collapse_repeated_frames=True
|
||
|
):
|
||
|
if options is None:
|
||
|
options = Options()
|
||
|
|
||
|
if pygmented and not options.pygments_formatter:
|
||
|
if show_executing_node:
|
||
|
pygments_style = style_with_executing_node(
|
||
|
pygments_style, executing_node_modifier
|
||
|
)
|
||
|
|
||
|
if pygments_formatter_cls is None:
|
||
|
from pygments.formatters.terminal256 import Terminal256Formatter \
|
||
|
as pygments_formatter_cls
|
||
|
|
||
|
options.pygments_formatter = pygments_formatter_cls(
|
||
|
style=pygments_style,
|
||
|
**pygments_formatter_kwargs or {},
|
||
|
)
|
||
|
|
||
|
self.pygmented = pygmented
|
||
|
self.show_executing_node = show_executing_node
|
||
|
assert_(
|
||
|
len(executing_node_underline) == 1,
|
||
|
ValueError("executing_node_underline must be a single character"),
|
||
|
)
|
||
|
self.executing_node_underline = executing_node_underline
|
||
|
self.current_line_indicator = current_line_indicator or ""
|
||
|
self.line_gap_string = line_gap_string
|
||
|
self.line_number_gap_string = line_number_gap_string
|
||
|
self.line_number_format_string = line_number_format_string
|
||
|
self.show_variables = show_variables
|
||
|
self.show_linenos = show_linenos
|
||
|
self.use_code_qualname = use_code_qualname
|
||
|
self.strip_leading_indent = strip_leading_indent
|
||
|
self.html = html
|
||
|
self.chain = chain
|
||
|
self.options = options
|
||
|
self.collapse_repeated_frames = collapse_repeated_frames
|
||
|
if not self.show_linenos and self.options.blank_lines == BlankLines.SINGLE:
|
||
|
raise ValueError(
|
||
|
"BlankLines.SINGLE option can only be used when show_linenos=True"
|
||
|
)
|
||
|
|
||
|
def set_hook(self):
|
||
|
def excepthook(_etype, evalue, _tb):
|
||
|
self.print_exception(evalue)
|
||
|
|
||
|
sys.excepthook = excepthook
|
||
|
|
||
|
def print_exception(self, e=None, *, file=None):
|
||
|
self.print_lines(self.format_exception(e), file=file)
|
||
|
|
||
|
def print_stack(self, frame_or_tb=None, *, file=None):
|
||
|
if frame_or_tb is None:
|
||
|
frame_or_tb = inspect.currentframe().f_back
|
||
|
|
||
|
self.print_lines(self.format_stack(frame_or_tb), file=file)
|
||
|
|
||
|
def print_lines(self, lines, *, file=None):
|
||
|
if file is None:
|
||
|
file = sys.stderr
|
||
|
for line in lines:
|
||
|
print(line, file=file, end="")
|
||
|
|
||
|
def format_exception(self, e=None) -> Iterable[str]:
|
||
|
if e is None:
|
||
|
e = sys.exc_info()[1]
|
||
|
|
||
|
if self.chain:
|
||
|
if e.__cause__ is not None:
|
||
|
yield from self.format_exception(e.__cause__)
|
||
|
yield traceback._cause_message
|
||
|
elif (e.__context__ is not None
|
||
|
and not e.__suppress_context__):
|
||
|
yield from self.format_exception(e.__context__)
|
||
|
yield traceback._context_message
|
||
|
|
||
|
yield 'Traceback (most recent call last):\n'
|
||
|
yield from self.format_stack(e.__traceback__)
|
||
|
yield from traceback.format_exception_only(type(e), e)
|
||
|
|
||
|
def format_stack(self, frame_or_tb=None) -> Iterable[str]:
|
||
|
if frame_or_tb is None:
|
||
|
frame_or_tb = inspect.currentframe().f_back
|
||
|
|
||
|
yield from self.format_stack_data(
|
||
|
FrameInfo.stack_data(
|
||
|
frame_or_tb,
|
||
|
self.options,
|
||
|
collapse_repeated_frames=self.collapse_repeated_frames,
|
||
|
)
|
||
|
)
|
||
|
|
||
|
def format_stack_data(
|
||
|
self, stack: Iterable[Union[FrameInfo, RepeatedFrames]]
|
||
|
) -> Iterable[str]:
|
||
|
for item in stack:
|
||
|
if isinstance(item, FrameInfo):
|
||
|
yield from self.format_frame(item)
|
||
|
else:
|
||
|
yield self.format_repeated_frames(item)
|
||
|
|
||
|
def format_repeated_frames(self, repeated_frames: RepeatedFrames) -> str:
|
||
|
return ' [... skipping similar frames: {}]\n'.format(
|
||
|
repeated_frames.description
|
||
|
)
|
||
|
|
||
|
def format_frame(self, frame: Union[FrameInfo, FrameType, TracebackType]) -> Iterable[str]:
|
||
|
if not isinstance(frame, FrameInfo):
|
||
|
frame = FrameInfo(frame, self.options)
|
||
|
|
||
|
yield self.format_frame_header(frame)
|
||
|
|
||
|
for line in frame.lines:
|
||
|
if isinstance(line, Line):
|
||
|
yield self.format_line(line)
|
||
|
elif isinstance(line, BlankLineRange):
|
||
|
yield self.format_blank_lines_linenumbers(line)
|
||
|
else:
|
||
|
assert_(line is LINE_GAP)
|
||
|
yield self.line_gap_string + "\n"
|
||
|
|
||
|
if self.show_variables:
|
||
|
try:
|
||
|
yield from self.format_variables(frame)
|
||
|
except Exception:
|
||
|
pass
|
||
|
|
||
|
def format_frame_header(self, frame_info: FrameInfo) -> str:
|
||
|
return ' File "{frame_info.filename}", line {frame_info.lineno}, in {name}\n'.format(
|
||
|
frame_info=frame_info,
|
||
|
name=(
|
||
|
frame_info.executing.code_qualname()
|
||
|
if self.use_code_qualname else
|
||
|
frame_info.code.co_name
|
||
|
),
|
||
|
)
|
||
|
|
||
|
def format_line(self, line: Line) -> str:
|
||
|
result = ""
|
||
|
if self.current_line_indicator:
|
||
|
if line.is_current:
|
||
|
result = self.current_line_indicator
|
||
|
else:
|
||
|
result = " " * len(self.current_line_indicator)
|
||
|
result += " "
|
||
|
else:
|
||
|
result = " "
|
||
|
|
||
|
if self.show_linenos:
|
||
|
result += self.line_number_format_string.format(line.lineno)
|
||
|
|
||
|
prefix = result
|
||
|
|
||
|
result += line.render(
|
||
|
pygmented=self.pygmented,
|
||
|
escape_html=self.html,
|
||
|
strip_leading_indent=self.strip_leading_indent,
|
||
|
) + "\n"
|
||
|
|
||
|
if self.show_executing_node and not self.pygmented:
|
||
|
for line_range in line.executing_node_ranges:
|
||
|
start = line_range.start - line.leading_indent
|
||
|
end = line_range.end - line.leading_indent
|
||
|
# if end <= start, we have an empty line inside a highlighted
|
||
|
# block of code. In this case, we need to avoid inserting
|
||
|
# an extra blank line with no markers present.
|
||
|
if end > start:
|
||
|
result += (
|
||
|
" " * (start + len(prefix))
|
||
|
+ self.executing_node_underline * (end - start)
|
||
|
+ "\n"
|
||
|
)
|
||
|
return result
|
||
|
|
||
|
|
||
|
def format_blank_lines_linenumbers(self, blank_line):
|
||
|
if self.current_line_indicator:
|
||
|
result = " " * len(self.current_line_indicator) + " "
|
||
|
else:
|
||
|
result = " "
|
||
|
if blank_line.begin_lineno == blank_line.end_lineno:
|
||
|
return result + self.line_number_format_string.format(blank_line.begin_lineno) + "\n"
|
||
|
return result + " {}\n".format(self.line_number_gap_string)
|
||
|
|
||
|
|
||
|
def format_variables(self, frame_info: FrameInfo) -> Iterable[str]:
|
||
|
for var in sorted(frame_info.variables, key=lambda v: v.name):
|
||
|
try:
|
||
|
yield self.format_variable(var) + "\n"
|
||
|
except Exception:
|
||
|
pass
|
||
|
|
||
|
def format_variable(self, var: Variable) -> str:
|
||
|
return "{} = {}".format(
|
||
|
var.name,
|
||
|
self.format_variable_value(var.value),
|
||
|
)
|
||
|
|
||
|
def format_variable_value(self, value) -> str:
|
||
|
return repr(value)
|