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

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 

14 

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

23 

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. 

31 

32Typically this will be converted to a pair of markers by markers_from_ranges. 

33""" 

34 

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. 

44 

45Typically this would be created from a RangeInLine by markers_from_ranges. 

46Then use Line.render to insert the markers correctly. 

47""" 

48 

49 

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 

64 

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__ 

80 

81 

82class Source(executing.Source): 

83 """ 

84 The source code of a single file and associated metadata. 

85 

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. 

90 

91 Don't construct this class. Get an instance from frame_info.source. 

92 """ 

93 

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

102 

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 ) 

111 

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 ] 

119 

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 

133 

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) 

140 

141 def is_blank(i): 

142 try: 

143 return not self.lines[i - 1].strip() 

144 except IndexError: 

145 return False 

146 

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) 

154 

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 

174 

175 yield start, end 

176 

177 def line_range(self, node: ast.AST) -> Tuple[int, int]: 

178 return line_range(self.asttext(), node) 

179 

180 

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.  

185 

186 before and after are the number of pieces of context to include in a frame 

187 in addition to the executing piece. 

188 

189 include_signature is whether to include the function signature as a piece in a frame. 

190 

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 

209 

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

214 

215 

216class LineGap(object): 

217 """ 

218 A singleton representing one or more lines of source code that were skipped 

219 in FrameInfo.lines. 

220 

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" 

228 

229 

230LINE_GAP = LineGap() 

231 

232 

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 

241 

242 

243class Line(object): 

244 """ 

245 A single line of source code for a particular stack frame. 

246 

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. 

250 

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 

261 

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] 

278 

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) 

282 

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 

290 

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] 

299 

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 ] 

315 

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 ] 

327 

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 ) 

337 

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 [] 

346 

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) 

357 

358 if not (start <= self.lineno <= end): 

359 return None 

360 

361 if start != self.lineno: 

362 range_start = common_indent 

363 

364 if end != self.lineno: 

365 range_end = len(self.text) 

366 

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 

372 

373 return RangeInLine(range_start, range_end, data) 

374 

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 

396 

397 text = self.text 

398 

399 # This just makes the loop below simpler 

400 markers = list(markers) + [MarkerInLine(position=len(text), is_start=False, string='')] 

401 

402 markers.sort(key=lambda t: t[:2]) 

403 

404 parts = [] 

405 if strip_leading_indent: 

406 start = self.leading_indent 

407 else: 

408 start = 0 

409 original_start = start 

410 

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) 

417 

418 # Ensure that start >= leading_indent 

419 start = max(marker.position, original_start) 

420 return ''.join(parts) 

421 

422 

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 

438 

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

442 

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 

448 

449 

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) 

454 

455 class NewStyle(style): 

456 for_executing_node = True 

457 

458 styles = { 

459 **style.styles, 

460 **{ 

461 k.ExecutingNode: v + " " + modifier 

462 for k, v in style.styles.items() 

463 } 

464 } 

465 

466 return NewStyle 

467 

468 

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. 

474 

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 

489 

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 ) 

506 

507 def __repr__(self): 

508 return '<{self.__class__.__name__} {self.description}>'.format(self=self) 

509 

510 

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. 

516 

517 Or use the classmethod FrameInfo.stack_data() for an iterator of FrameInfo and 

518 RepeatedFrames objects.  

519 

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. 

534 

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 

539 

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 

557 

558 

559 def __repr__(self): 

560 return "{self.__class__.__name__}({self.frame})".format(self=self) 

561 

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. 

574 

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

579 

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] 

585 

586 def mapper(f): 

587 return cls(f, options) 

588 

589 if not collapse_repeated_frames: 

590 yield from map(mapper, stack) 

591 return 

592 

593 def _frame_key(x): 

594 frame, lineno = frame_and_lineno(x) 

595 return frame.f_code, lineno 

596 

597 yield from collapse_repeated( 

598 stack, 

599 mapper=mapper, 

600 collapser=RepeatedFrames, 

601 key=_frame_key, 

602 ) 

603 

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 

613 

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 ] 

620 

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 

629 

630 if ( 

631 os.path.isabs(result) or 

632 ( 

633 result.startswith("<") and 

634 result.endswith(">") 

635 ) 

636 ): 

637 return result 

638 

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 

651 

652 return result 

653 

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 ) 

665 

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 [] 

680 

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] 

685 

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

693 

694 return pieces 

695 

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. 

701 

702 Intended to be used only internally. 

703 """ 

704 indents = [] 

705 lines = [line for line in self.lines if isinstance(line, Line)] 

706 

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) 

712 

713 if len(indents) <= 1: 

714 return 0 

715 

716 return min(indents[1:]) 

717 

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. 

725 

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. 

731 

732 The Line objects are all within the ranges from .included_pieces. 

733 """ 

734 pieces = self.included_pieces 

735 if not pieces: 

736 return [] 

737 

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

755 

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 

765 

766 real_lines = [ 

767 line 

768 for line in result 

769 if isinstance(line, Line) 

770 ] 

771 

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 

781 

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 

789 

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 

798 

799 @cached_property 

800 def _pygmented_scope_lines(self) -> Optional[Tuple[int, List[str]]]: 

801 # noinspection PyUnresolvedReferences 

802 from pygments.formatters import HtmlFormatter 

803 

804 formatter = self.options.pygments_formatter 

805 scope = self.scope 

806 assert_(formatter, ValueError("Must set a pygments formatter in Options")) 

807 assert_(scope) 

808 

809 if isinstance(formatter, HtmlFormatter): 

810 formatter.nowrap = True 

811 

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 = [] 

822 

823 code = atext.get_text(scope) 

824 lines = _pygmented_with_ranges(formatter, code, ranges) 

825 

826 start_line = self.source.line_range(scope)[0] 

827 

828 return start_line, lines 

829 

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 [] 

838 

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]] 

846 

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

858 

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) 

865 

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 

872 

873 grouped = group_by_key_func( 

874 node_values, 

875 lambda nv: ast.dump(normalise_node(nv[0])), 

876 ) 

877 

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

886 

887 return result 

888 

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 

903 

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 ) 

915 

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 )