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
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 # 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)
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.
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)
221 if not is_fmt_off and not is_fmt_skip:
222 return False, False, False
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
228 return True, is_fmt_off, is_fmt_skip
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.
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
242 prev = preceding_leaf(leaf)
243 if not prev:
244 return True
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
252 return True
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.
263 Returns True if a block was converted, False otherwise.
264 """
265 all_comments = list_comments(leaf.prefix, is_endmarker=False, mode=mode)
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
281 # Only proceed if we found both directives
282 if fmt_on_idx is None or fmt_off_idx is None:
283 return False
285 comment = all_comments[fmt_off_idx]
286 fmt_on_comment = all_comments[fmt_on_idx]
287 original_prefix = leaf.prefix
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
295 if hidden_value.endswith("\n"):
296 hidden_value = hidden_value[:-1]
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
308 preceding_prefix = original_prefix[:pre_fmt_off_consumed]
309 if fmt_off_idx > 0:
310 preceding_prefix = preceding_prefix.lstrip("\r\n")
312 standalone_comment_prefix = preceding_prefix + "\n" * comment.newlines
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
319 # Update leaf prefix
320 leaf.prefix = original_prefix[fmt_on_comment.consumed :]
322 # Insert the STANDALONE_COMMENT
323 parent = leaf.parent
324 assert parent is not None, "INTERNAL ERROR: fmt: on/off handling (prefix only)"
326 leaf_idx = None
327 for idx, child in enumerate(parent.children):
328 if child is leaf:
329 leaf_idx = idx
330 break
332 assert leaf_idx is not None, "INTERNAL ERROR: fmt: on/off handling (leaf index)"
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
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.
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
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
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
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
389 ignored_nodes = list(generate_ignored_nodes(leaf, comment, mode))
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
399 # Need actual nodes to process
400 if not ignored_nodes:
401 continue
403 # Handle regular fmt blocks
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
416 return len(leaves)
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
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
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
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)
481 parts.append(stringify_node(node))
482 else:
483 parts.append(str(node))
484 else:
485 parts.append(str(node))
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 )
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
510 if is_fmt_skip and not leaf_is_ignored:
511 hidden_value += comment.leading_whitespace + comment.value
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]
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
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)"
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 )
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`.
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
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
599def _find_compound_statement_context(parent: Node) -> Node | None:
600 """Return the body node of a compound statement if we should respect fmt: skip.
602 This handles one-line compound statements like:
603 if condition: body # fmt: skip
605 When Black expands such statements, they temporarily look like:
606 if condition:
607 body # fmt: skip
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
615 if not isinstance(parent.parent, Node):
616 return None
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
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
637 return None
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.
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
669 return any(leaf.type == token.SEMI for leaf in target.leaves())
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 []
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 []
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
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
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
721 if prev_sibling is None and parent is not None:
722 prev_sibling = parent.prev_sibling
724 if prev_sibling is None and comment.type == token.COMMENT:
725 prev_sibling = _find_closest_previous_sibling(leaf)
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`.
750 # Nodes to ignore are the ones on the same line as the
751 # `# fmt: skip` comment, excluding the `# fmt: skip`
752 # node itself.
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.
764 # Include all visited LEAVES in the ignored list, except INDENT
765 # or NEWLINE leaves.
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
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
780 # Track seen nodes to detect cycles that can occur after tree modifications
781 seen_nodes = {id(current_node)}
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
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
793 current_node = next_node
794 seen_nodes.add(id(current_node))
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
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
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
818 if current_node.type == token.DEDENT:
819 break
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
831 ignored_nodes.insert(0, current_node)
833 if current_node.prev_sibling is None and current_node.parent is not None:
834 current_node = current_node.parent
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
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]
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 :]
876 yield from ignored_nodes
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
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
899 return False
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
913 return False
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.
923 Defaults to checking all directives (skip, off, on, yapf), but can be
924 narrowed to specific ones.
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 ]
945 return any(comment in directives for comment in semantic_comment_blocks)