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