|
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: |
|
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) |
|
|