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

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

365 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 contains_fmt_directive(c.value, FMT_OFF): 

262 fmt_off_idx = idx 

263 if ( 

264 fmt_off_idx is not None 

265 and idx > fmt_off_idx 

266 and contains_fmt_directive(c.value, FMT_ON) 

267 ): 

268 fmt_on_idx = idx 

269 break 

270 

271 # Only proceed if we found both directives 

272 if fmt_on_idx is None or fmt_off_idx is None: 

273 return False 

274 

275 comment = all_comments[fmt_off_idx] 

276 fmt_on_comment = all_comments[fmt_on_idx] 

277 original_prefix = leaf.prefix 

278 

279 # Build the hidden value 

280 start_pos = comment.consumed 

281 end_pos = fmt_on_comment.consumed 

282 content_between_and_fmt_on = original_prefix[start_pos:end_pos] 

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

284 

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

286 hidden_value = hidden_value[:-1] 

287 

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

289 # including any comments that precede it 

290 if fmt_off_idx == 0: 

291 # No comments before fmt:off, use previous_consumed 

292 pre_fmt_off_consumed = previous_consumed 

293 else: 

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

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

296 pre_fmt_off_consumed = all_comments[fmt_off_idx - 1].consumed 

297 

298 standalone_comment_prefix = ( 

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

300 ) 

301 

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

303 if "\n" in fmt_off_prefix: 

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

305 standalone_comment_prefix += fmt_off_prefix 

306 

307 # Update leaf prefix 

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

309 

310 # Insert the STANDALONE_COMMENT 

311 parent = leaf.parent 

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

313 

314 leaf_idx = None 

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

316 if child is leaf: 

317 leaf_idx = idx 

318 break 

319 

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

321 

322 parent.insert_child( 

323 leaf_idx, 

324 Leaf( 

325 STANDALONE_COMMENT, 

326 hidden_value, 

327 prefix=standalone_comment_prefix, 

328 fmt_pass_converted_first_leaf=None, 

329 ), 

330 ) 

331 return True 

332 

333 

334def convert_one_fmt_off_pair( 

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

336) -> bool: 

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

338 

339 Returns True if a pair was converted. 

340 """ 

341 for leaf in node.leaves(): 

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

343 # to avoid reprocessing them in subsequent iterations 

344 if leaf.type == STANDALONE_COMMENT and hasattr( 

345 leaf, "fmt_pass_converted_first_leaf" 

346 ): 

347 continue 

348 

349 previous_consumed = 0 

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

351 should_process, is_fmt_off, is_fmt_skip = _should_process_fmt_comment( 

352 comment, leaf 

353 ) 

354 if not should_process: 

355 previous_consumed = comment.consumed 

356 continue 

357 

358 if not _is_valid_standalone_fmt_comment( 

359 comment, leaf, is_fmt_off, is_fmt_skip 

360 ): 

361 previous_consumed = comment.consumed 

362 continue 

363 

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

365 

366 # Handle comment-only blocks 

367 if not ignored_nodes and is_fmt_off: 

368 if _handle_comment_only_fmt_block( 

369 leaf, comment, previous_consumed, mode 

370 ): 

371 return True 

372 continue 

373 

374 # Need actual nodes to process 

375 if not ignored_nodes: 

376 continue 

377 

378 # Handle regular fmt blocks 

379 

380 _handle_regular_fmt_block( 

381 ignored_nodes, 

382 comment, 

383 previous_consumed, 

384 is_fmt_skip, 

385 lines, 

386 leaf, 

387 ) 

388 return True 

389 

390 return False 

391 

392 

393def _handle_regular_fmt_block( 

394 ignored_nodes: list[LN], 

395 comment: ProtoComment, 

396 previous_consumed: int, 

397 is_fmt_skip: bool, 

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

399 leaf: Leaf, 

400) -> None: 

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

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

403 parent = first.parent 

404 prefix = first.prefix 

405 

406 if contains_fmt_directive(comment.value, FMT_OFF): 

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

408 if is_fmt_skip: 

409 first.prefix = "" 

410 standalone_comment_prefix = prefix 

411 else: 

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

413 

414 # Ensure STANDALONE_COMMENT nodes have trailing newlines when stringified 

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

416 parts = [] 

417 for node in ignored_nodes: 

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

419 # Add newline after STANDALONE_COMMENT Leaf 

420 node_str = str(node) 

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

422 node_str += "\n" 

423 parts.append(node_str) 

424 elif isinstance(node, Node): 

425 # For nodes that might contain STANDALONE_COMMENT leaves, 

426 # we need custom stringify 

427 has_standalone = any( 

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

429 ) 

430 if has_standalone: 

431 # Stringify node with STANDALONE_COMMENT leaves having trailing newlines 

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

433 if isinstance(n, Leaf): 

434 if n.type == STANDALONE_COMMENT: 

435 result = n.prefix + n.value 

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

437 result += "\n" 

438 return result 

439 return str(n) 

440 else: 

441 # For nested nodes, recursively process children 

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

443 

444 parts.append(stringify_node(node)) 

445 else: 

446 parts.append(str(node)) 

447 else: 

448 parts.append(str(node)) 

449 

450 hidden_value = "".join(parts) 

451 comment_lineno = leaf.lineno - comment.newlines 

452 

453 if contains_fmt_directive(comment.value, FMT_OFF): 

454 fmt_off_prefix = "" 

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

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

457 ): 

458 # keeping indentation of comment by preserving original whitespaces. 

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

460 if "\n" in fmt_off_prefix: 

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

462 standalone_comment_prefix += fmt_off_prefix 

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

464 

465 if is_fmt_skip: 

466 hidden_value += comment.leading_whitespace + comment.value 

467 

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

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

470 # leaf (possibly followed by a DEDENT). 

471 hidden_value = hidden_value[:-1] 

472 

473 first_idx: int | None = None 

474 for ignored in ignored_nodes: 

475 index = ignored.remove() 

476 if first_idx is None: 

477 first_idx = index 

478 

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

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

481 

482 parent.insert_child( 

483 first_idx, 

484 Leaf( 

485 STANDALONE_COMMENT, 

486 hidden_value, 

487 prefix=standalone_comment_prefix, 

488 fmt_pass_converted_first_leaf=first_leaf_of(first), 

489 ), 

490 ) 

491 

492 

493def generate_ignored_nodes( 

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

495) -> Iterator[LN]: 

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

497 

498 If comment is skip, returns leaf only. 

499 Stops at the end of the block. 

500 """ 

