Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/black/comments.py: 14%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

330 statements  

1import re 

2from collections.abc import Collection, Iterator 

3from dataclasses import dataclass 

4from functools import lru_cache 

5from typing import Final, Union 

6 

7from black.mode import Mode, Preview 

8from black.nodes import ( 

9 CLOSING_BRACKETS, 

10 STANDALONE_COMMENT, 

11 STATEMENT, 

12 WHITESPACE, 

13 container_of, 

14 first_leaf_of, 

15 is_type_comment_string, 

16 make_simple_prefix, 

17 preceding_leaf, 

18 syms, 

19) 

20from blib2to3.pgen2 import token 

21from blib2to3.pytree import Leaf, Node 

22 

23# types 

24LN = Union[Leaf, Node] 

25 

26FMT_OFF: Final = {"# fmt: off", "# fmt:off", "# yapf: disable"} 

27FMT_SKIP: Final = {"# fmt: skip", "# fmt:skip"} 

28FMT_ON: Final = {"# fmt: on", "# fmt:on", "# yapf: enable"} 

29 

30# Compound statements we care about for fmt: skip handling 

31# (excludes except_clause and case_block which aren't standalone compound statements) 

32_COMPOUND_STATEMENTS: Final = STATEMENT - {syms.except_clause, syms.case_block} 

33 

34COMMENT_EXCEPTIONS = " !:#'" 

35_COMMENT_PREFIX = "# " 

36_COMMENT_LIST_SEPARATOR = ";" 

37 

38 

39@dataclass 

40class ProtoComment: 

41 """Describes a piece of syntax that is a comment. 

42 

43 It's not a :class:`blib2to3.pytree.Leaf` so that: 

44 

45 * it can be cached (`Leaf` objects should not be reused more than once as 

46 they store their lineno, column, prefix, and parent information); 

47 * `newlines` and `consumed` fields are kept separate from the `value`. This 

48 simplifies handling of special marker comments like ``# fmt: off/on``. 

49 """ 

50 

51 type: int # token.COMMENT or STANDALONE_COMMENT 

52 value: str # content of the comment 

53 newlines: int # how many newlines before the comment 

54 consumed: int # how many characters of the original leaf's prefix did we consume 

55 form_feed: bool # is there a form feed before the comment 

56 leading_whitespace: str # leading whitespace before the comment, if any 

57 

58 

59def generate_comments(leaf: LN, mode: Mode) -> Iterator[Leaf]: 

60 """Clean the prefix of the `leaf` and generate comments from it, if any. 

61 

62 Comments in lib2to3 are shoved into the whitespace prefix. This happens 

63 in `pgen2/driver.py:Driver.parse_tokens()`. This was a brilliant implementation 

64 move because it does away with modifying the grammar to include all the 

65 possible places in which comments can be placed. 

66 

67 The sad consequence for us though is that comments don't "belong" anywhere. 

68 This is why this function generates simple parentless Leaf objects for 

69 comments. We simply don't know what the correct parent should be. 

70 

71 No matter though, we can live without this. We really only need to 

72 differentiate between inline and standalone comments. The latter don't 

73 share the line with any code. 

74 

75 Inline comments are emitted as regular token.COMMENT leaves. Standalone 

76 are emitted with a fake STANDALONE_COMMENT token identifier. 

77 """ 

78 total_consumed = 0 

79 for pc in list_comments( 

80 leaf.prefix, is_endmarker=leaf.type == token.ENDMARKER, mode=mode 

81 ): 

82 total_consumed = pc.consumed 

83 prefix = make_simple_prefix(pc.newlines, pc.form_feed) 

84 yield Leaf(pc.type, pc.value, prefix=prefix) 

85 normalize_trailing_prefix(leaf, total_consumed) 

86 

87 

88@lru_cache(maxsize=4096) 

89def list_comments(prefix: str, *, is_endmarker: bool, mode: Mode) -> list[ProtoComment]: 

90 """Return a list of :class:`ProtoComment` objects parsed from the given `prefix`.""" 

91 result: list[ProtoComment] = [] 

92 if not prefix or "#" not in prefix: 

93 return result 

94 

95 consumed = 0 

96 nlines = 0 

97 ignored_lines = 0 

98 form_feed = False 

99 for index, full_line in enumerate(re.split("\r?\n|\r", prefix)): 

