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

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

358 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 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 content 

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

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

182 ): 

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

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

185 

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

187 content = " " + content 

188 return "#" + content 

189 

190 

191def normalize_fmt_off( 

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

193) -> None: 

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

195 try_again = True 

196 while try_again: 

197 try_again = convert_one_fmt_off_pair(node, mode, lines) 

198 

199 

200def _should_process_fmt_comment( 

201 comment: ProtoComment, leaf: Leaf 

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

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

204 

205 Returns (should_process, is_fmt_off, is_fmt_skip). 

206 """ 

207 is_fmt_off = contains_fmt_directive(comment.value, FMT_OFF) 

208 is_fmt_skip = contains_fmt_directive(comment.value, FMT_SKIP) 

209 

210 if not is_fmt_off and not is_fmt_skip: 

211 return False, False, False 

212 

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

214 if is_fmt_off and leaf.type in CLOSING_BRACKETS: 

215 return False, False, False 

216 

217 return True, is_fmt_off, is_fmt_skip 

218 

219 

220def _is_valid_standalone_fmt_comment( 

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

222) -> bool: 

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

224 

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

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

227 """ 

228 if comment.type == STANDALONE_COMMENT: 

229 return True 

230 

231 prev = preceding_leaf(leaf) 

232 if not prev: 

233 return True 

234 

235 # Treat STANDALONE_COMMENT nodes as whitespace for check 

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

237 return False 

238 if is_fmt_skip and prev.type in WHITESPACE: 

239 return False 

240 

241 return True 

242 

243 

244def _handle_comment_only_fmt_block( 

245 leaf: Leaf, 

246 comment: ProtoComment, 

247 previous_consumed: int, 

248 mode: Mode, 

249) -> bool: 

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

251 

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

253 """ 

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

255 

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

257 fmt_off_idx = None 

258 fmt_on_idx = None 

259 for idx, c in enumerate(all_comments): 

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

261 fmt_off_idx = idx 

262 if ( 

263 fmt_off_idx is not None 

264 and idx > fmt_off_idx 

265 and contains_fmt_directive(c.value, FMT_ON) 

266 ): 

267 fmt_on_idx = idx 

268 break 

269 

270 # Only proceed if we found both directives 

271 if fmt_on_idx is None or fmt_off_idx is None: 

272 return False 

273 

274 comment = all_comments[fmt_off_idx] 

275 fmt_on_comment = all_comments[fmt_on_idx] 

276 original_prefix = leaf.prefix 

277 

278 # Build the hidden value 

279 start_pos = comment.consumed 

280 end_pos = fmt_on_comment.consumed 

281 content_between_and_fmt_on = original_prefix[start_pos:end_pos] 

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

283 

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

285 hidden_value = hidden_value[:-1] 

286 

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

288 # including any comments that precede it 

289 if fmt_off_idx == 0: 

290 # No comments before fmt:off, use previous_consumed 

291 pre_fmt_off_consumed = previous_consumed 

292 else: 

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

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

295 pre_fmt_off_consumed = all_comments[fmt_off_idx - 1].consumed 

296 

297 standalone_comment_prefix = ( 

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

299 ) 

300 

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

302 if "\n" in fmt_off_prefix: 

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

304 standalone_comment_prefix += fmt_off_prefix 

305 

306 # Update leaf prefix 

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

308 

309 # Insert the STANDALONE_COMMENT 

310 parent = leaf.parent 

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

312 

313 leaf_idx = None 

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

315 if child is leaf: 

316 leaf_idx = idx 

317 break 

318 

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

320 

321 parent.insert_child( 

322 leaf_idx, 

323 Leaf( 

324 STANDALONE_COMMENT, 

325 hidden_value, 

326 prefix=standalone_comment_prefix, 

327 fmt_pass_converted_first_leaf=None, 

328 ), 

329 ) 

330 return True 

331 

332 

333def convert_one_fmt_off_pair( 

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

335) -> bool: 

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

337 

338 Returns True if a pair was converted. 

339 """ 

340 for leaf in node.leaves(): 

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

342 # to avoid reprocessing them in subsequent iterations 

343 if leaf.type == STANDALONE_COMMENT and hasattr( 

344 leaf, "fmt_pass_converted_first_leaf" 

345 ): 

346 continue 

347 

348 previous_consumed = 0 

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

350 should_process, is_fmt_off, is_fmt_skip = _should_process_fmt_comment( 

351 comment, leaf 

352 ) 

353 if not should_process: 

354 previous_consumed = comment.consumed 

355 continue 

356 

357 if not _is_valid_standalone_fmt_comment( 

358 comment, leaf, is_fmt_off, is_fmt_skip 

359 ): 

360 previous_consumed = comment.consumed 

361 continue 

362 

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

364 

365 # Handle comment-only blocks 

366 if not ignored_nodes and is_fmt_off: 

367 if _handle_comment_only_fmt_block( 

368 leaf, comment, previous_consumed, mode 

369 ): 

370 return True 

371 continue 

372 

373 # Need actual nodes to process 

374 if not ignored_nodes: 

375 continue 

376 

377 # Handle regular fmt blocks 

378 

379 _handle_regular_fmt_block( 

380 ignored_nodes, 

381 comment, 

382 previous_consumed, 

383 is_fmt_skip, 

384 lines, 

385 leaf, 

386 ) 

387 return True 

388 

389 return False 

390 

391 

392def _handle_regular_fmt_block( 

393 ignored_nodes: list[LN], 

394 comment: ProtoComment, 

395 previous_consumed: int, 

396 is_fmt_skip: bool, 

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

398 leaf: Leaf, 

399) -> None: 

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

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

402 parent = first.parent 

403 prefix = first.prefix 

404 

405 if contains_fmt_directive(comment.value, FMT_OFF): 

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

407 if is_fmt_skip: 

408 first.prefix = "" 

409 standalone_comment_prefix = prefix 

410 else: 

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

412 

413 # Ensure STANDALONE_COMMENT nodes have trailing newlines when stringified 

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

415 parts = [] 

416 for node in ignored_nodes: 

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

418 # Add newline after STANDALONE_COMMENT Leaf 

419 node_str = str(node) 

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

421 node_str += "\n" 

422 parts.append(node_str) 

423 elif isinstance(node, Node): 

424 # For nodes that might contain STANDALONE_COMMENT leaves, 

425 # we need custom stringify 

426 has_standalone = any( 

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

428 ) 

429 if has_standalone: 

430 # Stringify node with STANDALONE_COMMENT leaves having trailing newlines 

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

432 if isinstance(n, Leaf): 

433 if n.type == STANDALONE_COMMENT: 

434 result = n.prefix + n.value 

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

436 result += "\n" 

437 return result 

438 return str(n) 

439 else: 

440 # For nested nodes, recursively process children 

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

442 

443 parts.append(stringify_node(node)) 

444 else: 

445 parts.append(str(node)) 

446 else: 

447 parts.append(str(node)) 

448 

449 hidden_value = "".join(parts) 

450 comment_lineno = leaf.lineno - comment.newlines 

451 

452 if contains_fmt_directive(comment.value, FMT_OFF): 

453 fmt_off_prefix = "" 

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

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

456 ): 