501 if contains_fmt_directive(comment.value, FMT_SKIP): 

502 yield from _generate_ignored_nodes_from_fmt_skip(leaf, comment, mode) 

503 return 

504 container: LN | None = container_of(leaf) 

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

506 if is_fmt_on(container, mode=mode): 

507 return 

508 

509 # fix for fmt: on in children 

510 if children_contains_fmt_on(container, mode=mode): 

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

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

513 if child.type in CLOSING_BRACKETS: 

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

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

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

517 # The alternative is to fail the formatting. 

518 yield child 

519 return 

520 if ( 

521 child.type == token.INDENT 

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

523 and children_contains_fmt_on( 

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

525 ) 

526 ): 

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

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

529 return 

530 if children_contains_fmt_on(child, mode=mode): 

531 return 

532 yield child 

533 else: 

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

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

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

537 return 

538 yield container 

539 container = container.next_sibling 

540 

541 

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

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

544 

545 This handles one-line compound statements like: 

546 if condition: body # fmt: skip 

547 

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

549 if condition: 

550 body # fmt: skip 

551 

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

553 or the suite containing it). 

554 """ 

555 if parent.type != syms.simple_stmt: 

556 return None 

557 

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

559 return None 

560 

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

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

563 # if True: 

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

565 # Structure: compound_stmt -> suite -> simple_stmt 

566 if ( 

567 parent.parent.type == syms.suite 

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

569 and parent.parent.parent.type in _COMPOUND_STATEMENTS 

570 ): 

571 return parent.parent 

572 

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

574 # The statement is still on a single line: 

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

576 # Structure: compound_stmt -> simple_stmt 

577 if parent.parent.type in _COMPOUND_STATEMENTS: 

578 return parent 

579 

580 return None 

581 

582 

583def _should_keep_compound_statement_inline( 

584 body_node: Node, simple_stmt_parent: Node 

585) -> bool: 

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

587 

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

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

590 """ 

591 # Check if there are semicolons in the body 

592 for leaf in body_node.leaves(): 

593 if leaf.type == token.SEMI: 

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

595 if body_node.type == syms.suite: 

596 # After formatting: check suite has one simple_stmt child 

597 simple_stmts = [ 

598 child 

599 for child in body_node.children 

600 if child.type == syms.simple_stmt 

601 ] 

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

603 else: 

604 # Original form: body_node IS the simple_stmt 

605 return body_node is simple_stmt_parent 

606 return False 

607 

608 

609def _get_compound_statement_header( 

610 body_node: Node, simple_stmt_parent: Node 

611) -> list[LN]: 

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

613 if not _should_keep_compound_statement_inline(body_node, simple_stmt_parent): 

614 return [] 

615 

616 # Get the compound statement (parent of body) 

617 compound_stmt = body_node.parent 

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

619 return [] 

620 

621 # Collect all header leaves before the body 

622 header_leaves: list[LN] = [] 

623 for child in compound_stmt.children: 

624 if child is body_node: 

625 break 

626 if isinstance(child, Leaf): 

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