100 consumed += len(full_line) + 1 # adding the length of the split '\n' 

101 match = re.match(r"^(\s*)(\S.*|)$", full_line) 

102 assert match 

103 whitespace, line = match.groups() 

104 if not line: 

105 nlines += 1 

106 if "\f" in full_line: 

107 form_feed = True 

108 if not line.startswith("#"): 

109 # Escaped newlines outside of a comment are not really newlines at 

110 # all. We treat a single-line comment following an escaped newline 

111 # as a simple trailing comment. 

112 if line.endswith("\\"): 

113 ignored_lines += 1 

114 continue 

115 

116 if index == ignored_lines and not is_endmarker: 

117 comment_type = token.COMMENT # simple trailing comment 

118 else: 

119 comment_type = STANDALONE_COMMENT 

120 comment = make_comment(line, mode=mode) 

121 result.append( 

122 ProtoComment( 

123 type=comment_type, 

124 value=comment, 

125 newlines=nlines, 

126 consumed=consumed, 

127 form_feed=form_feed, 

128 leading_whitespace=whitespace, 

129 ) 

130 ) 

131 form_feed = False 

132 nlines = 0 

133 return result 

134 

135 

136def normalize_trailing_prefix(leaf: LN, total_consumed: int) -> None: 

137 """Normalize the prefix that's left over after generating comments. 

138 

139 Note: don't use backslashes for formatting or you'll lose your voting rights. 

140 """ 

141 remainder = leaf.prefix[total_consumed:] 

142 if "\\" not in remainder: 

143 nl_count = remainder.count("\n") 

144 form_feed = "\f" in remainder and remainder.endswith("\n") 

145 leaf.prefix = make_simple_prefix(nl_count, form_feed) 

146 return 

147 

148 leaf.prefix = "" 

149 

150 

151def make_comment(content: str, mode: Mode) -> str: 

152 """Return a consistently formatted comment from the given `content` string. 

153 

154 All comments (except for "##", "#!", "#:", '#'") should have a single 

155 space between the hash sign and the content. 

156 

157 If `content` didn't start with a hash sign, one is provided. 

158 

159 Comments containing fmt directives are preserved exactly as-is to respect 

160 user intent (e.g., `#no space # fmt: skip` stays as-is). 

161 """ 

162 content = content.rstrip() 

163 if not content: 

164 return "#" 

165 

166 # Preserve comments with fmt directives exactly as-is 

167 if content.startswith("#") and _contains_fmt_directive(content): 

168 return content 

169 

170 if content[0] == "#": 

171 content = content[1:] 

172 if ( 

173 content 

174 and content[0] == "\N{NO-BREAK SPACE}" 

175 and not is_type_comment_string("# " + content.lstrip(), mode=mode) 

176 ): 

177 content = " " + content[1:] # Replace NBSP by a simple space 

178 if ( 

179 Preview.standardize_type_comments in mode 

180 and content 

181 and "\N{NO-BREAK SPACE}" not in content 

182 and is_type_comment_string("#" + content, mode=mode) 

183 ): 

184 type_part, value_part = content.split(":", 1) 

185 content = type_part.strip() + ": " + value_part.strip() 

186 

187 if content and content[0] not in COMMENT_EXCEPTIONS: 

188 content = " " + content 

189 return "#" + content 

190 

191 

192def normalize_fmt_off( 

193 node: Node, mode: Mode, lines: Collection[tuple[int, int]] 

194) -> None: 

195 """Convert content between `# fmt: off`/`# fmt: on` into standalone comments.""" 

196 try_again = True 

197 while try_again: 

198 try_again = convert_one_fmt_off_pair(node, mode, lines) 

199 

200 

201def _should_process_fmt_comment( 

202 comment: ProtoComment, leaf: Leaf 

203) -> tuple[bool, bool, bool]: 

204 """Check if comment should be processed for fmt handling. 

205 

206 Returns (should_process, is_fmt_off, is_fmt_skip). 

207 """ 

208 is_fmt_off = _contains_fmt_directive(comment.value, FMT_OFF) 

209 is_fmt_skip = _contains_fmt_directive(comment.value, FMT_SKIP) 

210 

211 if not is_fmt_off and not is_fmt_skip: 

212 return False, False, False 

213 

214 # Invalid use when `# fmt: off` is applied before a closing bracket 