457 # keeping indentation of comment by preserving original whitespaces. 

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

459 if "\n" in fmt_off_prefix: 

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

461 standalone_comment_prefix += fmt_off_prefix 

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

463 

464 if is_fmt_skip: 

465 hidden_value += comment.leading_whitespace + comment.value 

466 

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

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

469 # leaf (possibly followed by a DEDENT). 

470 hidden_value = hidden_value[:-1] 

471 

472 first_idx: int | None = None 

473 for ignored in ignored_nodes: 

474 index = ignored.remove() 

475 if first_idx is None: 

476 first_idx = index 

477 

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

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

480 

481 parent.insert_child( 

482 first_idx, 

483 Leaf( 

484 STANDALONE_COMMENT, 

485 hidden_value, 

486 prefix=standalone_comment_prefix, 

487 fmt_pass_converted_first_leaf=first_leaf_of(first), 

488 ), 

489 ) 

490 

491 

492def generate_ignored_nodes( 

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

494) -> Iterator[LN]: 

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

496 

497 If comment is skip, returns leaf only. 

498 Stops at the end of the block. 

499 """ 

500 if contains_fmt_directive(comment.value, FMT_SKIP): 

501 yield from _generate_ignored_nodes_from_fmt_skip(leaf, comment, mode) 

502 return 

503 container: LN | None = container_of(leaf) 

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

505 if is_fmt_on(container, mode=mode): 

506 return 

507 

508 # fix for fmt: on in children 

509 if children_contains_fmt_on(container, mode=mode): 

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

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

512 if child.type in CLOSING_BRACKETS: 

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

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

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

516 # The alternative is to fail the formatting. 

517 yield child 

518 return 

519 if ( 

520 child.type == token.INDENT 

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

522 and children_contains_fmt_on( 

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

524 ) 

525 ): 

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

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

528 return 

529 if children_contains_fmt_on(child, mode=mode): 

530 return 

531 yield child 

532 else: 

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

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

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

536 return 

537 yield container 

538 container = container.next_sibling 

539 

540 

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

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

543 

544 This handles one-line compound statements like: 

545 if condition: body # fmt: skip 

546 

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

548 if condition: 

549 body # fmt: skip 

550 

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

552 or the suite containing it). 

553 """ 

