formatting.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import inspect
  2. import sys
  3. import traceback
  4. from types import FrameType, TracebackType
  5. from typing import Union, Iterable
  6. from stack_data import (style_with_executing_node, Options, Line, FrameInfo, LINE_GAP,
  7. Variable, RepeatedFrames, BlankLineRange, BlankLines)
  8. from stack_data.utils import assert_
  9. class Formatter:
  10. def __init__(
  11. self, *,
  12. options=None,
  13. pygmented=False,
  14. show_executing_node=True,
  15. pygments_formatter_cls=None,
  16. pygments_formatter_kwargs=None,
  17. pygments_style="monokai",
  18. executing_node_modifier="bg:#005080",
  19. executing_node_underline="^",
  20. current_line_indicator="-->",
  21. line_gap_string="(...)",
  22. line_number_gap_string=":",
  23. line_number_format_string="{:4} | ",
  24. show_variables=False,
  25. use_code_qualname=True,
  26. show_linenos=True,
  27. strip_leading_indent=True,
  28. html=False,
  29. chain=True,
  30. collapse_repeated_frames=True
  31. ):
  32. if options is None:
  33. options = Options()
  34. if pygmented and not options.pygments_formatter:
  35. if show_executing_node:
  36. pygments_style = style_with_executing_node(
  37. pygments_style, executing_node_modifier
  38. )
  39. if pygments_formatter_cls is None:
  40. from pygments.formatters.terminal256 import Terminal256Formatter \
  41. as pygments_formatter_cls
  42. options.pygments_formatter = pygments_formatter_cls(
  43. style=pygments_style,
  44. **pygments_formatter_kwargs or {},
  45. )
  46. self.pygmented = pygmented
  47. self.show_executing_node = show_executing_node
  48. assert_(
  49. len(executing_node_underline) == 1,
  50. ValueError("executing_node_underline must be a single character"),
  51. )
  52. self.executing_node_underline = executing_node_underline
  53. self.current_line_indicator = current_line_indicator or ""
  54. self.line_gap_string = line_gap_string
  55. self.line_number_gap_string = line_number_gap_string
  56. self.line_number_format_string = line_number_format_string
  57. self.show_variables = show_variables
  58. self.show_linenos = show_linenos
  59. self.use_code_qualname = use_code_qualname
  60. self.strip_leading_indent = strip_leading_indent
  61. self.html = html
  62. self.chain = chain
  63. self.options = options
  64. self.collapse_repeated_frames = collapse_repeated_frames
  65. if not self.show_linenos and self.options.blank_lines == BlankLines.SINGLE:
  66. raise ValueError(
  67. "BlankLines.SINGLE option can only be used when show_linenos=True"
  68. )
  69. def set_hook(self):
  70. def excepthook(_etype, evalue, _tb):
  71. self.print_exception(evalue)
  72. sys.excepthook = excepthook
  73. def print_exception(self, e=None, *, file=None):
  74. self.print_lines(self.format_exception(e), file=file)
  75. def print_stack(self, frame_or_tb=None, *, file=None):
  76. if frame_or_tb is None:
  77. frame_or_tb = inspect.currentframe().f_back
  78. self.print_lines(self.format_stack(frame_or_tb), file=file)
  79. def print_lines(self, lines, *, file=None):
  80. if file is None:
  81. file = sys.stderr
  82. for line in lines:
  83. print(line, file=file, end="")
  84. def format_exception(self, e=None) -> Iterable[str]:
  85. if e is None:
  86. e = sys.exc_info()[1]
  87. if self.chain:
  88. if e.__cause__ is not None:
  89. yield from self.format_exception(e.__cause__)
  90. yield traceback._cause_message
  91. elif (e.__context__ is not None
  92. and not e.__suppress_context__):
  93. yield from self.format_exception(e.__context__)
  94. yield traceback._context_message
  95. yield 'Traceback (most recent call last):\n'
  96. yield from self.format_stack(e.__traceback__)
  97. yield from traceback.format_exception_only(type(e), e)
  98. def format_stack(self, frame_or_tb=None) -> Iterable[str]:
  99. if frame_or_tb is None:
  100. frame_or_tb = inspect.currentframe().f_back
  101. yield from self.format_stack_data(
  102. FrameInfo.stack_data(
  103. frame_or_tb,
  104. self.options,
  105. collapse_repeated_frames=self.collapse_repeated_frames,
  106. )
  107. )
  108. def format_stack_data(
  109. self, stack: Iterable[Union[FrameInfo, RepeatedFrames]]
  110. ) -> Iterable[str]:
  111. for item in stack:
  112. if isinstance(item, FrameInfo):
  113. yield from self.format_frame(item)
  114. else:
  115. yield self.format_repeated_frames(item)
  116. def format_repeated_frames(self, repeated_frames: RepeatedFrames) -> str:
  117. return ' [... skipping similar frames: {}]\n'.format(
  118. repeated_frames.description
  119. )
  120. def format_frame(self, frame: Union[FrameInfo, FrameType, TracebackType]) -> Iterable[str]:
  121. if not isinstance(frame, FrameInfo):
  122. frame = FrameInfo(frame, self.options)
  123. yield self.format_frame_header(frame)
  124. for line in frame.lines:
  125. if isinstance(line, Line):
  126. yield self.format_line(line)
  127. elif isinstance(line, BlankLineRange):
  128. yield self.format_blank_lines_linenumbers(line)
  129. else:
  130. assert_(line is LINE_GAP)
  131. yield self.line_gap_string + "\n"
  132. if self.show_variables:
  133. try:
  134. yield from self.format_variables(frame)
  135. except Exception:
  136. pass
  137. def format_frame_header(self, frame_info: FrameInfo) -> str:
  138. return ' File "{frame_info.filename}", line {frame_info.lineno}, in {name}\n'.format(
  139. frame_info=frame_info,
  140. name=(
  141. frame_info.executing.code_qualname()
  142. if self.use_code_qualname else
  143. frame_info.code.co_name
  144. ),
  145. )
  146. def format_line(self, line: Line) -> str:
  147. result = ""
  148. if self.current_line_indicator:
  149. if line.is_current:
  150. result = self.current_line_indicator
  151. else:
  152. result = " " * len(self.current_line_indicator)
  153. result += " "
  154. else:
  155. result = " "
  156. if self.show_linenos:
  157. result += self.line_number_format_string.format(line.lineno)
  158. prefix = result
  159. result += line.render(
  160. pygmented=self.pygmented,
  161. escape_html=self.html,
  162. strip_leading_indent=self.strip_leading_indent,
  163. ) + "\n"
  164. if self.show_executing_node and not self.pygmented:
  165. for line_range in line.executing_node_ranges:
  166. start = line_range.start - line.leading_indent
  167. end = line_range.end - line.leading_indent
  168. # if end <= start, we have an empty line inside a highlighted
  169. # block of code. In this case, we need to avoid inserting
  170. # an extra blank line with no markers present.
  171. if end > start:
  172. result += (
  173. " " * (start + len(prefix))
  174. + self.executing_node_underline * (end - start)
  175. + "\n"
  176. )
  177. return result
  178. def format_blank_lines_linenumbers(self, blank_line):
  179. if self.current_line_indicator:
  180. result = " " * len(self.current_line_indicator) + " "
  181. else:
  182. result = " "
  183. if blank_line.begin_lineno == blank_line.end_lineno:
  184. return result + self.line_number_format_string.format(blank_line.begin_lineno) + "\n"
  185. return result + " {}\n".format(self.line_number_gap_string)
  186. def format_variables(self, frame_info: FrameInfo) -> Iterable[str]:
  187. for var in sorted(frame_info.variables, key=lambda v: v.name):
  188. try:
  189. yield self.format_variable(var) + "\n"
  190. except Exception:
  191. pass
  192. def format_variable(self, var: Variable) -> str:
  193. return "{} = {}".format(
  194. var.name,
  195. self.format_variable_value(var.value),
  196. )
  197. def format_variable_value(self, value) -> str:
  198. return repr(value)