215 if is_fmt_off and leaf.type in CLOSING_BRACKETS: 

216 return False, False, False 

217 

218 return True, is_fmt_off, is_fmt_skip 

219 

220 

221def _is_valid_standalone_fmt_comment( 

222 comment: ProtoComment, leaf: Leaf, is_fmt_off: bool, is_fmt_skip: bool 

223) -> bool: 

224 """Check if comment is a valid standalone fmt directive. 

225 

226 We only want standalone comments. If there's no previous leaf or if 

227 the previous leaf is indentation, it's a standalone comment in disguise. 

228 """ 

229 if comment.type == STANDALONE_COMMENT: 

230 return True 

231 

232 prev = preceding_leaf(leaf) 

233 if not prev: 

234 return True 

235 

236 # Treat STANDALONE_COMMENT nodes as whitespace for check 

237 if is_fmt_off and prev.type not in WHITESPACE and prev.type != STANDALONE_COMMENT: 

238 return False 

239 if is_fmt_skip and prev.type in WHITESPACE: 

240 return False 

241 

242 return True 

243 

244 

245def _handle_comment_only_fmt_block( 

246 leaf: Leaf, 

247 comment: ProtoComment, 

248 previous_consumed: int, 

249 mode: Mode, 

250) -> bool: 

251 """Handle fmt:off/on blocks that contain only comments. 

252 

253 Returns True if a block was converted, False otherwise. 

254 """ 

255 all_comments = list_comments(leaf.prefix, is_endmarker=False, mode=mode) 

256 

257 # Find the first fmt:off and its matching fmt:on 

258 fmt_off_idx = None 

259 fmt_on_idx = None 

260 for idx, c in enumerate(all_comments): 

261 if fmt_off_idx is None and c.value in FMT_OFF: 

262 fmt_off_idx = idx 

263 if fmt_off_idx is not None and idx > fmt_off_idx and c.value in FMT_ON: 

264 fmt_on_idx = idx 

265 break 

266 

267 # Only proceed if we found both directives 

268 if fmt_on_idx is None or fmt_off_idx is None: 

269 return False 

270 

271 comment = all_comments[fmt_off_idx] 

272 fmt_on_comment = all_comments[fmt_on_idx] 

273 original_prefix = leaf.prefix 

274 

275 # Build the hidden value 

276 start_pos = comment.consumed 

277 end_pos = fmt_on_comment.consumed 

278 content_between_and_fmt_on = original_prefix[start_pos:end_pos] 

279 hidden_value = comment.value + "\n" + content_between_and_fmt_on 

280 

281 if hidden_value.endswith("\n"): 

282 hidden_value = hidden_value[:-1] 

283 

284 # Build the standalone comment prefix - preserve all content before fmt:off 

285 # including any comments that precede it 

286 if fmt_off_idx == 0: 

287 # No comments before fmt:off, use previous_consumed 

288 pre_fmt_off_consumed = previous_consumed 

289 else: 

290 # Use the consumed position of the last comment before fmt:off 

291 # This preserves all comments and content before the fmt:off directive 

292 pre_fmt_off_consumed = all_comments[fmt_off_idx - 1].consumed 

293 

294 standalone_comment_prefix = ( 

295 original_prefix[:pre_fmt_off_consumed] + "\n" * comment.newlines 

296 ) 

297 

298 fmt_off_prefix = original_prefix.split(comment.value)[0] 

299 if "\n" in fmt_off_prefix: 

300 fmt_off_prefix = fmt_off_prefix.split("\n")[-1] 

301 standalone_comment_prefix += fmt_off_prefix 

302 

303 # Update leaf prefix 

304 leaf.prefix = original_prefix[fmt_on_comment.consumed :] 

305 

306 # Insert the STANDALONE_COMMENT 

307 parent = leaf.parent 

308 assert parent is not None, "INTERNAL ERROR: fmt: on/off handling (prefix only)" 

309 

310 leaf_idx = None 

311 for idx, child in enumerate(parent.children): 

312 if child is leaf: 

313 leaf_idx = idx 

314 break 

315 

316 assert leaf_idx is not None, "INTERNAL ERROR: fmt: on/off handling (leaf index)" 

317 