554 if parent.type != syms.simple_stmt: 

555 return None 

556 

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

558 return None 

559 

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

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

562 # if True: 

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

564 # Structure: compound_stmt -> suite -> simple_stmt 

565 if ( 

566 parent.parent.type == syms.suite 

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

568 and parent.parent.parent.type in _COMPOUND_STATEMENTS 

569 ): 

570 return parent.parent 

571 

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

573 # The statement is still on a single line: 

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

575 # Structure: compound_stmt -> simple_stmt 

576 if parent.parent.type in _COMPOUND_STATEMENTS: 

577 return parent 

578 

579 return None 

580 

581 

582def _should_keep_compound_statement_inline( 

583 body_node: Node, simple_stmt_parent: Node 

584) -> bool: 

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

586 

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

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

589 """ 

590 # Check if there are semicolons in the body 

591 for leaf in body_node.leaves(): 

592 if leaf.type == token.SEMI: 

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

594 if body_node.type == syms.suite: 

595 # After formatting: check suite has one simple_stmt child 

596 simple_stmts = [ 

597 child 

598 for child in body_node.children 

599 if child.type == syms.simple_stmt 

600 ] 

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

602 else: 

603 # Original form: body_node IS the simple_stmt 

604 return body_node is simple_stmt_parent 

605 return False 

606 

607 

608def _get_compound_statement_header( 

609 body_node: Node, simple_stmt_parent: Node 

610) -> list[LN]: 

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

612 if not _should_keep_compound_statement_inline(body_node, simple_stmt_parent): 

613 return [] 

614 

615 # Get the compound statement (parent of body) 

616 compound_stmt = body_node.parent 

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

618 return [] 

619 

620 # Collect all header leaves before the body 

621 header_leaves: list[LN] = [] 

622 for child in compound_stmt.children: 

623 if child is body_node: 

624 break 

625 if isinstance(child, Leaf): 

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

627 header_leaves.append(child) 

628 else: 

629 header_leaves.extend(child.leaves()) 

630 return header_leaves 

631 

632 

633def _generate_ignored_nodes_from_fmt_skip( 

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

635) -> Iterator[LN]: 

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

637 prev_sibling = leaf.prev_sibling 

638 parent = leaf.parent 

639 ignored_nodes: list[LN] = [] 

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

641 # which is also formatted 

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

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

644 return 

645 

646 if not prev_sibling and parent: 

647 prev_sibling = parent.prev_sibling 

648 

649 if prev_sibling is not None: 

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

651 

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

653 

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

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

656 # node itself. 

657 

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

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

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

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

662 # node and repeat. 

663 # 4. Continue until: 

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

665 # start of the line). 

666 # b. You reach the root node. 

667 

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

669 # or NEWLINE leaves. 

670 

671 current_node = prev_sibling 

672 ignored_nodes = [current_node] 

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

674 current_node = current_node.parent 

675 

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

677 seen_nodes = {id(current_node)} 

678 

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

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

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

682 

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

684 # This can happen when STANDALONE_COMMENT nodes are inserted 

685 # during processing 

686 if id(next_node) in seen_nodes: 

687 break 

688 

689 current_node = next_node 

690 seen_nodes.add(id(current_node)) 

691 

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

693 if ( 

694 isinstance(current_node, Leaf) 

695 and current_node.type == STANDALONE_COMMENT 

696 and hasattr(current_node, "fmt_pass_converted_first_leaf") 

697 ): 

698 break 

699 

700 if ( 

701 current_node.type in CLOSING_BRACKETS 

702 and current_node.parent 

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

704 ): 

705 current_node = current_node.parent 

706 

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

708 current_node.prefix = "" 

709 break 

710 

711 if current_node.type == token.DEDENT: 

712 break 

713 

714 # Special case for with expressions 

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

716 if ( 

717 current_node.parent 

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

719 and current_node.parent.parent 

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

721 ): 

722 current_node = current_node.parent 

723 

724 ignored_nodes.insert(0, current_node) 

725 

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

727 current_node = current_node.parent 

728 

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

730 if isinstance(parent, Node): 

731 body_node = _find_compound_statement_context(parent) 

732 if body_node is not None: 

733 header_nodes = _get_compound_statement_header(body_node, parent) 

734 if header_nodes: 

735 ignored_nodes = header_nodes + ignored_nodes 

736 

737 yield from ignored_nodes 

738 elif ( 

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

740 ): 

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

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

743 # parent suite node. 

744 leaf.prefix = "" 

745 parent_sibling = parent.prev_sibling 

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

747 ignored_nodes.insert(0, parent_sibling) 

748 parent_sibling = parent_sibling.prev_sibling 

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

750 # grandparent node. 

751 grandparent = parent.parent 

752 if ( 

753 grandparent is not None 

754 and grandparent.prev_sibling is not None 

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

756 ): 

757 ignored_nodes.insert(0, grandparent.prev_sibling) 

758 yield from iter(ignored_nodes) 

759 

760 

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

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

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

764 """ 

765 fmt_on = False 

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

767 if contains_fmt_directive(comment.value, FMT_ON): 

768 fmt_on = True 

769 elif contains_fmt_directive(comment.value, FMT_OFF): 

770 fmt_on = False 

771 return fmt_on 

772 

773 

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

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

776 for child in container.children: 

777 leaf = first_leaf_of(child) 

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

779 return True 

780 

781 return False 

782 

783 

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

785 """ 

786 Returns: 

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

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

789 pylint). 

790 """ 

791 for comment in comment_list: 

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

793 return True 

794 

795 return False 

796 

797 

798def contains_fmt_directive( 

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

800) -> bool: 

801 """ 

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

803 other comments. 

804 

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

806 narrowed to specific ones. 

807 

808 Matching styles: 

809 # foobar <-- single comment 

810 # foobar # foobar # foobar <-- multiple comments 

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

812 """ 

813 semantic_comment_blocks = [ 

814 comment_line, 

815 *[ 

816 _COMMENT_PREFIX + comment.strip() 

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

818 ], 

819 *[ 

820 _COMMENT_PREFIX + comment.strip() 

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

822 _COMMENT_LIST_SEPARATOR 

823 ) 

824 ], 

825 ] 

826 

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