Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/stack_data/core.py: 25%
375 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-20 06:09 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-20 06:09 +0000
1import ast
2import html
3import os
4import sys
5from collections import defaultdict, Counter
6from enum import Enum
7from textwrap import dedent
8from types import FrameType, CodeType, TracebackType
9from typing import (
10 Iterator, List, Tuple, Optional, NamedTuple,
11 Any, Iterable, Callable, Union,
12 Sequence)
13from typing import Mapping
15import executing
16from asttokens.util import Token
17from executing import only
18from pure_eval import Evaluator, is_expression_interesting
19from stack_data.utils import (
20 truncate, unique_in_order, line_range,
21 frame_and_lineno, iter_stack, collapse_repeated, group_by_key_func,
22 cached_property, is_frame, _pygmented_with_ranges, assert_)
24RangeInLine = NamedTuple('RangeInLine',
25 [('start', int),
26 ('end', int),
27 ('data', Any)])
28RangeInLine.__doc__ = """
29Represents a range of characters within one line of source code,
30and some associated data.
32Typically this will be converted to a pair of markers by markers_from_ranges.
33"""
35MarkerInLine = NamedTuple('MarkerInLine',
36 [('position', int),
37 ('is_start', bool),
38 ('string', str)])
39MarkerInLine.__doc__ = """
40A string that is meant to be inserted at a given position in a line of source code.
41For example, this could be an ANSI code or the opening or closing of an HTML tag.
42is_start should be True if this is the first of a pair such as the opening of an HTML tag.
43This will help to sort and insert markers correctly.
45Typically this would be created from a RangeInLine by markers_from_ranges.
46Then use Line.render to insert the markers correctly.
47"""
50class BlankLines(Enum):
51 """The values are intended to correspond to the following behaviour:
52 HIDDEN: blank lines are not shown in the output
53 VISIBLE: blank lines are visible in the output
54 SINGLE: any consecutive blank lines are shown as a single blank line
55 in the output. This option requires the line number to be shown.
56 For a single blank line, the corresponding line number is shown.
57 Two or more consecutive blank lines are shown as a single blank
58 line in the output with a custom string shown instead of a
59 specific line number.
60 """
61 HIDDEN = 1
62 VISIBLE = 2
63 SINGLE=3
65class Variable(
66 NamedTuple('_Variable',
67 [('name', str),
68 ('nodes', Sequence[ast.AST]),
69 ('value', Any)])
70):
71 """
72 An expression that appears one or more times in source code and its associated value.
73 This will usually be a variable but it can be any expression evaluated by pure_eval.
74 - name is the source text of the expression.
75 - nodes is a list of equivalent nodes representing the same expression.
76 - value is the safely evaluated value of the expression.
77 """
78 __hash__ = object.__hash__
79 __eq__ = object.__eq__
82class Source(executing.Source):
83 """
84 The source code of a single file and associated metadata.
86 In addition to the attributes from the base class executing.Source,
87 if .tree is not None, meaning this is valid Python code, objects have:
88 - pieces: a list of Piece objects
89 - tokens_by_lineno: a defaultdict(list) mapping line numbers to lists of tokens.
91 Don't construct this class. Get an instance from frame_info.source.
92 """
94 @cached_property
95 def pieces(self) -> List[range]:
96 if not self.tree:
97 return [
98 range(i, i + 1)
99 for i in range(1, len(self.lines) + 1)
100 ]
101 return list(self._clean_pieces())
103 @cached_property
104 def tokens_by_lineno(self) -> Mapping[int, List[Token]]:
105 if not self.tree:
106 raise AttributeError("This file doesn't contain valid Python, so .tokens_by_lineno doesn't exist")
107 return group_by_key_func(
108 self.asttokens().tokens,
109 lambda tok: tok.start[0],
110 )
112 def _clean_pieces(self) -> Iterator[range]:
113 pieces = self._raw_split_into_pieces(self.tree, 1, len(self.lines) + 1)
114 pieces = [
115 (start, end)
116 for (start, end) in pieces
117 if end > start
118 ]
120 # Combine overlapping pieces, i.e. consecutive pieces where the end of the first
121 # is greater than the start of the second.
122 # This can happen when two statements are on the same line separated by a semicolon.
123 new_pieces = pieces[:1]
124 for (start, end) in pieces[1:]:
125 (last_start, last_end) = new_pieces[-1]
126 if start < last_end:
127 assert start == last_end - 1
128 assert ';' in self.lines[start - 1]
129 new_pieces[-1] = (last_start, end)
130 else:
131 new_pieces.append((start, end))
132 pieces = new_pieces
134 starts = [start for start, end in pieces[1:]]
135 ends = [end for start, end in pieces[:-1]]
136 if starts != ends:
137 joins = list(map(set, zip(starts, ends)))
138 mismatches = [s for s in joins if len(s) > 1]
139 raise AssertionError("Pieces mismatches: %s" % mismatches)
141 def is_blank(i):
142 try:
143 return not self.lines[i - 1].strip()
144 except IndexError:
145 return False
147 for start, end in pieces:
148 while is_blank(start):
149 start += 1
150 while is_blank(end - 1):
151 end -= 1
152 if start < end:
153 yield range(start, end)
155 def _raw_split_into_pieces(
156 self,
157 stmt: ast.AST,
158 start: int,
159 end: int,
160 ) -> Iterator[Tuple[int, int]]:
161 for name, body in ast.iter_fields(stmt):
162 if (
163 isinstance(body, list) and body and
164 isinstance(body[0], (ast.stmt, ast.ExceptHandler, getattr(ast, 'match_case', ())))
165 ):
166 for rang, group in sorted(group_by_key_func(body, self.line_range).items()):
167 sub_stmt = group[0]
168 for inner_start, inner_end in self._raw_split_into_pieces(sub_stmt, *rang):
169 if start < inner_start:
170 yield start, inner_start
171 if inner_start < inner_end:
172 yield inner_start, inner_end
173 start = inner_end
175 yield start, end
177 def line_range(self, node: ast.AST) -> Tuple[int, int]:
178 return line_range(self.asttext(), node)
181class Options:
182 """
183 Configuration for FrameInfo, either in the constructor or the .stack_data classmethod.
184 These all determine which Lines and gaps are produced by FrameInfo.lines.
186 before and after are the number of pieces of context to include in a frame
187 in addition to the executing piece.
189 include_signature is whether to include the function signature as a piece in a frame.
191 If a piece (other than the executing piece) has more than max_lines_per_piece lines,
192 it will be truncated with a gap in the middle.
193 """
194 def __init__(
195 self, *,
196 before: int = 3,
197 after: int = 1,
198 include_signature: bool = False,
199 max_lines_per_piece: int = 6,
200 pygments_formatter=None,
201 blank_lines = BlankLines.HIDDEN
202 ):
203 self.before = before
204 self.after = after
205 self.include_signature = include_signature
206 self.max_lines_per_piece = max_lines_per_piece
207 self.pygments_formatter = pygments_formatter
208 self.blank_lines = blank_lines
210 def __repr__(self):
211 keys = sorted(self.__dict__)
212 items = ("{}={!r}".format(k, self.__dict__[k]) for k in keys)
213 return "{}({})".format(type(self).__name__, ", ".join(items))
216class LineGap(object):
217 """
218 A singleton representing one or more lines of source code that were skipped
219 in FrameInfo.lines.
221 LINE_GAP can be created in two ways:
222 - by truncating a piece of context that's too long.
223 - immediately after the signature piece if Options.include_signature is true
224 and the following piece isn't already part of the included pieces.
225 """
226 def __repr__(self):
227 return "LINE_GAP"
230LINE_GAP = LineGap()
233class BlankLineRange:
234 """
235 Records the line number range for blank lines gaps between pieces.
236 For a single blank line, begin_lineno == end_lineno.
237 """
238 def __init__(self, begin_lineno: int, end_lineno: int):
239 self.begin_lineno = begin_lineno
240 self.end_lineno = end_lineno
243class Line(object):
244 """
245 A single line of source code for a particular stack frame.
247 Typically this is obtained from FrameInfo.lines.
248 Since that list may also contain LINE_GAP, you should first check
249 that this is really a Line before using it.
251 Attributes:
252 - frame_info
253 - lineno: the 1-based line number within the file
254 - text: the raw source of this line. For displaying text, see .render() instead.
255 - leading_indent: the number of leading spaces that should probably be stripped.
256 This attribute is set within FrameInfo.lines. If you construct this class
257 directly you should probably set it manually (at least to 0).
258 - is_current: whether this is the line currently being executed by the interpreter
259 within this frame.
260 - tokens: a list of source tokens in this line
262 There are several helpers for constructing RangeInLines which can be converted to markers
263 using markers_from_ranges which can be passed to .render():
264 - token_ranges
265 - variable_ranges
266 - executing_node_ranges
267 - range_from_node
268 """
269 def __init__(
270 self,
271 frame_info: 'FrameInfo',
272 lineno: int,
273 ):
274 self.frame_info = frame_info
275 self.lineno = lineno
276 self.text = frame_info.source.lines[lineno - 1] # type: str
277 self.leading_indent = None # type: Optional[int]
279 def __repr__(self):
280 return "<{self.__class__.__name__} {self.lineno} (current={self.is_current}) " \
281 "{self.text!r} of {self.frame_info.filename}>".format(self=self)
283 @property
284 def is_current(self) -> bool:
285 """
286 Whether this is the line currently being executed by the interpreter
287 within this frame.
288 """
289 return self.lineno == self.frame_info.lineno
291 @property
292 def tokens(self) -> List[Token]:
293 """
294 A list of source tokens in this line.
295 The tokens are Token objects from asttokens:
296 https://asttokens.readthedocs.io/en/latest/api-index.html#asttokens.util.Token
297 """
298 return self.frame_info.source.tokens_by_lineno[self.lineno]
300 @cached_property
301 def token_ranges(self) -> List[RangeInLine]:
302 """
303 A list of RangeInLines for each token in .tokens,
304 where range.data is a Token object from asttokens:
305 https://asttokens.readthedocs.io/en/latest/api-index.html#asttokens.util.Token
306 """
307 return [
308 RangeInLine(
309 token.start[1],
310 token.end[1],
311 token,
312 )
313 for token in self.tokens
314 ]
316 @cached_property
317 def variable_ranges(self) -> List[RangeInLine]:
318 """
319 A list of RangeInLines for each Variable that appears at least partially in this line.
320 The data attribute of the range is a pair (variable, node) where node is the particular
321 AST node from the list variable.nodes that corresponds to this range.
322 """
323 return [
324 self.range_from_node(node, (variable, node))
325 for variable, node in self.frame_info.variables_by_lineno[self.lineno]
326 ]
328 @cached_property
329 def executing_node_ranges(self) -> List[RangeInLine]:
330 """
331 A list of one or zero RangeInLines for the executing node of this frame.
332 The list will have one element if the node can be found and it overlaps this line.
333 """
334 return self._raw_executing_node_ranges(
335 self.frame_info._executing_node_common_indent
336 )
338 def _raw_executing_node_ranges(self, common_indent=0) -> List[RangeInLine]:
339 ex = self.frame_info.executing
340 node = ex.node
341 if node:
342 rang = self.range_from_node(node, ex, common_indent)
343 if rang:
344 return [rang]
345 return []
347 def range_from_node(
348 self, node: ast.AST, data: Any, common_indent: int = 0
349 ) -> Optional[RangeInLine]:
350 """
351 If the given node overlaps with this line, return a RangeInLine
352 with the correct start and end and the given data.
353 Otherwise, return None.
354 """
355 atext = self.frame_info.source.asttext()
356 (start, range_start), (end, range_end) = atext.get_text_positions(node, padded=False)
358 if not (start <= self.lineno <= end):
359 return None
361 if start != self.lineno:
362 range_start = common_indent
364 if end != self.lineno:
365 range_end = len(self.text)
367 if range_start == range_end == 0:
368 # This is an empty line. If it were included, it would result
369 # in a value of zero for the common indentation assigned to
370 # a block of code.
371 return None
373 return RangeInLine(range_start, range_end, data)
375 def render(
376 self,
377 markers: Iterable[MarkerInLine] = (),
378 *,
379 strip_leading_indent: bool = True,
380 pygmented: bool = False,
381 escape_html: bool = False
382 ) -> str:
383 """
384 Produces a string for display consisting of .text
385 with the .strings of each marker inserted at the correct positions.
386 If strip_leading_indent is true (the default) then leading spaces
387 common to all lines in this frame will be excluded.
388 """
389 if pygmented and self.frame_info.scope:
390 assert_(not markers, ValueError("Cannot use pygmented with markers"))
391 start_line, lines = self.frame_info._pygmented_scope_lines
392 result = lines[self.lineno - start_line]
393 if strip_leading_indent:
394 result = result.replace(self.text[:self.leading_indent], "", 1)
395 return result
397 text = self.text
399 # This just makes the loop below simpler
400 markers = list(markers) + [MarkerInLine(position=len(text), is_start=False, string='')]
402 markers.sort(key=lambda t: t[:2])
404 parts = []
405 if strip_leading_indent:
406 start = self.leading_indent
407 else:
408 start = 0
409 original_start = start
411 for marker in markers:
412 text_part = text[start:marker.position]
413 if escape_html:
414 text_part = html.escape(text_part)
415 parts.append(text_part)
416 parts.append(marker.string)
418 # Ensure that start >= leading_indent
419 start = max(marker.position, original_start)
420 return ''.join(parts)
423def markers_from_ranges(
424 ranges: Iterable[RangeInLine],
425 converter: Callable[[RangeInLine], Optional[Tuple[str, str]]],
426) -> List[MarkerInLine]:
427 """
428 Helper to create MarkerInLines given some RangeInLines.
429 converter should be a function accepting a RangeInLine returning
430 either None (which is ignored) or a pair of strings which
431 are used to create two markers included in the returned list.
432 """
433 markers = []
434 for rang in ranges:
435 converted = converter(rang)
436 if converted is None:
437 continue
439 start_string, end_string = converted
440 if not (isinstance(start_string, str) and isinstance(end_string, str)):
441 raise TypeError("converter should return None or a pair of strings")
443 markers += [
444 MarkerInLine(position=rang.start, is_start=True, string=start_string),
445 MarkerInLine(position=rang.end, is_start=False, string=end_string),
446 ]
447 return markers
450def style_with_executing_node(style, modifier):
451 from pygments.styles import get_style_by_name
452 if isinstance(style, str):
453 style = get_style_by_name(style)
455 class NewStyle(style):
456 for_executing_node = True
458 styles = {
459 **style.styles,
460 **{
461 k.ExecutingNode: v + " " + modifier
462 for k, v in style.styles.items()
463 }
464 }
466 return NewStyle
469class RepeatedFrames:
470 """
471 A sequence of consecutive stack frames which shouldn't be displayed because
472 the same code and line number were repeated many times in the stack, e.g.
473 because of deep recursion.
475 Attributes:
476 - frames: list of raw frame or traceback objects
477 - frame_keys: list of tuples (frame.f_code, lineno) extracted from the frame objects.
478 It's this information from the frames that is used to determine
479 whether two frames should be considered similar (i.e. repeating).
480 - description: A string briefly describing frame_keys
481 """
482 def __init__(
483 self,
484 frames: List[Union[FrameType, TracebackType]],
485 frame_keys: List[Tuple[CodeType, int]],
486 ):
487 self.frames = frames
488 self.frame_keys = frame_keys
490 @cached_property
491 def description(self) -> str:
492 """
493 A string briefly describing the repeated frames, e.g.
494 my_function at line 10 (100 times)
495 """
496 counts = sorted(Counter(self.frame_keys).items(),
497 key=lambda item: (-item[1], item[0][0].co_name))
498 return ', '.join(
499 '{name} at line {lineno} ({count} times)'.format(
500 name=Source.for_filename(code.co_filename).code_qualname(code),
501 lineno=lineno,
502 count=count,
503 )
504 for (code, lineno), count in counts
505 )
507 def __repr__(self):
508 return '<{self.__class__.__name__} {self.description}>'.format(self=self)
511class FrameInfo(object):
512 """
513 Information about a frame!
514 Pass either a frame object or a traceback object,
515 and optionally an Options object to configure.
517 Or use the classmethod FrameInfo.stack_data() for an iterator of FrameInfo and
518 RepeatedFrames objects.
520 Attributes:
521 - frame: an actual stack frame object, either frame_or_tb or frame_or_tb.tb_frame
522 - options
523 - code: frame.f_code
524 - source: a Source object
525 - filename: a hopefully absolute file path derived from code.co_filename
526 - scope: the AST node of the innermost function, class or module being executed
527 - lines: a list of Line/LineGap objects to display, determined by options
528 - executing: an Executing object from the `executing` library, which has:
529 - .node: the AST node being executed in this frame, or None if it's unknown
530 - .statements: a set of one or more candidate statements (AST nodes, probably just one)
531 currently being executed in this frame.
532 - .code_qualname(): the __qualname__ of the function or class being executed,
533 or just the code name.
535 Properties returning one or more pieces of source code (ranges of lines):
536 - scope_pieces: all the pieces in the scope
537 - included_pieces: a subset of scope_pieces determined by options
538 - executing_piece: the piece currently being executed in this frame
540 Properties returning lists of Variable objects:
541 - variables: all variables in the scope
542 - variables_by_lineno: variables organised into lines
543 - variables_in_lines: variables contained within FrameInfo.lines
544 - variables_in_executing_piece: variables contained within FrameInfo.executing_piece
545 """
546 def __init__(
547 self,
548 frame_or_tb: Union[FrameType, TracebackType],
549 options: Optional[Options] = None,
550 ):
551 self.executing = Source.executing(frame_or_tb)
552 frame, self.lineno = frame_and_lineno(frame_or_tb)
553 self.frame = frame
554 self.code = frame.f_code
555 self.options = options or Options() # type: Options
556 self.source = self.executing.source # type: Source
559 def __repr__(self):
560 return "{self.__class__.__name__}({self.frame})".format(self=self)
562 @classmethod
563 def stack_data(
564 cls,
565 frame_or_tb: Union[FrameType, TracebackType],
566 options: Optional[Options] = None,
567 *,
568 collapse_repeated_frames: bool = True
569 ) -> Iterator[Union['FrameInfo', RepeatedFrames]]:
570 """
571 An iterator of FrameInfo and RepeatedFrames objects representing
572 a full traceback or stack. Similar consecutive frames are collapsed into RepeatedFrames
573 objects, so always check what type of object has been yielded.
575 Pass either a frame object or a traceback object,
576 and optionally an Options object to configure.
577 """
578 stack = list(iter_stack(frame_or_tb))
580 # Reverse the stack from a frame so that it's in the same order
581 # as the order from a traceback, which is the order of a printed
582 # traceback when read top to bottom (most recent call last)
583 if is_frame(frame_or_tb):
584 stack = stack[::-1]
586 def mapper(f):
587 return cls(f, options)
589 if not collapse_repeated_frames:
590 yield from map(mapper, stack)
591 return
593 def _frame_key(x):
594 frame, lineno = frame_and_lineno(x)
595 return frame.f_code, lineno
597 yield from collapse_repeated(
598 stack,
599 mapper=mapper,
600 collapser=RepeatedFrames,
601 key=_frame_key,
602 )
604 @cached_property
605 def scope_pieces(self) -> List[range]:
606 """
607 All the pieces (ranges of lines) contained in this object's .scope,
608 unless there is no .scope (because the source isn't valid Python syntax)
609 in which case it returns all the pieces in the source file, each containing one line.
610 """
611 if not self.scope:
612 return self.source.pieces
614 scope_start, scope_end = self.source.line_range(self.scope)
615 return [
616 piece
617 for piece in self.source.pieces
618 if scope_start <= piece.start and piece.stop <= scope_end
619 ]
621 @cached_property
622 def filename(self) -> str:
623 """
624 A hopefully absolute file path derived from .code.co_filename,
625 the current working directory, and sys.path.
626 Code based on ipython.
627 """
628 result = self.code.co_filename
630 if (
631 os.path.isabs(result) or
632 (
633 result.startswith("<") and
634 result.endswith(">")
635 )
636 ):
637 return result
639 # Try to make the filename absolute by trying all
640 # sys.path entries (which is also what linecache does)
641 # as well as the current working directory
642 for dirname in ["."] + list(sys.path):
643 try:
644 fullname = os.path.join(dirname, result)
645 if os.path.isfile(fullname):
646 return os.path.abspath(fullname)
647 except Exception:
648 # Just in case that sys.path contains very
649 # strange entries...
650 pass
652 return result
654 @cached_property
655 def executing_piece(self) -> range:
656 """
657 The piece (range of lines) containing the line currently being executed
658 by the interpreter in this frame.
659 """
660 return only(
661 piece
662 for piece in self.scope_pieces
663 if self.lineno in piece
664 )
666 @cached_property
667 def included_pieces(self) -> List[range]:
668 """
669 The list of pieces (ranges of lines) to display for this frame.
670 Consists of .executing_piece, surrounding context pieces
671 determined by .options.before and .options.after,
672 and the function signature if a function is being executed and
673 .options.include_signature is True (in which case this might not
674 be a contiguous range of pieces).
675 Always a subset of .scope_pieces.
676 """
677 scope_pieces = self.scope_pieces
678 if not self.scope_pieces:
679 return []
681 pos = scope_pieces.index(self.executing_piece)
682 pieces_start = max(0, pos - self.options.before)
683 pieces_end = pos + 1 + self.options.after
684 pieces = scope_pieces[pieces_start:pieces_end]
686 if (
687 self.options.include_signature
688 and not self.code.co_name.startswith('<')
689 and isinstance(self.scope, (ast.FunctionDef, ast.AsyncFunctionDef))
690 and pieces_start > 0
691 ):
692 pieces.insert(0, scope_pieces[0])
694 return pieces
696 @cached_property
697 def _executing_node_common_indent(self) -> int:
698 """
699 The common minimal indentation shared by the markers intended
700 for an exception node that spans multiple lines.
702 Intended to be used only internally.
703 """
704 indents = []
705 lines = [line for line in self.lines if isinstance(line, Line)]
707 for line in lines:
708 for rang in line._raw_executing_node_ranges():
709 begin_text = len(line.text) - len(line.text.lstrip())
710 indent = max(rang.start, begin_text)
711 indents.append(indent)
713 if len(indents) <= 1:
714 return 0
716 return min(indents[1:])
718 @cached_property
719 def lines(self) -> List[Union[Line, LineGap, BlankLineRange]]:
720 """
721 A list of lines to display, determined by options.
722 The objects yielded either have type Line, BlankLineRange
723 or are the singleton LINE_GAP.
724 Always check the type that you're dealing with when iterating.
726 LINE_GAP can be created in two ways:
727 - by truncating a piece of context that's too long, determined by
728 .options.max_lines_per_piece
729 - immediately after the signature piece if Options.include_signature is true
730 and the following piece isn't already part of the included pieces.
732 The Line objects are all within the ranges from .included_pieces.
733 """
734 pieces = self.included_pieces
735 if not pieces:
736 return []
738 add_empty_lines = self.options.blank_lines in (BlankLines.VISIBLE, BlankLines.SINGLE)
739 prev_piece = None
740 result = []
741 for i, piece in enumerate(pieces):
742 if (
743 i == 1
744 and self.scope
745 and pieces[0] == self.scope_pieces[0]
746 and pieces[1] != self.scope_pieces[1]
747 ):
748 result.append(LINE_GAP)
749 elif prev_piece and add_empty_lines and piece.start > prev_piece.stop:
750 if self.options.blank_lines == BlankLines.SINGLE:
751 result.append(BlankLineRange(prev_piece.stop, piece.start-1))
752 else: # BlankLines.VISIBLE
753 for lineno in range(prev_piece.stop, piece.start):
754 result.append(Line(self, lineno))
756 lines = [Line(self, i) for i in piece] # type: List[Line]
757 if piece != self.executing_piece:
758 lines = truncate(
759 lines,
760 max_length=self.options.max_lines_per_piece,
761 middle=[LINE_GAP],
762 )
763 result.extend(lines)
764 prev_piece = piece
766 real_lines = [
767 line
768 for line in result
769 if isinstance(line, Line)
770 ]
772 text = "\n".join(
773 line.text
774 for line in real_lines
775 )
776 dedented_lines = dedent(text).splitlines()
777 leading_indent = len(real_lines[0].text) - len(dedented_lines[0])
778 for line in real_lines:
779 line.leading_indent = leading_indent
780 return result
782 @cached_property
783 def scope(self) -> Optional[ast.AST]:
784 """
785 The AST node of the innermost function, class or module being executed.
786 """
787 if not self.source.tree or not self.executing.statements:
788 return None
790 stmt = list(self.executing.statements)[0]
791 while True:
792 # Get the parent first in case the original statement is already
793 # a function definition, e.g. if we're calling a decorator
794 # In that case we still want the surrounding scope, not that function
795 stmt = stmt.parent
796 if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Module)):
797 return stmt
799 @cached_property
800 def _pygmented_scope_lines(self) -> Optional[Tuple[int, List[str]]]:
801 # noinspection PyUnresolvedReferences
802 from pygments.formatters import HtmlFormatter
804 formatter = self.options.pygments_formatter
805 scope = self.scope
806 assert_(formatter, ValueError("Must set a pygments formatter in Options"))
807 assert_(scope)
809 if isinstance(formatter, HtmlFormatter):
810 formatter.nowrap = True
812 atext = self.source.asttext()
813 node = self.executing.node
814 if node and getattr(formatter.style, "for_executing_node", False):
815 scope_start = atext.get_text_range(scope)[0]
816 start, end = atext.get_text_range(node)
817 start -= scope_start
818 end -= scope_start
819 ranges = [(start, end)]
820 else:
821 ranges = []
823 code = atext.get_text(scope)
824 lines = _pygmented_with_ranges(formatter, code, ranges)
826 start_line = self.source.line_range(scope)[0]
828 return start_line, lines
830 @cached_property
831 def variables(self) -> List[Variable]:
832 """
833 All Variable objects whose nodes are contained within .scope
834 and whose values could be safely evaluated by pure_eval.
835 """
836 if not self.scope:
837 return []
839 evaluator = Evaluator.from_frame(self.frame)
840 scope = self.scope
841 node_values = [
842 pair
843 for pair in evaluator.find_expressions(scope)
844 if is_expression_interesting(*pair)
845 ] # type: List[Tuple[ast.AST, Any]]
847 if isinstance(scope, (ast.FunctionDef, ast.AsyncFunctionDef)):
848 for node in ast.walk(scope.args):
849 if not isinstance(node, ast.arg):
850 continue
851 name = node.arg
852 try:
853 value = evaluator.names[name]
854 except KeyError:
855 pass
856 else:
857 node_values.append((node, value))
859 # Group equivalent nodes together
860 def get_text(n):
861 if isinstance(n, ast.arg):
862 return n.arg
863 else:
864 return self.source.asttext().get_text(n)
866 def normalise_node(n):
867 try:
868 # Add parens to avoid syntax errors for multiline expressions
869 return ast.parse('(' + get_text(n) + ')')
870 except Exception:
871 return n
873 grouped = group_by_key_func(
874 node_values,
875 lambda nv: ast.dump(normalise_node(nv[0])),
876 )
878 result = []
879 for group in grouped.values():
880 nodes, values = zip(*group)
881 value = values[0]
882 text = get_text(nodes[0])
883 if not text:
884 continue
885 result.append(Variable(text, nodes, value))
887 return result
889 @cached_property
890 def variables_by_lineno(self) -> Mapping[int, List[Tuple[Variable, ast.AST]]]:
891 """
892 A mapping from 1-based line numbers to lists of pairs:
893 - A Variable object
894 - A specific AST node from the variable's .nodes list that's
895 in the line at that line number.
896 """
897 result = defaultdict(list)
898 for var in self.variables:
899 for node in var.nodes:
900 for lineno in range(*self.source.line_range(node)):
901 result[lineno].append((var, node))
902 return result
904 @cached_property
905 def variables_in_lines(self) -> List[Variable]:
906 """
907 A list of Variable objects contained within the lines returned by .lines.
908 """
909 return unique_in_order(
910 var
911 for line in self.lines
912 if isinstance(line, Line)
913 for var, node in self.variables_by_lineno[line.lineno]
914 )
916 @cached_property
917 def variables_in_executing_piece(self) -> List[Variable]:
918 """
919 A list of Variable objects contained within the lines
920 in the range returned by .executing_piece.
921 """
922 return unique_in_order(
923 var
924 for lineno in self.executing_piece
925 for var, node in self.variables_by_lineno[lineno]
926 )