318 parent.insert_child( 

319 leaf_idx, 

320 Leaf( 

321 STANDALONE_COMMENT, 

322 hidden_value, 

323 prefix=standalone_comment_prefix, 

324 fmt_pass_converted_first_leaf=None, 

325 ), 

326 ) 

327 return True 

328 

329 

330def convert_one_fmt_off_pair( 

331 node: Node, mode: Mode, lines: Collection[tuple[int, int]] 

332) -> bool: 

333 """Convert content of a single `# fmt: off`/`# fmt: on` into a standalone comment. 

334 

335 Returns True if a pair was converted. 

336 """ 

337 for leaf in node.leaves(): 

338 # Skip STANDALONE_COMMENT nodes that were created by fmt:off/on processing 

339 # to avoid reprocessing them in subsequent iterations 

340 if ( 

341 leaf.type == STANDALONE_COMMENT 

342 and hasattr(leaf, "fmt_pass_converted_first_leaf") 

343 and leaf.fmt_pass_converted_first_leaf is None 

344 ): 

345 continue 

346 

347 previous_consumed = 0 

348 for comment in list_comments(leaf.prefix, is_endmarker=False, mode=mode): 

349 should_process, is_fmt_off, is_fmt_skip = _should_process_fmt_comment( 

350 comment, leaf 

351 ) 

352 if not should_process: 

353 previous_consumed = comment.consumed 

354 continue 

355 

356 if not _is_valid_standalone_fmt_comment( 

357 comment, leaf, is_fmt_off, is_fmt_skip 

358 ): 

359 previous_consumed = comment.consumed 

360 continue 

361 

362 ignored_nodes = list(generate_ignored_nodes(leaf, comment, mode)) 

363 

364 # Handle comment-only blocks 

365 if not ignored_nodes and is_fmt_off: 

366 if _handle_comment_only_fmt_block( 

367 leaf, comment, previous_consumed, mode 

368 ): 

369 return True 

370 continue 

371 

372 # Need actual nodes to process 

373 if not ignored_nodes: 

374 continue 

375 

376 # Handle regular fmt blocks 

377 

378 _handle_regular_fmt_block( 

379 ignored_nodes, 

380 comment, 

381 previous_consumed, 

382 is_fmt_skip, 

383 lines, 

384 leaf, 

385 ) 

386 return True 

387 

388 return False 

389 

390 

391def _handle_regular_fmt_block( 

392 ignored_nodes: list[LN], 

393 comment: ProtoComment, 

394 previous_consumed: int, 

395 is_fmt_skip: bool, 

396 lines: Collection[tuple[int, int]], 

397 leaf: Leaf, 

398) -> None: 

399 """Handle fmt blocks with actual AST nodes.""" 

400 first = ignored_nodes[0] # Can be a container node with the `leaf`. 

401 parent = first.parent 

402 prefix = first.prefix 

403 

404 if comment.value in FMT_OFF: 

405 first.prefix = prefix[comment.consumed :] 

406 if is_fmt_skip: 

407 first.prefix = "" 

408 standalone_comment_prefix = prefix 

409 else: 

410 standalone_comment_prefix = prefix[:previous_consumed] + "\n" * comment.newlines 

411 

412 hidden_value = "".join(str(n) for n in ignored_nodes) 

413 comment_lineno = leaf.lineno - comment.newlines 

414 

415 if comment.value in FMT_OFF: 

416 fmt_off_prefix = "" 

417 if len(lines) > 0 and not any( 

418 line[0] <= comment_lineno <= line[1] for line in lines 

419 ): 

420 # keeping indentation of comment by preserving original whitespaces. 

421 fmt_off_prefix = prefix.split(comment.value)[0] 

422 if "\n" in fmt_off_prefix: 

423 fmt_off_prefix = fmt_off_prefix.split("\n")[-1] 

424 standalone_comment_prefix += fmt_off_prefix 

425 hidden_value = comment.value + "\n" + hidden_value 

426 

427 if is_fmt_skip: 

428 hidden_value += comment.leading_whitespace + comment.value 

429 

430 if hidden_value.endswith("\n"): 

431 # That happens when one of the `ignored_nodes` ended with a NEWLINE 

432 # leaf (possibly followed by a DEDENT). 

433 hidden_value = hidden_value[:-1] 

434 

435 first_idx: int | None = None 

436 for ignored in ignored_nodes: 

437 index = ignored.remove() 

438 if first_idx is None: 

