core.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882
  1. import ast
  2. import html
  3. import os
  4. import sys
  5. from collections import defaultdict, Counter
  6. from textwrap import dedent
  7. from types import FrameType, CodeType, TracebackType
  8. from typing import (
  9. Iterator, List, Tuple, Optional, NamedTuple,
  10. Any, Iterable, Callable, Union,
  11. Sequence)
  12. from typing import Mapping
  13. import executing
  14. from asttokens.util import Token
  15. from executing import only
  16. from pure_eval import Evaluator, is_expression_interesting
  17. from stack_data.utils import (
  18. truncate, unique_in_order, line_range,
  19. frame_and_lineno, iter_stack, collapse_repeated, group_by_key_func,
  20. cached_property, is_frame, _pygmented_with_ranges, assert_)
  21. RangeInLine = NamedTuple('RangeInLine',
  22. [('start', int),
  23. ('end', int),
  24. ('data', Any)])
  25. RangeInLine.__doc__ = """
  26. Represents a range of characters within one line of source code,
  27. and some associated data.
  28. Typically this will be converted to a pair of markers by markers_from_ranges.
  29. """
  30. MarkerInLine = NamedTuple('MarkerInLine',
  31. [('position', int),
  32. ('is_start', bool),
  33. ('string', str)])
  34. MarkerInLine.__doc__ = """
  35. A string that is meant to be inserted at a given position in a line of source code.
  36. For example, this could be an ANSI code or the opening or closing of an HTML tag.
  37. is_start should be True if this is the first of a pair such as the opening of an HTML tag.
  38. This will help to sort and insert markers correctly.
  39. Typically this would be created from a RangeInLine by markers_from_ranges.
  40. Then use Line.render to insert the markers correctly.
  41. """
  42. class Variable(
  43. NamedTuple('_Variable',
  44. [('name', str),
  45. ('nodes', Sequence[ast.AST]),
  46. ('value', Any)])
  47. ):
  48. """
  49. An expression that appears one or more times in source code and its associated value.
  50. This will usually be a variable but it can be any expression evaluated by pure_eval.
  51. - name is the source text of the expression.
  52. - nodes is a list of equivalent nodes representing the same expression.
  53. - value is the safely evaluated value of the expression.
  54. """
  55. __hash__ = object.__hash__
  56. __eq__ = object.__eq__
  57. class Source(executing.Source):
  58. """
  59. The source code of a single file and associated metadata.
  60. In addition to the attributes from the base class executing.Source,
  61. if .tree is not None, meaning this is valid Python code, objects have:
  62. - pieces: a list of Piece objects
  63. - tokens_by_lineno: a defaultdict(list) mapping line numbers to lists of tokens.
  64. Don't construct this class. Get an instance from frame_info.source.
  65. """
  66. def __init__(self, *args, **kwargs):
  67. super(Source, self).__init__(*args, **kwargs)
  68. if self.tree:
  69. self.asttokens()
  70. @cached_property
  71. def pieces(self) -> List[range]:
  72. if not self.tree:
  73. return [
  74. range(i, i + 1)
  75. for i in range(1, len(self.lines) + 1)
  76. ]
  77. return list(self._clean_pieces())
  78. @cached_property
  79. def tokens_by_lineno(self) -> Mapping[int, List[Token]]:
  80. if not self.tree:
  81. raise AttributeError("This file doesn't contain valid Python, so .tokens_by_lineno doesn't exist")
  82. return group_by_key_func(
  83. self.asttokens().tokens,
  84. lambda tok: tok.start[0],
  85. )
  86. def _clean_pieces(self) -> Iterator[range]:
  87. pieces = self._raw_split_into_pieces(self.tree, 1, len(self.lines) + 1)
  88. pieces = [
  89. (start, end)
  90. for (start, end) in pieces
  91. if end > start
  92. ]
  93. starts = [start for start, end in pieces[1:]]
  94. ends = [end for start, end in pieces[:-1]]
  95. if starts != ends:
  96. joins = list(map(set, zip(starts, ends)))
  97. mismatches = [s for s in joins if len(s) > 1]
  98. raise AssertionError("Pieces mismatches: %s" % mismatches)
  99. def is_blank(i):
  100. try:
  101. return not self.lines[i - 1].strip()
  102. except IndexError:
  103. return False
  104. for start, end in pieces:
  105. while is_blank(start):
  106. start += 1
  107. while is_blank(end - 1):
  108. end -= 1
  109. if start < end:
  110. yield range(start, end)
  111. def _raw_split_into_pieces(
  112. self,
  113. stmt: ast.AST,
  114. start: int,
  115. end: int,
  116. ) -> Iterator[Tuple[int, int]]:
  117. self.asttokens()
  118. for name, body in ast.iter_fields(stmt):
  119. if (
  120. isinstance(body, list) and body and
  121. isinstance(body[0], (ast.stmt, ast.ExceptHandler))
  122. ):
  123. for rang, group in sorted(group_by_key_func(body, line_range).items()):
  124. sub_stmt = group[0]
  125. for inner_start, inner_end in self._raw_split_into_pieces(sub_stmt, *rang):
  126. if start < inner_start:
  127. yield start, inner_start
  128. if inner_start < inner_end:
  129. yield inner_start, inner_end
  130. start = inner_end
  131. yield start, end
  132. class Options:
  133. """
  134. Configuration for FrameInfo, either in the constructor or the .stack_data classmethod.
  135. These all determine which Lines and gaps are produced by FrameInfo.lines.
  136. before and after are the number of pieces of context to include in a frame
  137. in addition to the executing piece.
  138. include_signature is whether to include the function signature as a piece in a frame.
  139. If a piece (other than the executing piece) has more than max_lines_per_piece lines,
  140. it will be truncated with a gap in the middle.
  141. """
  142. def __init__(
  143. self, *,
  144. before: int = 3,
  145. after: int = 1,
  146. include_signature: bool = False,
  147. max_lines_per_piece: int = 6,
  148. pygments_formatter=None
  149. ):
  150. self.before = before
  151. self.after = after
  152. self.include_signature = include_signature
  153. self.max_lines_per_piece = max_lines_per_piece
  154. self.pygments_formatter = pygments_formatter
  155. def __repr__(self):
  156. keys = sorted(self.__dict__)
  157. items = ("{}={!r}".format(k, self.__dict__[k]) for k in keys)
  158. return "{}({})".format(type(self).__name__, ", ".join(items))
  159. class LineGap(object):
  160. """
  161. A singleton representing one or more lines of source code that were skipped
  162. in FrameInfo.lines.
  163. LINE_GAP can be created in two ways:
  164. - by truncating a piece of context that's too long.
  165. - immediately after the signature piece if Options.include_signature is true
  166. and the following piece isn't already part of the included pieces.
  167. """
  168. def __repr__(self):
  169. return "LINE_GAP"
  170. LINE_GAP = LineGap()
  171. class Line(object):
  172. """
  173. A single line of source code for a particular stack frame.
  174. Typically this is obtained from FrameInfo.lines.
  175. Since that list may also contain LINE_GAP, you should first check
  176. that this is really a Line before using it.
  177. Attributes:
  178. - frame_info
  179. - lineno: the 1-based line number within the file
  180. - text: the raw source of this line. For displaying text, see .render() instead.
  181. - leading_indent: the number of leading spaces that should probably be stripped.
  182. This attribute is set within FrameInfo.lines. If you construct this class
  183. directly you should probably set it manually (at least to 0).
  184. - is_current: whether this is the line currently being executed by the interpreter
  185. within this frame.
  186. - tokens: a list of source tokens in this line
  187. There are several helpers for constructing RangeInLines which can be converted to markers
  188. using markers_from_ranges which can be passed to .render():
  189. - token_ranges
  190. - variable_ranges
  191. - executing_node_ranges
  192. - range_from_node
  193. """
  194. def __init__(
  195. self,
  196. frame_info: 'FrameInfo',
  197. lineno: int,
  198. ):
  199. self.frame_info = frame_info
  200. self.lineno = lineno
  201. self.text = frame_info.source.lines[lineno - 1] # type: str
  202. self.leading_indent = None # type: Optional[int]
  203. def __repr__(self):
  204. return "<{self.__class__.__name__} {self.lineno} (current={self.is_current}) " \
  205. "{self.text!r} of {self.frame_info.filename}>".format(self=self)
  206. @property
  207. def is_current(self) -> bool:
  208. """
  209. Whether this is the line currently being executed by the interpreter
  210. within this frame.
  211. """
  212. return self.lineno == self.frame_info.lineno
  213. @property
  214. def tokens(self) -> List[Token]:
  215. """
  216. A list of source tokens in this line.
  217. The tokens are Token objects from asttokens:
  218. https://asttokens.readthedocs.io/en/latest/api-index.html#asttokens.util.Token
  219. """
  220. return self.frame_info.source.tokens_by_lineno[self.lineno]
  221. @cached_property
  222. def token_ranges(self) -> List[RangeInLine]:
  223. """
  224. A list of RangeInLines for each token in .tokens,
  225. where range.data is a Token object from asttokens:
  226. https://asttokens.readthedocs.io/en/latest/api-index.html#asttokens.util.Token
  227. """
  228. return [
  229. RangeInLine(
  230. token.start[1],
  231. token.end[1],
  232. token,
  233. )
  234. for token in self.tokens
  235. ]
  236. @cached_property
  237. def variable_ranges(self) -> List[RangeInLine]:
  238. """
  239. A list of RangeInLines for each Variable that appears at least partially in this line.
  240. The data attribute of the range is a pair (variable, node) where node is the particular
  241. AST node from the list variable.nodes that corresponds to this range.
  242. """
  243. return [
  244. self.range_from_node(node, (variable, node))
  245. for variable, node in self.frame_info.variables_by_lineno[self.lineno]
  246. ]
  247. @cached_property
  248. def executing_node_ranges(self) -> List[RangeInLine]:
  249. """
  250. A list of one or zero RangeInLines for the executing node of this frame.
  251. The list will have one element if the node can be found and it overlaps this line.
  252. """
  253. return self._raw_executing_node_ranges(
  254. self.frame_info._executing_node_common_indent
  255. )
  256. def _raw_executing_node_ranges(self, common_indent=0) -> List[RangeInLine]:
  257. ex = self.frame_info.executing
  258. node = ex.node
  259. if node:
  260. rang = self.range_from_node(node, ex, common_indent)
  261. if rang:
  262. return [rang]
  263. return []
  264. def range_from_node(
  265. self, node: ast.AST, data: Any, common_indent: int = 0
  266. ) -> Optional[RangeInLine]:
  267. """
  268. If the given node overlaps with this line, return a RangeInLine
  269. with the correct start and end and the given data.
  270. Otherwise, return None.
  271. """
  272. start, end = line_range(node)
  273. end -= 1
  274. if not (start <= self.lineno <= end):
  275. return None
  276. if start == self.lineno:
  277. try:
  278. range_start = node.first_token.start[1]
  279. except AttributeError:
  280. range_start = node.col_offset
  281. else:
  282. range_start = 0
  283. range_start = max(range_start, common_indent)
  284. if end == self.lineno:
  285. try:
  286. range_end = node.last_token.end[1]
  287. except AttributeError:
  288. try:
  289. range_end = node.end_col_offset
  290. except AttributeError:
  291. return None
  292. else:
  293. range_end = len(self.text)
  294. return RangeInLine(range_start, range_end, data)
  295. def render(
  296. self,
  297. markers: Iterable[MarkerInLine] = (),
  298. *,
  299. strip_leading_indent: bool = True,
  300. pygmented: bool = False,
  301. escape_html: bool = False
  302. ) -> str:
  303. """
  304. Produces a string for display consisting of .text
  305. with the .strings of each marker inserted at the correct positions.
  306. If strip_leading_indent is true (the default) then leading spaces
  307. common to all lines in this frame will be excluded.
  308. """
  309. if pygmented and self.frame_info.scope:
  310. assert_(not markers, ValueError("Cannot use pygmented with markers"))
  311. start_line, lines = self.frame_info._pygmented_scope_lines
  312. result = lines[self.lineno - start_line]
  313. if strip_leading_indent:
  314. result = result.replace(self.text[:self.leading_indent], "", 1)
  315. return result
  316. text = self.text
  317. # This just makes the loop below simpler
  318. markers = list(markers) + [MarkerInLine(position=len(text), is_start=False, string='')]
  319. markers.sort(key=lambda t: t[:2])
  320. parts = []
  321. if strip_leading_indent:
  322. start = self.leading_indent
  323. else:
  324. start = 0
  325. original_start = start
  326. for marker in markers:
  327. text_part = text[start:marker.position]
  328. if escape_html:
  329. text_part = html.escape(text_part)
  330. parts.append(text_part)
  331. parts.append(marker.string)
  332. # Ensure that start >= leading_indent
  333. start = max(marker.position, original_start)
  334. return ''.join(parts)
  335. def markers_from_ranges(
  336. ranges: Iterable[RangeInLine],
  337. converter: Callable[[RangeInLine], Optional[Tuple[str, str]]],
  338. ) -> List[MarkerInLine]:
  339. """
  340. Helper to create MarkerInLines given some RangeInLines.
  341. converter should be a function accepting a RangeInLine returning
  342. either None (which is ignored) or a pair of strings which
  343. are used to create two markers included in the returned list.
  344. """
  345. markers = []
  346. for rang in ranges:
  347. converted = converter(rang)
  348. if converted is None:
  349. continue
  350. start_string, end_string = converted
  351. if not (isinstance(start_string, str) and isinstance(end_string, str)):
  352. raise TypeError("converter should return None or a pair of strings")
  353. markers += [
  354. MarkerInLine(position=rang.start, is_start=True, string=start_string),
  355. MarkerInLine(position=rang.end, is_start=False, string=end_string),
  356. ]
  357. return markers
  358. def style_with_executing_node(style, modifier):
  359. from pygments.styles import get_style_by_name
  360. if isinstance(style, str):
  361. style = get_style_by_name(style)
  362. class NewStyle(style):
  363. for_executing_node = True
  364. styles = {
  365. **style.styles,
  366. **{
  367. k.ExecutingNode: v + " " + modifier
  368. for k, v in style.styles.items()
  369. }
  370. }
  371. return NewStyle
  372. class RepeatedFrames:
  373. """
  374. A sequence of consecutive stack frames which shouldn't be displayed because
  375. the same code and line number were repeated many times in the stack, e.g.
  376. because of deep recursion.
  377. Attributes:
  378. - frames: list of raw frame or traceback objects
  379. - frame_keys: list of tuples (frame.f_code, lineno) extracted from the frame objects.
  380. It's this information from the frames that is used to determine
  381. whether two frames should be considered similar (i.e. repeating).
  382. - description: A string briefly describing frame_keys
  383. """
  384. def __init__(
  385. self,
  386. frames: List[Union[FrameType, TracebackType]],
  387. frame_keys: List[Tuple[CodeType, int]],
  388. ):
  389. self.frames = frames
  390. self.frame_keys = frame_keys
  391. @cached_property
  392. def description(self) -> str:
  393. """
  394. A string briefly describing the repeated frames, e.g.
  395. my_function at line 10 (100 times)
  396. """
  397. counts = sorted(Counter(self.frame_keys).items(),
  398. key=lambda item: (-item[1], item[0][0].co_name))
  399. return ', '.join(
  400. '{name} at line {lineno} ({count} times)'.format(
  401. name=Source.for_filename(code.co_filename).code_qualname(code),
  402. lineno=lineno,
  403. count=count,
  404. )
  405. for (code, lineno), count in counts
  406. )
  407. def __repr__(self):
  408. return '<{self.__class__.__name__} {self.description}>'.format(self=self)
  409. class FrameInfo(object):
  410. """
  411. Information about a frame!
  412. Pass either a frame object or a traceback object,
  413. and optionally an Options object to configure.
  414. Or use the classmethod FrameInfo.stack_data() for an iterator of FrameInfo and
  415. RepeatedFrames objects.
  416. Attributes:
  417. - frame: an actual stack frame object, either frame_or_tb or frame_or_tb.tb_frame
  418. - options
  419. - code: frame.f_code
  420. - source: a Source object
  421. - filename: a hopefully absolute file path derived from code.co_filename
  422. - scope: the AST node of the innermost function, class or module being executed
  423. - lines: a list of Line/LineGap objects to display, determined by options
  424. - executing: an Executing object from the `executing` library, which has:
  425. - .node: the AST node being executed in this frame, or None if it's unknown
  426. - .statements: a set of one or more candidate statements (AST nodes, probably just one)
  427. currently being executed in this frame.
  428. - .code_qualname(): the __qualname__ of the function or class being executed,
  429. or just the code name.
  430. Properties returning one or more pieces of source code (ranges of lines):
  431. - scope_pieces: all the pieces in the scope
  432. - included_pieces: a subset of scope_pieces determined by options
  433. - executing_piece: the piece currently being executed in this frame
  434. Properties returning lists of Variable objects:
  435. - variables: all variables in the scope
  436. - variables_by_lineno: variables organised into lines
  437. - variables_in_lines: variables contained within FrameInfo.lines
  438. - variables_in_executing_piece: variables contained within FrameInfo.executing_piece
  439. """
  440. def __init__(
  441. self,
  442. frame_or_tb: Union[FrameType, TracebackType],
  443. options: Optional[Options] = None,
  444. ):
  445. self.executing = Source.executing(frame_or_tb)
  446. frame, self.lineno = frame_and_lineno(frame_or_tb)
  447. self.frame = frame
  448. self.code = frame.f_code
  449. self.options = options or Options() # type: Options
  450. self.source = self.executing.source # type: Source
  451. def __repr__(self):
  452. return "{self.__class__.__name__}({self.frame})".format(self=self)
  453. @classmethod
  454. def stack_data(
  455. cls,
  456. frame_or_tb: Union[FrameType, TracebackType],
  457. options: Optional[Options] = None,
  458. *,
  459. collapse_repeated_frames: bool = True
  460. ) -> Iterator[Union['FrameInfo', RepeatedFrames]]:
  461. """
  462. An iterator of FrameInfo and RepeatedFrames objects representing
  463. a full traceback or stack. Similar consecutive frames are collapsed into RepeatedFrames
  464. objects, so always check what type of object has been yielded.
  465. Pass either a frame object or a traceback object,
  466. and optionally an Options object to configure.
  467. """
  468. stack = list(iter_stack(frame_or_tb))
  469. # Reverse the stack from a frame so that it's in the same order
  470. # as the order from a traceback, which is the order of a printed
  471. # traceback when read top to bottom (most recent call last)
  472. if is_frame(frame_or_tb):
  473. stack = stack[::-1]
  474. def mapper(f):
  475. return cls(f, options)
  476. if not collapse_repeated_frames:
  477. yield from map(mapper, stack)
  478. return
  479. def _frame_key(x):
  480. frame, lineno = frame_and_lineno(x)
  481. return frame.f_code, lineno
  482. yield from collapse_repeated(
  483. stack,
  484. mapper=mapper,
  485. collapser=RepeatedFrames,
  486. key=_frame_key,
  487. )
  488. @cached_property
  489. def scope_pieces(self) -> List[range]:
  490. """
  491. All the pieces (ranges of lines) contained in this object's .scope,
  492. unless there is no .scope (because the source isn't valid Python syntax)
  493. in which case it returns all the pieces in the source file, each containing one line.
  494. """
  495. if not self.scope:
  496. return self.source.pieces
  497. scope_start, scope_end = line_range(self.scope)
  498. return [
  499. piece
  500. for piece in self.source.pieces
  501. if scope_start <= piece.start and piece.stop <= scope_end
  502. ]
  503. @cached_property
  504. def filename(self) -> str:
  505. """
  506. A hopefully absolute file path derived from .code.co_filename,
  507. the current working directory, and sys.path.
  508. Code based on ipython.
  509. """
  510. result = self.code.co_filename
  511. if (
  512. os.path.isabs(result) or
  513. (
  514. result.startswith("<") and
  515. result.endswith(">")
  516. )
  517. ):
  518. return result
  519. # Try to make the filename absolute by trying all
  520. # sys.path entries (which is also what linecache does)
  521. # as well as the current working directory
  522. for dirname in ["."] + list(sys.path):
  523. try:
  524. fullname = os.path.join(dirname, result)
  525. if os.path.isfile(fullname):
  526. return os.path.abspath(fullname)
  527. except Exception:
  528. # Just in case that sys.path contains very
  529. # strange entries...
  530. pass
  531. return result
  532. @cached_property
  533. def executing_piece(self) -> range:
  534. """
  535. The piece (range of lines) containing the line currently being executed
  536. by the interpreter in this frame.
  537. """
  538. return only(
  539. piece
  540. for piece in self.scope_pieces
  541. if self.lineno in piece
  542. )
  543. @cached_property
  544. def included_pieces(self) -> List[range]:
  545. """
  546. The list of pieces (ranges of lines) to display for this frame.
  547. Consists of .executing_piece, surrounding context pieces
  548. determined by .options.before and .options.after,
  549. and the function signature if a function is being executed and
  550. .options.include_signature is True (in which case this might not
  551. be a contiguous range of pieces).
  552. Always a subset of .scope_pieces.
  553. """
  554. scope_pieces = self.scope_pieces
  555. if not self.scope_pieces:
  556. return []
  557. pos = scope_pieces.index(self.executing_piece)
  558. pieces_start = max(0, pos - self.options.before)
  559. pieces_end = pos + 1 + self.options.after
  560. pieces = scope_pieces[pieces_start:pieces_end]
  561. if (
  562. self.options.include_signature
  563. and not self.code.co_name.startswith('<')
  564. and isinstance(self.scope, (ast.FunctionDef, ast.AsyncFunctionDef))
  565. and pieces_start > 0
  566. ):
  567. pieces.insert(0, scope_pieces[0])
  568. return pieces
  569. @cached_property
  570. def _executing_node_common_indent(self) -> int:
  571. """
  572. The common minimal indentation shared by the markers intended
  573. for an exception node that spans multiple lines.
  574. Intended to be used only internally.
  575. """
  576. indents = []
  577. lines = [line for line in self.lines if isinstance(line, Line)]
  578. for line in lines:
  579. for rang in line._raw_executing_node_ranges():
  580. begin_text = len(line.text) - len(line.text.lstrip())
  581. indent = max(rang.start, begin_text)
  582. indents.append(indent)
  583. return min(indents) if indents else 0
  584. @cached_property
  585. def lines(self) -> List[Union[Line, LineGap]]:
  586. """
  587. A list of lines to display, determined by options.
  588. The objects yielded either have type Line or are the singleton LINE_GAP.
  589. Always check the type that you're dealing with when iterating.
  590. LINE_GAP can be created in two ways:
  591. - by truncating a piece of context that's too long, determined by
  592. .options.max_lines_per_piece
  593. - immediately after the signature piece if Options.include_signature is true
  594. and the following piece isn't already part of the included pieces.
  595. The Line objects are all within the ranges from .included_pieces.
  596. """
  597. pieces = self.included_pieces
  598. if not pieces:
  599. return []
  600. result = []
  601. for i, piece in enumerate(pieces):
  602. if (
  603. i == 1
  604. and self.scope
  605. and pieces[0] == self.scope_pieces[0]
  606. and pieces[1] != self.scope_pieces[1]
  607. ):
  608. result.append(LINE_GAP)
  609. lines = [Line(self, i) for i in piece] # type: List[Line]
  610. if piece != self.executing_piece:
  611. lines = truncate(
  612. lines,
  613. max_length=self.options.max_lines_per_piece,
  614. middle=[LINE_GAP],
  615. )
  616. result.extend(lines)
  617. real_lines = [
  618. line
  619. for line in result
  620. if isinstance(line, Line)
  621. ]
  622. text = "\n".join(
  623. line.text
  624. for line in real_lines
  625. )
  626. dedented_lines = dedent(text).splitlines()
  627. leading_indent = len(real_lines[0].text) - len(dedented_lines[0])
  628. for line in real_lines:
  629. line.leading_indent = leading_indent
  630. return result
  631. @cached_property
  632. def scope(self) -> Optional[ast.AST]:
  633. """
  634. The AST node of the innermost function, class or module being executed.
  635. """
  636. if not self.source.tree or not self.executing.statements:
  637. return None
  638. stmt = list(self.executing.statements)[0]
  639. while True:
  640. # Get the parent first in case the original statement is already
  641. # a function definition, e.g. if we're calling a decorator
  642. # In that case we still want the surrounding scope, not that function
  643. stmt = stmt.parent
  644. if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Module)):
  645. return stmt
  646. @cached_property
  647. def _pygmented_scope_lines(self) -> Optional[Tuple[int, List[str]]]:
  648. # noinspection PyUnresolvedReferences
  649. from pygments.formatters import HtmlFormatter
  650. formatter = self.options.pygments_formatter
  651. scope = self.scope
  652. assert_(formatter, ValueError("Must set a pygments formatter in Options"))
  653. assert_(scope)
  654. if isinstance(formatter, HtmlFormatter):
  655. formatter.nowrap = True
  656. atok = self.source.asttokens()
  657. node = self.executing.node
  658. if node and getattr(formatter.style, "for_executing_node", False):
  659. scope_start = atok.get_text_range(scope)[0]
  660. start, end = atok.get_text_range(node)
  661. start -= scope_start
  662. end -= scope_start
  663. ranges = [(start, end)]
  664. else:
  665. ranges = []
  666. code = atok.get_text(scope)
  667. lines = _pygmented_with_ranges(formatter, code, ranges)
  668. start_line = line_range(scope)[0]
  669. return start_line, lines
  670. @cached_property
  671. def variables(self) -> List[Variable]:
  672. """
  673. All Variable objects whose nodes are contained within .scope
  674. and whose values could be safely evaluated by pure_eval.
  675. """
  676. if not self.scope:
  677. return []
  678. evaluator = Evaluator.from_frame(self.frame)
  679. scope = self.scope
  680. node_values = [
  681. pair
  682. for pair in evaluator.find_expressions(scope)
  683. if is_expression_interesting(*pair)
  684. ] # type: List[Tuple[ast.AST, Any]]
  685. if isinstance(scope, (ast.FunctionDef, ast.AsyncFunctionDef)):
  686. for node in ast.walk(scope.args):
  687. if not isinstance(node, ast.arg):
  688. continue
  689. name = node.arg
  690. try:
  691. value = evaluator.names[name]
  692. except KeyError:
  693. pass
  694. else:
  695. node_values.append((node, value))
  696. # Group equivalent nodes together
  697. def get_text(n):
  698. if isinstance(n, ast.arg):
  699. return n.arg
  700. else:
  701. return self.source.asttokens().get_text(n)
  702. def normalise_node(n):
  703. try:
  704. # Add parens to avoid syntax errors for multiline expressions
  705. return ast.parse('(' + get_text(n) + ')')
  706. except Exception:
  707. return n
  708. grouped = group_by_key_func(
  709. node_values,
  710. lambda nv: ast.dump(normalise_node(nv[0])),
  711. )
  712. result = []
  713. for group in grouped.values():
  714. nodes, values = zip(*group)
  715. value = values[0]
  716. text = get_text(nodes[0])
  717. if not text:
  718. continue
  719. result.append(Variable(text, nodes, value))
  720. return result
  721. @cached_property
  722. def variables_by_lineno(self) -> Mapping[int, List[Tuple[Variable, ast.AST]]]:
  723. """
  724. A mapping from 1-based line numbers to lists of pairs:
  725. - A Variable object
  726. - A specific AST node from the variable's .nodes list that's
  727. in the line at that line number.
  728. """
  729. result = defaultdict(list)
  730. for var in self.variables:
  731. for node in var.nodes:
  732. for lineno in range(*line_range(node)):
  733. result[lineno].append((var, node))
  734. return result
  735. @cached_property
  736. def variables_in_lines(self) -> List[Variable]:
  737. """
  738. A list of Variable objects contained within the lines returned by .lines.
  739. """
  740. return unique_in_order(
  741. var
  742. for line in self.lines
  743. if isinstance(line, Line)
  744. for var, node in self.variables_by_lineno[line.lineno]
  745. )
  746. @cached_property
  747. def variables_in_executing_piece(self) -> List[Variable]:
  748. """
  749. A list of Variable objects contained within the lines
  750. in the range returned by .executing_piece.
  751. """
  752. return unique_in_order(
  753. var
  754. for lineno in self.executing_piece
  755. for var, node in self.variables_by_lineno[lineno]
  756. )