# coding: utf8
|
|
from __future__ import unicode_literals, print_function
|
|
import os
|
|
from .util import color, supports_ansi, NO_UTF8
|
|
|
|
|
|
LINE_EDGE = "└─" if not NO_UTF8 else "|_"
|
|
LINE_FORK = "├─" if not NO_UTF8 else "|__"
|
|
LINE_PATH = "──" if not NO_UTF8 else "__"
|
|
|
|
|
|
class TracebackPrinter(object):
|
|
def __init__(
|
|
self,
|
|
color_error="red",
|
|
color_tb="blue",
|
|
color_highlight="yellow",
|
|
indent=2,
|
|
tb_base=None,
|
|
tb_exclude=tuple(),
|
|
tb_range_start=-5,
|
|
tb_range_end=-2,
|
|
):
|
|
"""Initialize a traceback printer.
|
|
|
|
color_error (unicode / int): Color name or code for errors.
|
|
color_tb (unicode / int): Color name or code for traceback headline.
|
|
color_highlight (unicode / int): Color name or code for highlights.
|
|
indent (int): Indentation in spaces.
|
|
tb_base (unicode): Name of directory to use to show relative paths. For
|
|
example, "thinc" will look for the last occurence of "/thinc/" in
|
|
a path and only show path to the right of it.
|
|
tb_exclude (tuple): List of filenames to exclude from traceback.
|
|
tb_range_start (int): The starting index from a traceback to include.
|
|
tb_range_end (int): The final index from a traceback to include. If None
|
|
the traceback will continue until the last record.
|
|
RETURNS (TracebackPrinter): The traceback printer.
|
|
"""
|
|
self.color_error = color_error
|
|
self.color_tb = color_tb
|
|
self.color_highlight = color_highlight
|
|
self.indent = " " * indent
|
|
if tb_base == ".":
|
|
tb_base = "{}{}".format(os.getcwd(), os.path.sep)
|
|
elif tb_base is not None:
|
|
tb_base = "/{}/".format(tb_base)
|
|
self.tb_base = tb_base
|
|
self.tb_exclude = tuple(tb_exclude)
|
|
self.tb_range_start = tb_range_start
|
|
self.tb_range_end = tb_range_end
|
|
self.supports_ansi = supports_ansi()
|
|
|
|
def __call__(self, title, *texts, **settings):
|
|
"""Output custom formatted tracebacks and errors.
|
|
|
|
title (unicode): The message title.
|
|
*texts (unicode): The texts to print (one per line).
|
|
highlight (unicode): Optional sequence to highlight in the traceback,
|
|
e.g. the bad value that caused the error.
|
|
tb (iterable): The traceback, e.g. generated by traceback.extract_stack().
|
|
RETURNS (unicode): The formatted traceback. Can be printed or raised
|
|
by custom exception.
|
|
"""
|
|
highlight = settings.get("highlight", False)
|
|
tb = settings.get("tb", None)
|
|
if self.supports_ansi: # use first line as title
|
|
title = color(title, fg=self.color_error, bold=True)
|
|
info = "\n" + "\n".join([self.indent + text for text in texts]) if texts else ""
|
|
tb = self._get_traceback(tb, highlight) if tb else ""
|
|
msg = "\n\n{}{}{}{}\n".format(self.indent, title, info, tb)
|
|
return msg
|
|
|
|
def _get_traceback(self, tb, highlight):
|
|
# Exclude certain file names from traceback
|
|
tb = [record for record in tb if not record[0].endswith(self.tb_exclude)]
|
|
tb_range = (
|
|
tb[self.tb_range_start : self.tb_range_end]
|
|
if self.tb_range_end is not None
|
|
else tb[self.tb_range_start :]
|
|
)
|
|
tb_list = [
|
|
self._format_traceback(path, line, fn, text, i, len(tb_range), highlight)
|
|
for i, (path, line, fn, text) in enumerate(tb_range)
|
|
]
|
|
tb_data = "\n".join(tb_list).strip()
|
|
title = "Traceback:"
|
|
if self.supports_ansi:
|
|
title = color(title, fg=self.color_tb, bold=True)
|
|
return "\n\n{indent}{title}\n{indent}{tb}".format(
|
|
title=title, tb=tb_data, indent=self.indent
|
|
)
|
|
|
|
def _format_traceback(self, path, line, fn, text, i, count, highlight):
|
|
template = "{base_indent}{indent} {fn} in {path}:{line}{text}"
|
|
indent = (LINE_EDGE if i == count - 1 else LINE_FORK) + LINE_PATH * i
|
|
if self.tb_base and self.tb_base in path:
|
|
path = path.rsplit(self.tb_base, 1)[1]
|
|
text = self._format_user_error(text, i, highlight) if i == count - 1 else ""
|
|
if self.supports_ansi:
|
|
fn = color(fn, bold=True)
|
|
path = color(path, underline=True)
|
|
return template.format(
|
|
base_indent=self.indent,
|
|
line=line,
|
|
indent=indent,
|
|
text=text,
|
|
fn=fn,
|
|
path=path,
|
|
)
|
|
|
|
def _format_user_error(self, text, i, highlight):
|
|
spacing = " " * i + " >>>"
|
|
if self.supports_ansi:
|
|
spacing = color(spacing, fg=self.color_error)
|
|
if highlight and self.supports_ansi:
|
|
formatted_highlight = color(highlight, fg=self.color_highlight)
|
|
text = text.replace(highlight, formatted_highlight)
|
|
return "\n{} {} {}".format(self.indent, spacing, text)
|