439 first_idx = index 

440 

441 assert parent is not None, "INTERNAL ERROR: fmt: on/off handling (1)" 

442 assert first_idx is not None, "INTERNAL ERROR: fmt: on/off handling (2)" 

443 

444 parent.insert_child( 

445 first_idx, 

446 Leaf( 

447 STANDALONE_COMMENT, 

448 hidden_value, 

449 prefix=standalone_comment_prefix, 

450 fmt_pass_converted_first_leaf=first_leaf_of(first), 

451 ), 

452 ) 

453 

454 

455def generate_ignored_nodes( 

456 leaf: Leaf, comment: ProtoComment, mode: Mode 

457) -> Iterator[LN]: 

458 """Starting from the container of `leaf`, generate all leaves until `# fmt: on`. 

459 

460 If comment is skip, returns leaf only. 

461 Stops at the end of the block. 

462 """ 

463 if _contains_fmt_directive(comment.value, FMT_SKIP): 

464 yield from _generate_ignored_nodes_from_fmt_skip(leaf, comment, mode) 

465 return 

466 container: LN | None = container_of(leaf) 

467 while container is not None and container.type != token.ENDMARKER: 

468 if is_fmt_on(container, mode=mode): 

469 return 

470 

471 # fix for fmt: on in children 

472 if children_contains_fmt_on(container, mode=mode): 

473 for index, child in enumerate(container.children): 

474 if isinstance(child, Leaf) and is_fmt_on(child, mode=mode): 

475 if child.type in CLOSING_BRACKETS: 

476 # This means `# fmt: on` is placed at a different bracket level 

477 # than `# fmt: off`. This is an invalid use, but as a courtesy, 

478 # we include this closing bracket in the ignored nodes. 

479 # The alternative is to fail the formatting. 

480 yield child 

481 return 

482 if ( 

483 child.type == token.INDENT 

484 and index < len(container.children) - 1 

485 and children_contains_fmt_on( 

486 container.children[index + 1], mode=mode 

487 ) 

488 ): 

489 # This means `# fmt: on` is placed right after an indentation 

490 # level, and we shouldn't swallow the previous INDENT token. 

491 return 

492 if children_contains_fmt_on(child, mode=mode): 

493 return 

494 yield child 

495 else: 

496 if container.type == token.DEDENT and container.next_sibling is None: 

497 # This can happen when there is no matching `# fmt: on` comment at the 

498 # same level as `# fmt: on`. We need to keep this DEDENT. 

499 return 

500 yield container 

501 container = container.next_sibling 

502 

503 

504def _find_compound_statement_context(parent: Node) -> Node | None: 

505 """Return the body node of a compound statement if we should respect fmt: skip. 

506 

507 This handles one-line compound statements like: 

508 if condition: body # fmt: skip 

509 

510 When Black expands such statements, they temporarily look like: 

511 if condition: 

512 body # fmt: skip 

513 

514 In both cases, we want to return the body node (either the simple_stmt directly 

515 or the suite containing it). 

516 """ 

517 if parent.type != syms.simple_stmt: 

518 return None 

519 

520 if not isinstance(parent.parent, Node): 

521 return None 

522 

523 # Case 1: Expanded form after Black's initial formatting pass. 

524 # The one-liner has been split across multiple lines: 

525 # if True: 

526 # print("a"); print("b") # fmt: skip 

527 # Structure: compound_stmt -> suite -> simple_stmt 

528 if ( 

529 parent.parent.type == syms.suite 

530 and isinstance(parent.parent.parent, Node) 

531 and parent.parent.parent.type in _COMPOUND_STATEMENTS 

532 ): 

533 return parent.parent 

534 

535 # Case 2: Original one-line form from the input source. 

536 # The statement is still on a single line: 

537 # if True: print("a"); print("b") # fmt: skip 

538 # Structure: compound_stmt -> simple_stmt 

539 if parent.parent.type in _COMPOUND_STATEMENTS: 

540 return parent 

541 

542 return None 

543 

544 

545def _should_keep_compound_statement_inline( 

546 body_node: Node, simple_stmt_parent: Node 

547) -> bool: 

548 """Check if a compound statement should be kept on one line. 

549 

550 Returns True only for compound statements with semicolon-separated bodies, 

551 like: if True: print("a"); print("b") # fmt: skip 

552 """ 

