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, 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
23# types
24LN = Union[Leaf, Node]
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"}
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}
34COMMENT_EXCEPTIONS = " !:#'"
35_COMMENT_PREFIX = "# "
36_COMMENT_LIST_SEPARATOR = ";"
39@dataclass
40class ProtoComment:
41 """Describes a piece of syntax that is a comment.
43 It's not a :class:`blib2to3.pytree.Leaf` so that:
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 """
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
59def generate_comments(leaf: LN, mode: Mode) -> Iterator[Leaf]:
60 """Clean the prefix of the `leaf` and generate comments from it, if any.
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.
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.
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.
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)
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
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
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
136def normalize_trailing_prefix(leaf: LN, total_consumed: int) -> None:
137 """Normalize the prefix that's left over after generating comments.
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
148 leaf.prefix = ""
151def make_comment(content: str, mode: Mode) -> str:
152 """Return a consistently formatted comment from the given `content` string.
154 All comments (except for "##", "#!", "#:", '#'") should have a single
155 space between the hash sign and the content.
157 If `content` didn't start with a hash sign, one is provided.
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 "#"
166 # Preserve comments with fmt directives exactly as-is
167 if content.startswith("#") and contains_fmt_directive(content):
168 return content
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()
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
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
465 if is_fmt_skip:
466 hidden_value += comment.leading_whitespace + comment.value
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]
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
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)"
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 )
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`.
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
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
542def _find_compound_statement_context(parent: Node) -> Node | None:
543 """Return the body node of a compound statement if we should respect fmt: skip.
545 This handles one-line compound statements like:
546 if condition: body # fmt: skip
548 When Black expands such statements, they temporarily look like:
549 if condition:
550 body # fmt: skip
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
558 if not isinstance(parent.parent, Node):
559 return None
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
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
580 return None
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.
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
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 []
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 []
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
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
647 if Preview.fix_fmt_skip_in_one_liners in mode and not prev_sibling and parent:
648 prev_sibling = parent.prev_sibling
650 if prev_sibling is not None:
651 leaf.prefix = leaf.prefix[comment.consumed :]
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
664 # Generates the nodes to be ignored by `fmt: skip`.
666 # Nodes to ignore are the ones on the same line as the
667 # `# fmt: skip` comment, excluding the `# fmt: skip`
668 # node itself.
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.
680 # Include all visited LEAVES in the ignored list, except INDENT
681 # or NEWLINE leaves.
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
688 # Track seen nodes to detect cycles that can occur after tree modifications
689 seen_nodes = {id(current_node)}
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
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
701 current_node = next_node
702 seen_nodes.add(id(current_node))
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
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
719 if current_node.type in (token.NEWLINE, token.INDENT):
720 current_node.prefix = ""
721 break
723 if current_node.type == token.DEDENT:
724 break
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
736 ignored_nodes.insert(0, current_node)
738 if current_node.prev_sibling is None and current_node.parent is not None:
739 current_node = current_node.parent
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
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)
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
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
793 return False
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
807 return False
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.
817 Defaults to checking all directives (skip, off, on, yapf), but can be
818 narrowed to specific ones.
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 ]
839 return any(comment in directives for comment in semantic_comment_blocks)