You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

118 lines
4.8 KiB

4 years ago
  1. # coding: utf8
  2. from __future__ import unicode_literals, print_function
  3. import os
  4. from .util import color, supports_ansi, NO_UTF8
  5. LINE_EDGE = "└─" if not NO_UTF8 else "|_"
  6. LINE_FORK = "├─" if not NO_UTF8 else "|__"
  7. LINE_PATH = "──" if not NO_UTF8 else "__"
  8. class TracebackPrinter(object):
  9. def __init__(
  10. self,
  11. color_error="red",
  12. color_tb="blue",
  13. color_highlight="yellow",
  14. indent=2,
  15. tb_base=None,
  16. tb_exclude=tuple(),
  17. tb_range_start=-5,
  18. tb_range_end=-2,
  19. ):
  20. """Initialize a traceback printer.
  21. color_error (unicode / int): Color name or code for errors.
  22. color_tb (unicode / int): Color name or code for traceback headline.
  23. color_highlight (unicode / int): Color name or code for highlights.
  24. indent (int): Indentation in spaces.
  25. tb_base (unicode): Name of directory to use to show relative paths. For
  26. example, "thinc" will look for the last occurence of "/thinc/" in
  27. a path and only show path to the right of it.
  28. tb_exclude (tuple): List of filenames to exclude from traceback.
  29. tb_range_start (int): The starting index from a traceback to include.
  30. tb_range_end (int): The final index from a traceback to include. If None
  31. the traceback will continue until the last record.
  32. RETURNS (TracebackPrinter): The traceback printer.
  33. """
  34. self.color_error = color_error
  35. self.color_tb = color_tb
  36. self.color_highlight = color_highlight
  37. self.indent = " " * indent
  38. if tb_base == ".":
  39. tb_base = "{}{}".format(os.getcwd(), os.path.sep)
  40. elif tb_base is not None:
  41. tb_base = "/{}/".format(tb_base)
  42. self.tb_base = tb_base
  43. self.tb_exclude = tuple(tb_exclude)
  44. self.tb_range_start = tb_range_start
  45. self.tb_range_end = tb_range_end
  46. self.supports_ansi = supports_ansi()
  47. def __call__(self, title, *texts, **settings):
  48. """Output custom formatted tracebacks and errors.
  49. title (unicode): The message title.
  50. *texts (unicode): The texts to print (one per line).
  51. highlight (unicode): Optional sequence to highlight in the traceback,
  52. e.g. the bad value that caused the error.
  53. tb (iterable): The traceback, e.g. generated by traceback.extract_stack().
  54. RETURNS (unicode): The formatted traceback. Can be printed or raised
  55. by custom exception.
  56. """
  57. highlight = settings.get("highlight", False)
  58. tb = settings.get("tb", None)
  59. if self.supports_ansi: # use first line as title
  60. title = color(title, fg=self.color_error, bold=True)
  61. info = "\n" + "\n".join([self.indent + text for text in texts]) if texts else ""
  62. tb = self._get_traceback(tb, highlight) if tb else ""
  63. msg = "\n\n{}{}{}{}\n".format(self.indent, title, info, tb)
  64. return msg
  65. def _get_traceback(self, tb, highlight):
  66. # Exclude certain file names from traceback
  67. tb = [record for record in tb if not record[0].endswith(self.tb_exclude)]
  68. tb_range = (
  69. tb[self.tb_range_start : self.tb_range_end]
  70. if self.tb_range_end is not None
  71. else tb[self.tb_range_start :]
  72. )
  73. tb_list = [
  74. self._format_traceback(path, line, fn, text, i, len(tb_range), highlight)
  75. for i, (path, line, fn, text) in enumerate(tb_range)
  76. ]
  77. tb_data = "\n".join(tb_list).strip()
  78. title = "Traceback:"
  79. if self.supports_ansi:
  80. title = color(title, fg=self.color_tb, bold=True)
  81. return "\n\n{indent}{title}\n{indent}{tb}".format(
  82. title=title, tb=tb_data, indent=self.indent
  83. )
  84. def _format_traceback(self, path, line, fn, text, i, count, highlight):
  85. template = "{base_indent}{indent} {fn} in {path}:{line}{text}"
  86. indent = (LINE_EDGE if i == count - 1 else LINE_FORK) + LINE_PATH * i
  87. if self.tb_base and self.tb_base in path:
  88. path = path.rsplit(self.tb_base, 1)[1]
  89. text = self._format_user_error(text, i, highlight) if i == count - 1 else ""
  90. if self.supports_ansi:
  91. fn = color(fn, bold=True)
  92. path = color(path, underline=True)
  93. return template.format(
  94. base_indent=self.indent,
  95. line=line,
  96. indent=indent,
  97. text=text,
  98. fn=fn,
  99. path=path,
  100. )
  101. def _format_user_error(self, text, i, highlight):
  102. spacing = " " * i + " >>>"
  103. if self.supports_ansi:
  104. spacing = color(spacing, fg=self.color_error)
  105. if highlight and self.supports_ansi:
  106. formatted_highlight = color(highlight, fg=self.color_highlight)
  107. text = text.replace(highlight, formatted_highlight)
  108. return "\n{} {} {}".format(self.indent, spacing, text)