553 # Check if there are semicolons in the body 

554 for leaf in body_node.leaves(): 

555 if leaf.type == token.SEMI: 

556 # Verify it's a single-line body (one simple_stmt) 

557 if body_node.type == syms.suite: 

558 # After formatting: check suite has one simple_stmt child 

559 simple_stmts = [ 

560 child 

561 for child in body_node.children 

562 if child.type == syms.simple_stmt 

563 ] 

564 return len(simple_stmts) == 1 and simple_stmts[0] is simple_stmt_parent 

565 else: 

566 # Original form: body_node IS the simple_stmt 

567 return body_node is simple_stmt_parent 

568 return False 

569 

570 

571def _get_compound_statement_header( 

572 body_node: Node, simple_stmt_parent: Node 

573) -> list[LN]: 

574 """Get header nodes for a compound statement that should be preserved inline.""" 

575 if not _should_keep_compound_statement_inline(body_node, simple_stmt_parent): 

576 return [] 

577 

578 # Get the compound statement (parent of body) 

579 compound_stmt = body_node.parent 

580 if compound_stmt is None or compound_stmt.type not in _COMPOUND_STATEMENTS: 

581 return [] 

582 

583 # Collect all header leaves before the body 

584 header_leaves: list[LN] = [] 

585 for child in compound_stmt.children: 

586 if child is body_node: 

587 break 

588 if isinstance(child, Leaf): 

589 if child.type not in (token.NEWLINE, token.INDENT): 

590 header_leaves.append(child) 

591 else: 

592 header_leaves.extend(child.leaves()) 

593 return header_leaves 

594 

595 

596def _generate_ignored_nodes_from_fmt_skip( 

597 leaf: Leaf, comment: ProtoComment, mode: Mode 

598) -> Iterator[LN]: 

599 """Generate all leaves that should be ignored by the `# fmt: skip` from `leaf`.""" 

600 prev_sibling = leaf.prev_sibling 

601 parent = leaf.parent 

602 ignored_nodes: list[LN] = [] 

603 # Need to properly format the leaf prefix to compare it to comment.value, 

604 # which is also formatted 

605 comments = list_comments(leaf.prefix, is_endmarker=False, mode=mode) 

606 if not comments or comment.value != comments[0].value: 

607 return 

608 if prev_sibling is not None: 

609 leaf.prefix = leaf.prefix[comment.consumed :] 

610 

611 if Preview.fix_fmt_skip_in_one_liners not in mode: 

612 siblings = [prev_sibling] 

613 while ( 

614 "\n" not in prev_sibling.prefix 

615 and prev_sibling.prev_sibling is not None 

616 ): 

617 prev_sibling = prev_sibling.prev_sibling 

618 siblings.insert(0, prev_sibling) 

619 yield from siblings 

620 return 

621 

622 # Generates the nodes to be ignored by `fmt: skip`. 

623 

624 # Nodes to ignore are the ones on the same line as the 

625 # `# fmt: skip` comment, excluding the `# fmt: skip` 

626 # node itself. 

627 

628 # Traversal process (starting at the `# fmt: skip` node): 

629 # 1. Move to the `prev_sibling` of the current node. 

630 # 2. If `prev_sibling` has children, go to its rightmost leaf. 

631 # 3. If there's no `prev_sibling`, move up to the parent 

632 # node and repeat. 

633 # 4. Continue until: 

634 # a. You encounter an `INDENT` or `NEWLINE` node (indicates 

635 # start of the line). 

636 # b. You reach the root node. 

637 

638 # Include all visited LEAVES in the ignored list, except INDENT 

639 # or NEWLINE leaves. 

640 

641 current_node = prev_sibling 

642 ignored_nodes = [current_node] 

643 if current_node.prev_sibling is None and current_node.parent is not None: 

644 current_node = current_node.parent 

645 while "\n" not in current_node.prefix and current_node.prev_sibling is not None: 

646 leaf_nodes = list(current_node.prev_sibling.leaves()) 

647 current_node = leaf_nodes[-1] if leaf_nodes else current_node 

648 

649 if current_node.type in (token.NEWLINE, token.INDENT): 

650 current_node.prefix = "" 

651 break 

652 

653 # Special case for with expressions 

654 # Without this, we can stuck inside the asexpr_test's children's children 

