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

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

411 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 

8from black.nodes import ( 

9 CLOSING_BRACKETS, 

10 OPENING_BRACKETS, 

11 STANDALONE_COMMENT, 

12 STATEMENT, 

13 WHITESPACE, 

14 container_of, 

15 first_leaf_of, 

16 is_type_comment_string, 

17 make_simple_prefix, 

18 preceding_leaf, 

19 syms, 

20) 

21from blib2to3.pgen2 import token 

22from blib2to3.pytree import Leaf, Node 

23 

24# types 

25LN = Union[Leaf, Node] 

26 

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

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

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

30 

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

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

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

34 

35COMMENT_EXCEPTIONS = " !:#'" 

36_COMMENT_PREFIX = "# " 

37_COMMENT_LIST_SEPARATOR = ";" 

38 

39 

40@dataclass 

41class ProtoComment: 

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

43 

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

45 

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

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

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

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

50 """ 

51 

52 type: int # token.COMMENT or STANDALONE_COMMENT 

53 value: str # content of the comment 

54 newlines: int # how many newlines before the comment 

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

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

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

58 

59 

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

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

62 

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

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

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

66 possible places in which comments can be placed. 

67 

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

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

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

71 

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

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

74 share the line with any code. 

75 

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

77 are emitted with a fake STANDALONE_COMMENT token identifier. 

78 """ 

79 total_consumed = 0 

80 for pc in list_comments( 

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

82 ): 

83 total_consumed = pc.consumed 

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

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

86 normalize_trailing_prefix(leaf, total_consumed) 

87 

88 

89@lru_cache(maxsize=4096) 

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

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

92 result: list[ProtoComment] = [] 

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

94 return result 

95 

96 consumed = 0 

97 nlines = 0 

98 ignored_lines = 0 

99 form_feed = False 

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

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

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

103 assert match 

104 whitespace, line = match.groups() 

105 if not line: 

106 nlines += 1 

107 if "\f" in full_line: 

108 form_feed = True 

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

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

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

112 # as a simple trailing comment. 

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

114 ignored_lines += 1 

115 continue 

116 

117 if index == ignored_lines and not is_endmarker: 

118 comment_type = token.COMMENT # simple trailing comment 

119 else: 

120 comment_type = STANDALONE_COMMENT 

121 comment = make_comment(line, mode=mode) 

122 result.append( 

123 ProtoComment( 

124 type=comment_type, 

125 value=comment, 

126 newlines=nlines, 

127 consumed=consumed, 

128 form_feed=form_feed, 

129 leading_whitespace=whitespace, 

130 ) 

131 ) 

132 form_feed = False 

133 nlines = 0 

134 return result 

135 

136 

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

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

139 

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

141 """ 

142 remainder = leaf.prefix[total_consumed:] 

143 if "\\" not in remainder: 

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

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

146 leaf.prefix = make_simple_prefix(nl_count, form_feed) 

147 return 

148 

149 leaf.prefix = "" 

150 

151 

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

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

154 

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

156 space between the hash sign and the content. 

157 

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

159 

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

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

162 """ 

163 content = content.rstrip() 

164 if not content: 

165 return "#" 

166 

167 # Preserve comments with fmt directives exactly as-is 

168 if content.startswith("#") and contains_fmt_directive(content): 

169 return content 

170 

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

172 content = content[1:] 

173 if ( 

174 content 

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

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

177 ): 

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