628 header_leaves.append(child) 

629 else: 

630 header_leaves.extend(child.leaves()) 

631 return header_leaves 

632 

633 

634def _generate_ignored_nodes_from_fmt_skip( 

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

636) -> Iterator[LN]: 

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

638 prev_sibling = leaf.prev_sibling 

639 parent = leaf.parent 

640 ignored_nodes: list[LN] = [] 

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

642 # which is also formatted 

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

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

645 return 

646 

647 if Preview.fix_fmt_skip_in_one_liners in mode and not prev_sibling and parent: 

648 prev_sibling = parent.prev_sibling 

649 

650 if prev_sibling is not None: 

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

652 

653 if Preview.fix_fmt_skip_in_one_liners not in mode: 

654 siblings = [prev_sibling] 

655 while ( 

656 "\n" not in prev_sibling.prefix 

657 and prev_sibling.prev_sibling is not None 

658 ): 

659 prev_sibling = prev_sibling.prev_sibling 

660 siblings.insert(0, prev_sibling) 

661 yield from siblings 

662 return 

663 

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

665 

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

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

668 # node itself. 

669 

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

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

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

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

674 # node and repeat. 

675 # 4. Continue until: 

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

677 # start of the line). 

678 # b. You reach the root node. 

679 

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

681 # or NEWLINE leaves. 

682 

683 current_node = prev_sibling 

684 ignored_nodes = [current_node] 

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

686 current_node = current_node.parent 

687 

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

689 seen_nodes = {id(current_node)} 

690 

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

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

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

694 

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

696 # This can happen when STANDALONE_COMMENT nodes are inserted 

697 # during processing 

698 if id(next_node) in seen_nodes: 

699 break 

700 

701 current_node = next_node 

702 seen_nodes.add(id(current_node)) 

703 

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

705 if ( 

706 isinstance(current_node, Leaf) 

707 and current_node.type == STANDALONE_COMMENT 

708 and hasattr(current_node, "fmt_pass_converted_first_leaf") 

709 ): 

710 break 

711 

712 if ( 

713 current_node.type in CLOSING_BRACKETS 

714 and current_node.parent 

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

716 ): 

717 current_node = current_node.parent 

718 

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

720 current_node.prefix = "" 

721 break 

722 

723 if current_node.type == token.DEDENT: 

724 break 

725 

726 # Special case for with expressions 

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

728 if ( 

729 current_node.parent 

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

731 and current_node.parent.parent 

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

733 ): 

734 current_node = current_node.parent 

735 

736 ignored_nodes.insert(0, current_node) 

737 

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

739 current_node = current_node.parent 

740 

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

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

743 body_node = _find_compound_statement_context(parent) 

744 if body_node is not None: 

745 header_nodes = _get_compound_statement_header(body_node, parent) 

746 if header_nodes: 

747 ignored_nodes = header_nodes + ignored_nodes 

748 

749 yield from ignored_nodes 

750 elif ( 

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

752 ): 

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

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

755 # parent suite node. 

756 leaf.prefix = "" 

757 parent_sibling = parent.prev_sibling 

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

759 ignored_nodes.insert(0, parent_sibling) 

760 parent_sibling = parent_sibling.prev_sibling 

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

762 # grandparent node. 

763 grandparent = parent.parent 

764 if ( 

765 grandparent is not None 

766 and grandparent.prev_sibling is not None 

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

768 ): 

769 ignored_nodes.insert(0, grandparent.prev_sibling) 

770 yield from iter(ignored_nodes) 

771 

772 

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

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

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

776 """ 

777 fmt_on = False 

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

779 if contains_fmt_directive(comment.value, FMT_ON): 

780 fmt_on = True 

781 elif contains_fmt_directive(comment.value, FMT_OFF): 

782 fmt_on = False 

783 return fmt_on 

784 

785 

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

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

788 for child in container.children: 

789 leaf = first_leaf_of(child) 

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

791 return True 

792 

793 return False 

794 

795 

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

797 """ 

798 Returns: 

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

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

801 pylint). 

802 """ 

803 for comment in comment_list: 

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

805 return True 

806 

807 return False 

808 

809 

810def contains_fmt_directive( 

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

812) -> bool: 

813 """ 

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

815 other comments. 

816 

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

818 narrowed to specific ones. 

819 

820 Matching styles: 

821 # foobar <-- single comment 

822 # foobar # foobar # foobar <-- multiple comments 

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

824 """ 

825 semantic_comment_blocks = [ 

826 comment_line, 

827 *[ 

828 _COMMENT_PREFIX + comment.strip() 

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

830 ], 

831 *[ 

832 _COMMENT_PREFIX + comment.strip() 

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

834 _COMMENT_LIST_SEPARATOR 

835 ) 

836 ], 

837 ] 

838 

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