655 if ( 

656 current_node.parent 

657 and current_node.parent.type == syms.asexpr_test 

658 and current_node.parent.parent 

659 and current_node.parent.parent.type == syms.with_stmt 

660 ): 

661 current_node = current_node.parent 

662 

663 ignored_nodes.insert(0, current_node) 

664 

665 if current_node.prev_sibling is None and current_node.parent is not None: 

666 current_node = current_node.parent 

667 

668 # Special handling for compound statements with semicolon-separated bodies 

669 if Preview.fix_fmt_skip_in_one_liners in mode and isinstance(parent, Node): 

670 body_node = _find_compound_statement_context(parent) 

671 if body_node is not None: 

672 header_nodes = _get_compound_statement_header(body_node, parent) 

673 if header_nodes: 

674 ignored_nodes = header_nodes + ignored_nodes 

675 

676 yield from ignored_nodes 

677 elif ( 

678 parent is not None and parent.type == syms.suite and leaf.type == token.NEWLINE 

679 ): 

680 # The `# fmt: skip` is on the colon line of the if/while/def/class/... 

681 # statements. The ignored nodes should be previous siblings of the 

682 # parent suite node. 

683 leaf.prefix = "" 

684 parent_sibling = parent.prev_sibling 

685 while parent_sibling is not None and parent_sibling.type != syms.suite: 

686 ignored_nodes.insert(0, parent_sibling) 

687 parent_sibling = parent_sibling.prev_sibling 

688 # Special case for `async_stmt` where the ASYNC token is on the 

689 # grandparent node. 

690 grandparent = parent.parent 

691 if ( 

692 grandparent is not None 

693 and grandparent.prev_sibling is not None 

694 and grandparent.prev_sibling.type == token.ASYNC 

695 ): 

696 ignored_nodes.insert(0, grandparent.prev_sibling) 

697 yield from iter(ignored_nodes) 

698 

699 

700def is_fmt_on(container: LN, mode: Mode) -> bool: 

701 """Determine whether formatting is switched on within a container. 

702 Determined by whether the last `# fmt:` comment is `on` or `off`. 

703 """ 

704 fmt_on = False 

705 for comment in list_comments(container.prefix, is_endmarker=False, mode=mode): 

706 if comment.value in FMT_ON: 

707 fmt_on = True 

708 elif comment.value in FMT_OFF: 

709 fmt_on = False 

710 return fmt_on 

711 

712 

713def children_contains_fmt_on(container: LN, mode: Mode) -> bool: 

714 """Determine if children have formatting switched on.""" 

715 for child in container.children: 

716 leaf = first_leaf_of(child) 

717 if leaf is not None and is_fmt_on(leaf, mode=mode): 

718 return True 

719 

720 return False 

721 

722 

723def contains_pragma_comment(comment_list: list[Leaf]) -> bool: 

724 """ 

725 Returns: 

726 True iff one of the comments in @comment_list is a pragma used by one 

727 of the more common static analysis tools for python (e.g. mypy, flake8, 

728 pylint). 

729 """ 

730 for comment in comment_list: 

731 if comment.value.startswith(("# type:", "# noqa", "# pylint:")): 

732 return True 

733 

734 return False 

735 

736 

737def _contains_fmt_directive( 

738 comment_line: str, directives: set[str] = FMT_OFF | FMT_ON | FMT_SKIP 

739) -> bool: 

740 """ 

741 Checks if the given comment contains format directives, alone or paired with 

742 other comments. 

743 

744 Defaults to checking all directives (skip, off, on, yapf), but can be 

745 narrowed to specific ones. 

746 

747 Matching styles: 

748 # foobar <-- single comment 

749 # foobar # foobar # foobar <-- multiple comments 

750 # foobar; foobar <-- list of comments (; separated) 

751 """ 

752 semantic_comment_blocks = [ 

753 comment_line, 

754 *[ 

755 _COMMENT_PREFIX + comment.strip() 

756 for comment in comment_line.split(_COMMENT_PREFIX)[1:] 

757 ], 

758 *[ 

759 _COMMENT_PREFIX + comment.strip() 

760 for comment in comment_line.strip(_COMMENT_PREFIX).split( 

761 _COMMENT_LIST_SEPARATOR 

762 ) 

763 ], 

764 ] 

765 

766 return any(comment in directives for comment in semantic_comment_blocks)