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
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
1import re
2from collections.abc import Collection, Iterator
3from dataclasses import dataclass
4from functools import lru_cache
5from typing import Final, Union
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
24# types
25LN = Union[Leaf, Node]
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"}
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}
35COMMENT_EXCEPTIONS = " !:#'"
36_COMMENT_PREFIX = "# "
37_COMMENT_LIST_SEPARATOR = ";"
40@dataclass
41class ProtoComment:
42 """Describes a piece of syntax that is a comment.
44 It's not a :class:`blib2to3.pytree.Leaf` so that:
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 """
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
60def generate_comments(leaf: LN, mode: Mode) -> Iterator[Leaf]:
61 """Clean the prefix of the `leaf` and generate comments from it, if any.
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.
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.
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.
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)
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
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
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
137def normalize_trailing_prefix(leaf: LN, total_consumed: int) -> None:
138 """Normalize the prefix that's left over after generating comments.
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
149 leaf.prefix = ""
152def make_comment(content: str, mode: Mode) -> str:
153 """Return a consistently formatted comment from the given `content` string.
155 All comments (except for "##", "#!", "#:", '#'") should have a single
156 space between the hash sign and the content.
158 If `content` didn't start with a hash sign, one is provided.
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 "#"
167 # Preserve comments with fmt directives exactly as-is
168 if content.startswith("#") and contains_fmt_directive(content):
169 return content
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()
187 if content and content[0] not in COMMENT_EXCEPTIONS:
188 content = " " + content
189 return "#" + content
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)
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.
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)
211 if not is_fmt_off and not is_fmt_skip:
212 return False, False, False
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
218 return True, is_fmt_off, is_fmt_skip
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.
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
232 prev = preceding_leaf(leaf)
233 if not prev:
234 return True
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
242 return True
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.
253 Returns True if a block was converted, False otherwise.
254 """
255 all_comments = list_comments(leaf.prefix, is_endmarker=False, mode=mode)
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
271 # Only proceed if we found both directives
272 if fmt_on_idx is None or fmt_off_idx is None:
273 return False
275 comment = all_comments[fmt_off_idx]
276 fmt_on_comment = all_comments[fmt_on_idx]
277 original_prefix = leaf.prefix
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
285 if hidden_value.endswith("\n"):
286 hidden_value = hidden_value[:-1]
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
298 standalone_comment_prefix = (
299 original_prefix[:pre_fmt_off_consumed] + "\n" * comment.newlines
300 )
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
307 # Update leaf prefix
308 leaf.prefix = original_prefix[fmt_on_comment.consumed :]
310 # Insert the STANDALONE_COMMENT
311 parent = leaf.parent
312 assert parent is not None, "INTERNAL ERROR: fmt: on/off handling (prefix only)"
314 leaf_idx = None
315 for idx, child in enumerate(parent.children):
316 if child is leaf:
317 leaf_idx = idx
318 break
320 assert leaf_idx is not None, "INTERNAL ERROR: fmt: on/off handling (leaf index)"
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
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.
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
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
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
364 ignored_nodes = list(generate_ignored_nodes(leaf, comment, mode))
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
374 # Need actual nodes to process
375 if not ignored_nodes:
376 continue
378 # Handle regular fmt blocks
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
390 return False
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
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
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)
444 parts.append(stringify_node(node))
445 else:
446 parts.append(str(node))
447 else:
448 parts.append(str(node))
450 hidden_value = "".join(parts)
451 comment_lineno = leaf.lineno - comment.newlines
452 leaf_is_ignored = any(
453 ignored is leaf
454 or (
455 isinstance(ignored, Node)
456 and any(child is leaf for child in ignored.leaves())
457 )
458 for ignored in ignored_nodes
459 )
461 if contains_fmt_directive(comment.value, FMT_OFF):
462 fmt_off_prefix = ""
463 if len(lines) > 0 and not any(
464 line[0] <= comment_lineno <= line[1] for line in lines
465 ):
466 # keeping indentation of comment by preserving original whitespaces.
467 fmt_off_prefix = prefix.split(comment.value)[0]
468 if "\n" in fmt_off_prefix:
469 fmt_off_prefix = fmt_off_prefix.split("\n")[-1]
470 standalone_comment_prefix += fmt_off_prefix
471 hidden_value = comment.value + "\n" + hidden_value
473 if is_fmt_skip and not leaf_is_ignored:
474 hidden_value += comment.leading_whitespace + comment.value
476 if hidden_value.endswith("\n"):
477 # That happens when one of the `ignored_nodes` ended with a NEWLINE
478 # leaf (possibly followed by a DEDENT).
479 hidden_value = hidden_value[:-1]
481 first_idx: int | None = None
482 for ignored in ignored_nodes:
483 index = ignored.remove()
484 if first_idx is None:
485 first_idx = index
487 assert parent is not None, "INTERNAL ERROR: fmt: on/off handling (1)"
488 assert first_idx is not None, "INTERNAL ERROR: fmt: on/off handling (2)"
490 parent.insert_child(
491 first_idx,
492 Leaf(
493 STANDALONE_COMMENT,
494 hidden_value,
495 prefix=standalone_comment_prefix,
496 fmt_pass_converted_first_leaf=first_leaf_of(first),
497 ),
498 )
501def generate_ignored_nodes(
502 leaf: Leaf, comment: ProtoComment, mode: Mode
503) -> Iterator[LN]:
504 """Starting from the container of `leaf`, generate all leaves until `# fmt: on`.
506 If comment is skip, returns leaf only.
507 Stops at the end of the block.
508 """
509 if contains_fmt_directive(comment.value, FMT_SKIP):
510 yield from _generate_ignored_nodes_from_fmt_skip(leaf, comment, mode)
511 return
512 container: LN | None = container_of(leaf)
513 while container is not None and container.type != token.ENDMARKER:
514 if is_fmt_on(container, mode=mode):
515 return
517 # fix for fmt: on in children
518 if children_contains_fmt_on(container, mode=mode):
519 for index, child in enumerate(container.children):
520 if isinstance(child, Leaf) and is_fmt_on(child, mode=mode):
521 if child.type in CLOSING_BRACKETS:
522 # This means `# fmt: on` is placed at a different bracket level
523 # than `# fmt: off`. This is an invalid use, but as a courtesy,
524 # we include this closing bracket in the ignored nodes.
525 # The alternative is to fail the formatting.
526 yield child
527 return
528 if (
529 child.type == token.INDENT
530 and index < len(container.children) - 1
531 and children_contains_fmt_on(
532 container.children[index + 1], mode=mode
533 )
534 ):
535 # This means `# fmt: on` is placed right after an indentation
536 # level, and we shouldn't swallow the previous INDENT token.
537 return
538 if children_contains_fmt_on(child, mode=mode):
539 return
540 yield child
541 else:
542 if container.type == token.DEDENT and container.next_sibling is None:
543 # This can happen when there is no matching `# fmt: on` comment at the
544 # same level as `# fmt: on`. We need to keep this DEDENT.
545 return
546 yield container
547 container = container.next_sibling
550def _find_compound_statement_context(parent: Node) -> Node | None:
551 """Return the body node of a compound statement if we should respect fmt: skip.
553 This handles one-line compound statements like:
554 if condition: body # fmt: skip
556 When Black expands such statements, they temporarily look like:
557 if condition:
558 body # fmt: skip
560 In both cases, we want to return the body node (either the simple_stmt directly
561 or the suite containing it).
562 """
563 if parent.type != syms.simple_stmt:
564 return None
566 if not isinstance(parent.parent, Node):
567 return None
569 # Case 1: Expanded form after Black's initial formatting pass.
570 # The one-liner has been split across multiple lines:
571 # if True:
572 # print("a"); print("b") # fmt: skip
573 # Structure: compound_stmt -> suite -> simple_stmt
574 if (
575 parent.parent.type == syms.suite
576 and isinstance(parent.parent.parent, Node)
577 and parent.parent.parent.type in _COMPOUND_STATEMENTS
578 ):
579 return parent.parent
581 # Case 2: Original one-line form from the input source.
582 # The statement is still on a single line:
583 # if True: print("a"); print("b") # fmt: skip
584 # Structure: compound_stmt -> simple_stmt
585 if parent.parent.type in _COMPOUND_STATEMENTS:
586 return parent
588 return None
591def _should_keep_compound_statement_inline(
592 body_node: Node, simple_stmt_parent: Node
593) -> bool:
594 """Check if a compound statement should be kept on one line.
596 Returns True only for compound statements with semicolon-separated bodies,
597 like: if True: print("a"); print("b") # fmt: skip
598 """
599 # Check if there are semicolons in the body
600 for leaf in body_node.leaves():
601 if leaf.type == token.SEMI:
602 # Verify it's a single-line body (one simple_stmt)
603 if body_node.type == syms.suite:
604 # After formatting: check suite has one simple_stmt child
605 simple_stmts = [
606 child
607 for child in body_node.children
608 if child.type == syms.simple_stmt
609 ]
610 return len(simple_stmts) == 1 and simple_stmts[0] is simple_stmt_parent
611 else:
612 # Original form: body_node IS the simple_stmt
613 return body_node is simple_stmt_parent
614 return False
617def _get_compound_statement_header(
618 body_node: Node, simple_stmt_parent: Node
619) -> list[LN]:
620 """Get header nodes for a compound statement that should be preserved inline."""
621 if not _should_keep_compound_statement_inline(body_node, simple_stmt_parent):
622 return []
624 # Get the compound statement (parent of body)
625 compound_stmt = body_node.parent
626 if compound_stmt is None or compound_stmt.type not in _COMPOUND_STATEMENTS:
627 return []
629 # Collect all header leaves before the body
630 header_leaves: list[LN] = []
631 for child in compound_stmt.children:
632 if child is body_node:
633 break
634 if isinstance(child, Leaf):
635 if child.type not in (token.NEWLINE, token.INDENT):
636 header_leaves.append(child)
637 else:
638 header_leaves.extend(child.leaves())
639 return header_leaves
642def _find_closest_previous_sibling(node: LN) -> LN | None:
643 """Find the closest previous sibling by walking up the ancestor chain."""
644 current: LN | None = node
645 while current is not None:
646 prev_sibling = current.prev_sibling
647 if prev_sibling is not None:
648 return prev_sibling
649 current = current.parent
650 return None
653def _generate_ignored_nodes_from_fmt_skip(
654 leaf: Leaf, comment: ProtoComment, mode: Mode
655) -> Iterator[LN]:
656 """Generate all leaves that should be ignored by the `# fmt: skip` from `leaf`."""
657 prev_sibling = leaf.prev_sibling
658 parent = leaf.parent
659 ignored_nodes: list[LN] = []
660 # Need to properly format the leaf prefix to compare it to comment.value,
661 # which is also formatted
662 comments = list_comments(leaf.prefix, is_endmarker=False, mode=mode)
663 if not comments or comment.value != comments[0].value:
664 return
666 if prev_sibling is None and parent is not None:
667 prev_sibling = parent.prev_sibling
669 if prev_sibling is None and comment.type == token.COMMENT:
670 prev_sibling = _find_closest_previous_sibling(leaf)
672 if parent is not None and parent.type == syms.suite and leaf.type == token.NEWLINE:
673 # The `# fmt: skip` is on the colon line of the if/while/def/class/...
674 # statements. The ignored nodes should be previous siblings of the
675 # parent suite node. Do this before the generic "same physical line"
676 # logic so multiline headers are preserved as a whole.
677 leaf.prefix = ""
678 parent_sibling = parent.prev_sibling
679 while parent_sibling is not None and parent_sibling.type != syms.suite:
680 ignored_nodes.insert(0, parent_sibling)
681 parent_sibling = parent_sibling.prev_sibling
682 # Special case for `async_stmt` where the ASYNC token is on the
683 # grandparent node.
684 grandparent = parent.parent
685 if (
686 grandparent is not None
687 and grandparent.prev_sibling is not None
688 and grandparent.prev_sibling.type == token.ASYNC
689 ):
690 ignored_nodes.insert(0, grandparent.prev_sibling)
691 yield from iter(ignored_nodes)
692 elif prev_sibling is not None:
693 # Generates the nodes to be ignored by `fmt: skip`.
695 # Nodes to ignore are the ones on the same line as the
696 # `# fmt: skip` comment, excluding the `# fmt: skip`
697 # node itself.
699 # Traversal process (starting at the `# fmt: skip` node):
700 # 1. Move to the `prev_sibling` of the current node.
701 # 2. If `prev_sibling` has children, go to its rightmost leaf.
702 # 3. If there's no `prev_sibling`, move up to the parent
703 # node and repeat.
704 # 4. Continue until:
705 # a. You encounter an `INDENT` or `NEWLINE` node (indicates
706 # start of the line).
707 # b. You reach the root node.
709 # Include all visited LEAVES in the ignored list, except INDENT
710 # or NEWLINE leaves.
712 current_node = prev_sibling
713 if (
714 isinstance(current_node, Leaf)
715 and current_node.type in OPENING_BRACKETS
716 and current_node.parent
717 and current_node.parent.type == syms.atom
718 ):
719 current_node = current_node.parent
721 ignored_nodes = [current_node]
722 if current_node.prev_sibling is None and current_node.parent is not None:
723 current_node = current_node.parent
725 # Track seen nodes to detect cycles that can occur after tree modifications
726 seen_nodes = {id(current_node)}
728 while "\n" not in current_node.prefix and current_node.prev_sibling is not None:
729 leaf_nodes = list(current_node.prev_sibling.leaves())
730 next_node = leaf_nodes[-1] if leaf_nodes else current_node
732 # Detect infinite loop - if we've seen this node before, stop
733 # This can happen when STANDALONE_COMMENT nodes are inserted
734 # during processing
735 if id(next_node) in seen_nodes:
736 break
738 current_node = next_node
739 seen_nodes.add(id(current_node))
741 # Stop if we encounter a STANDALONE_COMMENT created by fmt processing
742 if (
743 isinstance(current_node, Leaf)
744 and current_node.type == STANDALONE_COMMENT
745 and hasattr(current_node, "fmt_pass_converted_first_leaf")
746 ):
747 break
749 if (
750 current_node.type in CLOSING_BRACKETS
751 and current_node.parent
752 and current_node.parent.type == syms.atom
753 ):
754 current_node = current_node.parent
756 if current_node.type in (token.NEWLINE, token.INDENT):
757 if not list_comments(
758 current_node.prefix, is_endmarker=False, mode=mode
759 ):
760 current_node.prefix = ""
761 break
763 if current_node.type == token.DEDENT:
764 break
766 # Special case for with expressions
767 # Without this, we can stuck inside the asexpr_test's children's children
768 if (
769 current_node.parent
770 and current_node.parent.type == syms.asexpr_test
771 and current_node.parent.parent
772 and current_node.parent.parent.type == syms.with_stmt
773 ):
774 current_node = current_node.parent
776 ignored_nodes.insert(0, current_node)
778 if current_node.prev_sibling is None and current_node.parent is not None:
779 current_node = current_node.parent
781 # Special handling for compound statements with semicolon-separated bodies
782 if isinstance(parent, Node):
783 body_node = _find_compound_statement_context(parent)
784 if body_node is not None:
785 header_nodes = _get_compound_statement_header(body_node, parent)
786 if header_nodes:
787 ignored_nodes = header_nodes + ignored_nodes
789 leaf_is_ignored = any(
790 ignored is leaf
791 or (
792 isinstance(ignored, Node)
793 and any(child is leaf for child in ignored.leaves())
794 )
795 for ignored in ignored_nodes
796 )
797 if not leaf_is_ignored:
798 leaf.prefix = leaf.prefix[comment.consumed :]
800 yield from ignored_nodes
803def is_fmt_on(container: LN, mode: Mode) -> bool:
804 """Determine whether formatting is switched on within a container.
805 Determined by whether the last `# fmt:` comment is `on` or `off`.
806 """
807 fmt_on = False
808 for comment in list_comments(container.prefix, is_endmarker=False, mode=mode):
809 if contains_fmt_directive(comment.value, FMT_ON):
810 fmt_on = True
811 elif contains_fmt_directive(comment.value, FMT_OFF):
812 fmt_on = False
813 return fmt_on
816def children_contains_fmt_on(container: LN, mode: Mode) -> bool:
817 """Determine if children have formatting switched on."""
818 for child in container.children:
819 leaf = first_leaf_of(child)
820 if leaf is not None and is_fmt_on(leaf, mode=mode):
821 return True
823 return False
826def contains_pragma_comment(comment_list: list[Leaf]) -> bool:
827 """
828 Returns:
829 True iff one of the comments in @comment_list is a pragma used by one
830 of the more common static analysis tools for python (e.g. mypy, flake8,
831 pylint).
832 """
833 for comment in comment_list:
834 if comment.value.startswith(("# type:", "# noqa", "# pylint:")):
835 return True
837 return False
840def contains_fmt_directive(
841 comment_line: str, directives: set[str] = FMT_OFF | FMT_ON | FMT_SKIP
842) -> bool:
843 """
844 Checks if the given comment contains format directives, alone or paired with
845 other comments.
847 Defaults to checking all directives (skip, off, on, yapf), but can be
848 narrowed to specific ones.
850 Matching styles:
851 # foobar <-- single comment
852 # foobar # foobar # foobar <-- multiple comments
853 # foobar; foobar <-- list of comments (; separated)
854 """
855 semantic_comment_blocks = [
856 comment_line,
857 *[
858 _COMMENT_PREFIX + comment.strip()
859 for comment in comment_line.split(_COMMENT_PREFIX)[1:]
860 ],
861 *[
862 _COMMENT_PREFIX + comment.strip()
863 for comment in comment_line.strip(_COMMENT_PREFIX).split(
864 _COMMENT_LIST_SEPARATOR
865 )
866 ],
867 ]
869 return any(comment in directives for comment in semantic_comment_blocks)