179 if ( 

180 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 # Walk the leaves once and resume scanning from the last converted position 

197 # instead of restarting from the root for every pair. Converting a pair only 

198 # mutates the tree at or after the converted leaf, so earlier leaves never 

199 # need to be revisited. 

200 leaves = list(node.leaves()) 

201 # Per-parent hint of where the previous conversion removed a child, so the 

202 # left-to-right removals below don't rescan each parent's children list from 

203 # index 0 every time (quadratic when a file has many `# fmt: off` blocks 

204 # sharing one parent). 

205 search_hints: dict[int, int] = {} 

206 i = 0 

207 while i < len(leaves): 

208 i = convert_one_fmt_off_pair(node, leaves, i, mode, lines, search_hints) 

209 

210 

211def _should_process_fmt_comment( 

212 comment: ProtoComment, leaf: Leaf 

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

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

215 

216 Returns (should_process, is_fmt_off, is_fmt_skip). 

217 """ 

218 is_fmt_off = contains_fmt_directive(comment.value, FMT_OFF) 

219 is_fmt_skip = contains_fmt_directive(comment.value, FMT_SKIP) 

220 

221 if not is_fmt_off and not is_fmt_skip: 

222 return False, False, False 

223 

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

225 if is_fmt_off and leaf.type in CLOSING_BRACKETS: 

226 return False, False, False 

227 

228 return True, is_fmt_off, is_fmt_skip 

229 

230 

231def _is_valid_standalone_fmt_comment( 

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

233) -> bool: 

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

235 

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

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

238 """ 

239 if comment.type == STANDALONE_COMMENT: 

240 return True 

241 

242 prev = preceding_leaf(leaf) 

243 if not prev: 

244 return True 

245 

246 # Treat STANDALONE_COMMENT nodes as whitespace for check 

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

248 return False 

249 if is_fmt_skip and prev.type in WHITESPACE: 

250 return False 

251 

252 return True 

253 

254 

255def _handle_comment_only_fmt_block( 

256 leaf: Leaf, 

257 comment: ProtoComment, 

258 previous_consumed: int, 

259 mode: Mode, 

260) -> bool: 

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

262 

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

264 """ 

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

266 

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

268 fmt_off_idx = None 

269 fmt_on_idx = None 

270 for idx, c in enumerate(all_comments): 

271 if fmt_off_idx is None and contains_fmt_directive(c.value, FMT_OFF): 

272 fmt_off_idx = idx 

273 if ( 

274 fmt_off_idx is not None 

275 and idx > fmt_off_idx 

276 and contains_fmt_directive(c.value, FMT_ON) 

277 ): 

278 fmt_on_idx = idx 

279 break 

280 

281 # Only proceed if we found both directives 

282 if fmt_on_idx is None or fmt_off_idx is None: 

283 return False 

284 

285 comment = all_comments[fmt_off_idx] 

286 fmt_on_comment = all_comments[fmt_on_idx] 

287 original_prefix = leaf.prefix 

288 

289 # Build the hidden value 

290 start_pos = comment.consumed 

291 end_pos = fmt_on_comment.consumed 

292 content_between_and_fmt_on = original_prefix[start_pos:end_pos] 

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

294 

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

296 hidden_value = hidden_value[:-1] 

297 

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

299 # including any comments that precede it 

300 if fmt_off_idx == 0: 

301 # No comments before fmt:off, use previous_consumed 

302 pre_fmt_off_consumed = previous_consumed 

303 else: 

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

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

306 pre_fmt_off_consumed = all_comments[fmt_off_idx - 1].consumed 

307 

308 preceding_prefix = original_prefix[:pre_fmt_off_consumed] 

309 if fmt_off_idx > 0: 

310 preceding_prefix = preceding_prefix.lstrip("\r\n") 

311 

312 standalone_comment_prefix = preceding_prefix + "\n" * comment.newlines 

313 

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

315 if "\n" in fmt_off_prefix: 

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

317 standalone_comment_prefix += fmt_off_prefix 

318 

319 # Update leaf prefix 

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

321 

322 # Insert the STANDALONE_COMMENT 

323 parent = leaf.parent 

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

325 

326 leaf_idx = None 

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

328 if child is leaf: 

329 leaf_idx = idx 

330 break 

331 

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

333 

334 parent.insert_child( 

335 leaf_idx, 

336 Leaf( 

337 STANDALONE_COMMENT, 

338 hidden_value, 

339 prefix=standalone_comment_prefix, 

340 fmt_pass_converted_first_leaf=None, 

341 ), 

342 ) 

343 return True 

344 

345 

346def convert_one_fmt_off_pair( 

347 node: Node, 

348 leaves: list[Leaf], 

349 start: int, 

350 mode: Mode, 

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

352 search_hints: dict[int, int] | None = None, 

353) -> int: 

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

355 

356 Scans `leaves` starting at `start`. Returns the index to resume scanning from: 

357 the index of the leaf that was just converted (so further directives on the same 

358 leaf are reconsidered), or ``len(leaves)`` when no further pair is found. 

359 """ 

360 for idx in range(start, len(leaves)): 

361 leaf = leaves[idx] 

362 # A previous conversion may have detached this leaf from the tree (e.g. it 

363 # was absorbed into a fmt: off block); such leaves are already handled. 

364 if not _is_attached(leaf, node): 

365 continue 

366 

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

368 # to avoid reprocessing them in subsequent iterations 

369 if leaf.type == STANDALONE_COMMENT and hasattr( 

370 leaf, "fmt_pass_converted_first_leaf" 

371 ): 

372 continue 

373 

374 previous_consumed = 0 

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

376 should_process, is_fmt_off, is_fmt_skip = _should_process_fmt_comment( 

377 comment, leaf 

378 ) 

379 if not should_process: 

380 previous_consumed = comment.consumed 

381 continue 

382 

383 if not _is_valid_standalone_fmt_comment( 

384 comment, leaf, is_fmt_off, is_fmt_skip 

385 ): 

386 previous_consumed = comment.consumed 

387 continue 

388 

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

390 

391 # Handle comment-only blocks 

392 if not ignored_nodes and is_fmt_off: 

393 if _handle_comment_only_fmt_block( 

394 leaf, comment, previous_consumed, mode 

395 ): 

396 return idx 

397 continue 

398 

399 # Need actual nodes to process 

400 if not ignored_nodes: 

401 continue 

402 

403 # Handle regular fmt blocks 

404 

405 _handle_regular_fmt_block( 

406 ignored_nodes, 

407 comment, 

408 previous_consumed, 

409 is_fmt_skip, 

410 lines, 

411 leaf, 

412 search_hints, 

413 ) 

414 return idx 

415 

416 return len(leaves) 

417 

418 

419def _is_attached(leaf: Leaf, root: Node) -> bool: 

420 """Return whether `leaf` is still reachable from `root` through its parents.""" 

421 current: LN | None = leaf 

422 while current is not None: 

423 if current is root: 

424 return True 

425 current = current.parent 

426 return False 

427 

428 

429def _handle_regular_fmt_block( 

430 ignored_nodes: list[LN], 

431 comment: ProtoComment, 

432 previous_consumed: int, 

433 is_fmt_skip: bool, 

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

435 leaf: Leaf, 

436 search_hints: dict[int, int] | None = None, 

437) -> None: 

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

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

440 parent = first.parent 

441 prefix = first.prefix 

442 

443 if contains_fmt_directive(comment.value, FMT_OFF): 

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

445 if is_fmt_skip: 

446 first.prefix = "" 

447 standalone_comment_prefix = prefix 

448 else: 

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

450 

451 # Ensure STANDALONE_COMMENT nodes have trailing newlines when stringified 

452 # This prevents multiple fmt: skip comments from being concatenated on one line 

453 parts = [] 

454 for node in ignored_nodes: 

455 if isinstance(node, Leaf) and node.type == STANDALONE_COMMENT: 

456 # Add newline after STANDALONE_COMMENT Leaf 

457 node_str = str(node) 

458 if not node_str.endswith("\n"): 

459 node_str += "\n" 

460 parts.append(node_str) 

461 elif isinstance(node, Node): 

462 # For nodes that might contain STANDALONE_COMMENT leaves, 

463 # we need custom stringify 

464 has_standalone = any( 

465 leaf.type == STANDALONE_COMMENT for leaf in node.leaves() 

466 ) 

467 if has_standalone: 

468 # Stringify node with STANDALONE_COMMENT leaves having trailing newlines 

469 def stringify_node(n: LN) -> str: 

470 if isinstance(n, Leaf): 

471 if n.type == STANDALONE_COMMENT: 

472 result = n.prefix + n.value 

473 if not result.endswith("\n"): 

474 result += "\n" 

475 return result 

476 return str(n) 

477 else: 

478 # For nested nodes, recursively process children 

479 return "".join(stringify_node(child) for child in n.children) 

480 

481 parts.append(stringify_node(node)) 

482 else: 

483 parts.append(str(node)) 

484 else: 

485 parts.append(str(node)) 

486 

487 hidden_value = "".join(parts) 

488 comment_lineno = leaf.lineno - comment.newlines 

489 leaf_is_ignored = any( 

490 ignored is leaf 

491 or ( 

492 isinstance(ignored, Node) 

493 and any(child is leaf for child in ignored.leaves()) 

494 ) 

495 for ignored in ignored_nodes 

496 ) 

497 

498 if contains_fmt_directive(comment.value, FMT_OFF): 

499 fmt_off_prefix = "" 

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

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

502 ): 

503 # keeping indentation of comment by preserving original whitespaces. 

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

505 if "\n" in fmt_off_prefix: 

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

507 standalone_comment_prefix += fmt_off_prefix 

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

509 

510 if is_fmt_skip and not leaf_is_ignored: 

511 hidden_value += comment.leading_whitespace + comment.value 

512 

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

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

515 # leaf (possibly followed by a DEDENT). 

516 hidden_value = hidden_value[:-1] 

517 

518 first_idx: int | None = None 

519 for ignored in ignored_nodes: 

520 ignored_parent = ignored.parent 

521 hint = ( 

522 search_hints.get(id(ignored_parent), 0) 

523 if search_hints is not None and ignored_parent is not None 

524 else 0 

525 ) 

526 index = ignored.remove(hint) 

527 if ( 

528 index is not None 

529 and search_hints is not None 

530 and ignored_parent is not None 

531 ): 

532 search_hints[id(ignored_parent)] = index 

533 if first_idx is None: 

534 first_idx = index 

535 

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

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

538 

539 parent.insert_child( 

540 first_idx, 

541 Leaf( 

542 STANDALONE_COMMENT, 

543 hidden_value, 

544 prefix=standalone_comment_prefix, 

545 fmt_pass_converted_first_leaf=first_leaf_of(first), 

546 ), 

547 ) 

548 

549 

550def generate_ignored_nodes( 

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

552) -> Iterator[LN]: 

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

554 

555 If comment is skip, returns leaf only. 

556 Stops at the end of the block. 

557 """ 

558 if contains_fmt_directive(comment.value, FMT_SKIP): 

559 yield from _generate_ignored_nodes_from_fmt_skip(leaf, comment, mode) 

560 return 

561 container: LN | None = container_of(leaf) 

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

563 if is_fmt_on(container, mode=mode): 

564 return 

565 

566 # fix for fmt: on in children 

567 if children_contains_fmt_on(container, mode=mode): 

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

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

570 if child.type in CLOSING_BRACKETS: 

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

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

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

574 # The alternative is to fail the formatting. 

575 yield child 

576 return 

577 if ( 

578 child.type == token.INDENT 

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

580 and children_contains_fmt_on( 

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

582 ) 

583 ): 

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

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

586 return 

587 if children_contains_fmt_on(child, mode=mode): 

588 return 

589 yield child 

590 else: 

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

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

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

594 return 

595 yield container 

596 container = container.next_sibling 

597 

598 

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

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

601 

602 This handles one-line compound statements like: 

603 if condition: body # fmt: skip 

604 

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

606 if condition: 

607 body # fmt: skip 

608 

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

610 or the suite containing it). 

611 """ 

612 if parent.type != syms.simple_stmt: 

613 return None 

614 

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

616 return None 

617 

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

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

620 # if True: 

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

622 # Structure: compound_stmt -> suite -> simple_stmt 

623 if ( 

624 parent.parent.type == syms.suite 

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

626 and parent.parent.parent.type in _COMPOUND_STATEMENTS 

627 ): 

628 return parent.parent 

629 

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

631 # The statement is still on a single line: 

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

633 # Structure: compound_stmt -> simple_stmt 

634 if parent.parent.type in _COMPOUND_STATEMENTS: 

635 return parent 

636 

637 return None 

638 

639 

640def _should_keep_compound_statement_inline( 

641 body_node: Node, simple_stmt_parent: Node 

642) -> bool: 

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

644 

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

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

647 """ 

648 # Narrow down to the single simple_stmt that may carry the semicolons before 

649 # scanning any leaves. A compound statement's suite holds one child per body 

650 # statement, so walking the whole suite here is O(n) and, called once per 

651 # `# fmt: skip` line in the block, makes the pass O(n^2). 

652 if body_node.type == syms.suite: 

653 # After formatting: the suite must hold exactly one simple_stmt and it 

654 # must be the one carrying the directive. Stop at the second simple_stmt. 

655 target: LN | None = None 

656 for child in body_node.children: 

657 if child.type == syms.simple_stmt: 

658 if target is not None: 

659 return False 

660 target = child 

661 if target is None or target is not simple_stmt_parent: 

662 return False 

663 else: 

664 # Original form: body_node IS the simple_stmt 

665 if body_node is not simple_stmt_parent: 

666 return False 

667 target = body_node 

668 

669 return any(leaf.type == token.SEMI for leaf in target.leaves()) 

670 

671 

672def _get_compound_statement_header( 

673 body_node: Node, simple_stmt_parent: Node 

674) -> list[LN]: 

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

676 if not _should_keep_compound_statement_inline(body_node, simple_stmt_parent): 

677 return [] 

678 

679 # Get the compound statement (parent of body) 

680 compound_stmt = body_node.parent 

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

682 return [] 

683 

684 # Collect all header leaves before the body 

685 header_leaves: list[LN] = [] 

686 for child in compound_stmt.children: 

687 if child is body_node: 

688 break 

689 if isinstance(child, Leaf): 

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

691 header_leaves.append(child) 

692 else: 

693 header_leaves.extend(child.leaves()) 

694 return header_leaves 

695 

696 

697def _find_closest_previous_sibling(node: LN) -> LN | None: 

698 """Find the closest previous sibling by walking up the ancestor chain.""" 

699 current: LN | None = node 

700 while current is not None: 

701 prev_sibling = current.prev_sibling 

702 if prev_sibling is not None: 

703 return prev_sibling 

704 current = current.parent 

705 return None 

706 

707 

708def _generate_ignored_nodes_from_fmt_skip( 

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

710) -> Iterator[LN]: 

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

712 prev_sibling = leaf.prev_sibling 

713 parent = leaf.parent 

714 ignored_nodes: list[LN] = [] 

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

716 # which is also formatted 

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

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

719 return 

720 

721 if prev_sibling is None and parent is not None: 

722 prev_sibling = parent.prev_sibling 

723 

724 if prev_sibling is None and comment.type == token.COMMENT: 

725 prev_sibling = _find_closest_previous_sibling(leaf) 

726 

727 if parent is not None and parent.type == syms.suite and leaf.type == token.NEWLINE: 

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

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

730 # parent suite node. Do this before the generic "same physical line" 

731 # logic so multiline headers are preserved as a whole. 

732 leaf.prefix = "" 

733 parent_sibling = parent.prev_sibling 

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

735 ignored_nodes.insert(0, parent_sibling) 

736 parent_sibling = parent_sibling.prev_sibling 

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

738 # grandparent node. 

739 grandparent = parent.parent 

740 if ( 

741 grandparent is not None 

742 and grandparent.prev_sibling is not None 

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

744 ): 

745 ignored_nodes.insert(0, grandparent.prev_sibling) 

746 yield from iter(ignored_nodes) 

747 elif prev_sibling is not None: 

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

749 

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

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

752 # node itself. 

753 

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

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

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

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

758 # node and repeat. 

759 # 4. Continue until: 

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

761 # start of the line). 

762 # b. You reach the root node. 

763 

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

765 # or NEWLINE leaves. 

766 

767 current_node = prev_sibling 

768 if ( 

769 isinstance(current_node, Leaf) 

770 and current_node.type in OPENING_BRACKETS 

771 and current_node.parent 

772 and current_node.parent.type == syms.atom 

773 ): 

774 current_node = current_node.parent 

775 

776 ignored_nodes = [current_node] 

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

778 current_node = current_node.parent 

779 

780 # Track seen nodes to detect cycles that can occur after tree modifications 

781 seen_nodes = {id(current_node)} 

782 

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

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

785 next_node = leaf_nodes[-1] if leaf_nodes else current_node 

786 

787 # Detect infinite loop - if we've seen this node before, stop 

788 # This can happen when STANDALONE_COMMENT nodes are inserted 

789 # during processing 

790 if id(next_node) in seen_nodes: 

791 break 

792 

793 current_node = next_node 

794 seen_nodes.add(id(current_node)) 

795 

796 # Stop if we encounter a STANDALONE_COMMENT created by fmt processing 

797 if ( 

798 isinstance(current_node, Leaf) 

799 and current_node.type == STANDALONE_COMMENT 

800 and hasattr(current_node, "fmt_pass_converted_first_leaf") 

801 ): 

802 break 

803 

804 if ( 

805 current_node.type in CLOSING_BRACKETS 

806 and current_node.parent 

807 and current_node.parent.type == syms.atom 

808 ): 

809 current_node = current_node.parent 

810 

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

812 if not list_comments( 

813 current_node.prefix, is_endmarker=False, mode=mode 

814 ): 

815 current_node.prefix = "" 

816 break 

817 

818 if current_node.type == token.DEDENT: 

819 break 

820 

821 # Special case for with expressions 

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

823 if ( 

824 current_node.parent 

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

826 and current_node.parent.parent 

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

828 ): 

829 current_node = current_node.parent 

830 

831 ignored_nodes.insert(0, current_node) 

832 

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

834 current_node = current_node.parent 

835 

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

837 if isinstance(parent, Node): 

838 body_node = _find_compound_statement_context(parent) 

839 if body_node is not None: 

840 header_nodes = _get_compound_statement_header(body_node, parent) 

841 if header_nodes: 

842 ignored_nodes = header_nodes + ignored_nodes 

843 

844 # If the nodes captured for the comment's physical line leave a bracket 

845 # open, the `# fmt: skip` sits inside a multi-line bracketed statement 

846 # (e.g. on the `from x import (` line). Skipping only that line would 

847 # reformat the rest of the statement, so preserve the whole enclosing 

848 # statement instead. 

849 bracket_depth = 0 

850 for node in ignored_nodes: 

851 for ignored_leaf in node.leaves(): 

852 if ignored_leaf.type in OPENING_BRACKETS: 

853 bracket_depth += 1 

854 elif ignored_leaf.type in CLOSING_BRACKETS: 

855 bracket_depth -= 1 

856 if bracket_depth > 0: 

857 statement: LN = leaf 

858 while statement.parent is not None and statement.parent.type not in ( 

859 syms.file_input, 

860 syms.suite, 

861 ): 

862 statement = statement.parent 

863 ignored_nodes = [statement] 

864 

865 leaf_is_ignored = any( 

866 ignored is leaf 

867 or ( 

868 isinstance(ignored, Node) 

869 and any(child is leaf for child in ignored.leaves()) 

870 ) 

871 for ignored in ignored_nodes 

872 ) 

873 if not leaf_is_ignored: 

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

875 

876 yield from ignored_nodes 

877 

878 

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

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

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

882 """ 

883 fmt_on = False 

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

885 if contains_fmt_directive(comment.value, FMT_ON): 

886 fmt_on = True 

887 elif contains_fmt_directive(comment.value, FMT_OFF): 

888 fmt_on = False 

889 return fmt_on 

890 

891 

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

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

894 for child in container.children: 

895 leaf = first_leaf_of(child) 

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

897 return True 

898 

899 return False 

900 

901 

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

903 """ 

904 Returns: 

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

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

907 pylint). 

908 """ 

909 for comment in comment_list: 

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

911 return True 

912 

913 return False 

914 

915 

916def contains_fmt_directive( 

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

918) -> bool: 

919 """ 

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

921 other comments. 

922 

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

924 narrowed to specific ones. 

925 

926 Matching styles: 

927 # foobar <-- single comment 

928 # foobar # foobar # foobar <-- multiple comments 

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

930 """ 

931 semantic_comment_blocks = [ 

932 comment_line, 

933 *[ 

934 _COMMENT_PREFIX + comment.strip() 

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

936 ], 

937 *[ 

938 _COMMENT_PREFIX + comment.strip() 

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

940 _COMMENT_LIST_SEPARATOR 

941 ) 

942 ], 

943 ] 

944 

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