|
import inspect |
|
import logging |
|
import sys |
|
import traceback |
|
from collections import Counter |
|
from html import escape as escape_html |
|
from types import FrameType, TracebackType |
|
from typing import Union, Iterable, List |
|
|
|
from stack_data import ( |
|
style_with_executing_node, |
|
Options, |
|
Line, |
|
FrameInfo, |
|
Variable, |
|
RepeatedFrames, |
|
) |
|
from stack_data.utils import some_str |
|
|
|
log = logging.getLogger(__name__) |
|
|
|
|
|
class Serializer: |
|
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", |
|
use_code_qualname=True, |
|
strip_leading_indent=True, |
|
html=False, |
|
chain=True, |
|
collapse_repeated_frames=True, |
|
show_variables=False, |
|
): |
|
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: |
|
if html: |
|
from pygments.formatters.html import ( |
|
HtmlFormatter as pygments_formatter_cls, |
|
) |
|
else: |
|
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.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 |
|
self.show_variables = show_variables |
|
|
|
def format_exception(self, e=None) -> List[dict]: |
|
if e is None: |
|
e = sys.exc_info()[1] |
|
|
|
result = [] |
|
|
|
if self.chain: |
|
if e.__cause__ is not None: |
|
result = self.format_exception(e.__cause__) |
|
result[-1]["tail"] = traceback._cause_message.strip() |
|
elif e.__context__ is not None and not e.__suppress_context__: |
|
result = self.format_exception(e.__context__) |
|
result[-1]["tail"] = traceback._context_message.strip() |
|
|
|
result.append(self.format_traceback_part(e)) |
|
return result |
|
|
|
def format_traceback_part(self, e: BaseException) -> dict: |
|
return dict( |
|
frames=self.format_stack(e.__traceback__ or sys.exc_info()[2]), |
|
exception=dict( |
|
type=type(e).__name__, |
|
message=some_str(e), |
|
), |
|
tail="", |
|
) |
|
|
|
def format_stack(self, frame_or_tb=None) -> List[dict]: |
|
if frame_or_tb is None: |
|
frame_or_tb = inspect.currentframe().f_back |
|
|
|
return list( |
|
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[dict]: |
|
for item in stack: |
|
if isinstance(item, FrameInfo): |
|
if not self.should_include_frame(item): |
|
continue |
|
yield dict(type="frame", **self.format_frame(item)) |
|
else: |
|
yield dict(type="repeated_frames", **self.format_repeated_frames(item)) |
|
|
|
def format_repeated_frames(self, repeated_frames: RepeatedFrames) -> dict: |
|
counts = sorted( |
|
Counter(repeated_frames.frame_keys).items(), |
|
key=lambda item: (-item[1], item[0][0].co_name), |
|
) |
|
return dict( |
|
frames=[ |
|
dict( |
|
name=code.co_name, |
|
lineno=lineno, |
|
count=count, |
|
) |
|
for (code, lineno), count in counts |
|
] |
|
) |
|
|
|
def format_frame(self, frame: Union[FrameInfo, FrameType, TracebackType]) -> dict: |
|
if not isinstance(frame, FrameInfo): |
|
frame = FrameInfo(frame, self.options) |
|
|
|
result = dict( |
|
name=( |
|
frame.executing.code_qualname() |
|
if self.use_code_qualname |
|
else frame.code.co_name |
|
), |
|
filename=frame.filename, |
|
lineno=frame.lineno, |
|
lines=list(self.format_lines(frame.lines)), |
|
) |
|
if self.show_variables: |
|
result["variables"] = list(self.format_variables(frame)) |
|
return result |
|
|
|
def format_lines(self, lines): |
|
for line in lines: |
|
if isinstance(line, Line): |
|
yield dict(type="line", **self.format_line(line)) |
|
else: |
|
yield dict(type="line_gap") |
|
|
|
def format_line(self, line: Line) -> dict: |
|
return dict( |
|
is_current=line.is_current, |
|
lineno=line.lineno, |
|
text=line.render( |
|
pygmented=self.pygmented, |
|
escape_html=self.html, |
|
strip_leading_indent=self.strip_leading_indent, |
|
), |
|
) |
|
|
|
def format_variables(self, frame_info: FrameInfo) -> Iterable[dict]: |
|
try: |
|
for var in sorted(frame_info.variables, key=lambda v: v.name): |
|
yield self.format_variable(var) |
|
except Exception: |
|
log.exception("Error in getting frame variables") |
|
|
|
def format_variable(self, var: Variable) -> dict: |
|
return dict( |
|
name=self.format_variable_part(var.name), |
|
value=self.format_variable_part(self.format_variable_value(var.value)), |
|
) |
|
|
|
def format_variable_part(self, text): |
|
if self.html: |
|
return escape_html(text) |
|
else: |
|
return text |
|
|
|
def format_variable_value(self, value) -> str: |
|
return repr(value) |
|
|
|
def should_include_frame(self, frame_info: FrameInfo) -> bool: |
|
return True |
|
|