Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/black/comments.py: 13%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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 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 content
180 and "\N{NO-BREAK SPACE}" not in content
181 and is_type_comment_string("#" + content, mode=mode)
182 ):
183 type_part, value_part = content.split(":", 1)
184 content = type_part.strip() + ": " + value_part.strip()
186 if content and content[0] not in COMMENT_EXCEPTIONS:
187 content = " " + content
188 return "#" + content
191def normalize_fmt_off(
192 node: Node, mode: Mode, lines: Collection[tuple[int, int]]
193) -> None:
194 """Convert content between `# fmt: off`/`# fmt: on` into standalone comments."""
195 try_again = True
196 while try_again:
197 try_again = convert_one_fmt_off_pair(node, mode, lines)
200def _should_process_fmt_comment(
201 comment: ProtoComment, leaf: Leaf
202) -> tuple[bool, bool, bool]:
203 """Check if comment should be processed for fmt handling.
205 Returns (should_process, is_fmt_off, is_fmt_skip).
206 """
207 is_fmt_off = contains_fmt_directive(comment.value, FMT_OFF)
208 is_fmt_skip = contains_fmt_directive(comment.value, FMT_SKIP)
210 if not is_fmt_off and not is_fmt_skip:
211 return False, False, False
213 # Invalid use when `# fmt: off` is applied before a closing bracket
214 if is_fmt_off and leaf.type in CLOSING_BRACKETS:
215 return False, False, False
217 return True, is_fmt_off, is_fmt_skip
220def _is_valid_standalone_fmt_comment(
221 comment: ProtoComment, leaf: Leaf, is_fmt_off: bool, is_fmt_skip: bool
222) -> bool:
223 """Check if comment is a valid standalone fmt directive.
225 We only want standalone comments. If there's no previous leaf or if
226 the previous leaf is indentation, it's a standalone comment in disguise.
227 """
228 if comment.type == STANDALONE_COMMENT:
229 return True
231 prev = preceding_leaf(leaf)
232 if not prev:
233 return True
235 # Treat STANDALONE_COMMENT nodes as whitespace for check
236 if is_fmt_off and prev.type not in WHITESPACE and prev.type != STANDALONE_COMMENT:
237 return False
238 if is_fmt_skip and prev.type in WHITESPACE:
239 return False
241 return True
244def _handle_comment_only_fmt_block(
245 leaf: Leaf,
246 comment: ProtoComment,
247 previous_consumed: int,
248 mode: Mode,
249) -> bool:
250 """Handle fmt:off/on blocks that contain only comments.
252 Returns True if a block was converted, False otherwise.
253 """
254 all_comments = list_comments(leaf.prefix, is_endmarker=False, mode=mode)
256 # Find the first fmt:off and its matching fmt:on
257 fmt_off_idx = None
258 fmt_on_idx = None
259 for idx, c in enumerate(all_comments):
260 if fmt_off_idx is None and contains_fmt_directive(c.value, FMT_OFF):
261 fmt_off_idx = idx
262 if (
263 fmt_off_idx is not None
264 and idx > fmt_off_idx
265 and contains_fmt_directive(c.value, FMT_ON)
266 ):
267 fmt_on_idx = idx
268 break
270 # Only proceed if we found both directives
271 if fmt_on_idx is None or fmt_off_idx is None:
272 return False
274 comment = all_comments[fmt_off_idx]
275 fmt_on_comment = all_comments[fmt_on_idx]
276 original_prefix = leaf.prefix
278 # Build the hidden value
279 start_pos = comment.consumed
280 end_pos = fmt_on_comment.consumed
281 content_between_and_fmt_on = original_prefix[start_pos:end_pos]
282 hidden_value = comment.value + "\n" + content_between_and_fmt_on
284 if hidden_value.endswith("\n"):
285 hidden_value = hidden_value[:-1]
287 # Build the standalone comment prefix - preserve all content before fmt:off
288 # including any comments that precede it
289 if fmt_off_idx == 0:
290 # No comments before fmt:off, use previous_consumed
291 pre_fmt_off_consumed = previous_consumed
292 else:
293 # Use the consumed position of the last comment before fmt:off
294 # This preserves all comments and content before the fmt:off directive
295 pre_fmt_off_consumed = all_comments[fmt_off_idx - 1].consumed
297 standalone_comment_prefix = (
298 original_prefix[:pre_fmt_off_consumed] + "\n" * comment.newlines
299 )
301 fmt_off_prefix = original_prefix.split(comment.value)[0]
302 if "\n" in fmt_off_prefix:
303 fmt_off_prefix = fmt_off_prefix.split("\n")[-1]
304 standalone_comment_prefix += fmt_off_prefix
306 # Update leaf prefix
307 leaf.prefix = original_prefix[fmt_on_comment.consumed :]
309 # Insert the STANDALONE_COMMENT
310 parent = leaf.parent
311 assert parent is not None, "INTERNAL ERROR: fmt: on/off handling (prefix only)"
313 leaf_idx = None
314 for idx, child in enumerate(parent.children):
315 if child is leaf:
316 leaf_idx = idx
317 break
319 assert leaf_idx is not None, "INTERNAL ERROR: fmt: on/off handling (leaf index)"
321 parent.insert_child(
322 leaf_idx,
323 Leaf(
324 STANDALONE_COMMENT,
325 hidden_value,
326 prefix=standalone_comment_prefix,
327 fmt_pass_converted_first_leaf=None,
328 ),
329 )
330 return True
333def convert_one_fmt_off_pair(
334 node: Node, mode: Mode, lines: Collection[tuple[int, int]]
335) -> bool:
336 """Convert content of a single `# fmt: off`/`# fmt: on` into a standalone comment.
338 Returns True if a pair was converted.
339 """
340 for leaf in node.leaves():
341 # Skip STANDALONE_COMMENT nodes that were created by fmt:off/on/skip processing
342 # to avoid reprocessing them in subsequent iterations
343 if leaf.type == STANDALONE_COMMENT and hasattr(
344 leaf, "fmt_pass_converted_first_leaf"
345 ):
346 continue
348 previous_consumed = 0
349 for comment in list_comments(leaf.prefix, is_endmarker=False, mode=mode):
350 should_process, is_fmt_off, is_fmt_skip = _should_process_fmt_comment(
351 comment, leaf
352 )
353 if not should_process:
354 previous_consumed = comment.consumed
355 continue
357 if not _is_valid_standalone_fmt_comment(
358 comment, leaf, is_fmt_off, is_fmt_skip
359 ):
360 previous_consumed = comment.consumed
361 continue
363 ignored_nodes = list(generate_ignored_nodes(leaf, comment, mode))
365 # Handle comment-only blocks
366 if not ignored_nodes and is_fmt_off:
367 if _handle_comment_only_fmt_block(
368 leaf, comment, previous_consumed, mode
369 ):
370 return True
371 continue
373 # Need actual nodes to process
374 if not ignored_nodes:
375 continue
377 # Handle regular fmt blocks
379 _handle_regular_fmt_block(
380 ignored_nodes,
381 comment,
382 previous_consumed,
383 is_fmt_skip,
384 lines,
385 leaf,
386 )
387 return True
389 return False
392def _handle_regular_fmt_block(
393 ignored_nodes: list[LN],
394 comment: ProtoComment,
395 previous_consumed: int,
396 is_fmt_skip: bool,
397 lines: Collection[tuple[int, int]],
398 leaf: Leaf,
399) -> None:
400 """Handle fmt blocks with actual AST nodes."""
401 first = ignored_nodes[0] # Can be a container node with the `leaf`.
402 parent = first.parent
403 prefix = first.prefix
405 if contains_fmt_directive(comment.value, FMT_OFF):
406 first.prefix = prefix[comment.consumed :]
407 if is_fmt_skip:
408 first.prefix = ""
409 standalone_comment_prefix = prefix
410 else:
411 standalone_comment_prefix = prefix[:previous_consumed] + "\n" * comment.newlines
413 # Ensure STANDALONE_COMMENT nodes have trailing newlines when stringified
414 # This prevents multiple fmt: skip comments from being concatenated on one line
415 parts = []
416 for node in ignored_nodes:
417 if isinstance(node, Leaf) and node.type == STANDALONE_COMMENT:
418 # Add newline after STANDALONE_COMMENT Leaf
419 node_str = str(node)
420 if not node_str.endswith("\n"):
421 node_str += "\n"
422 parts.append(node_str)
423 elif isinstance(node, Node):
424 # For nodes that might contain STANDALONE_COMMENT leaves,
425 # we need custom stringify
426 has_standalone = any(
427 leaf.type == STANDALONE_COMMENT for leaf in node.leaves()
428 )
429 if has_standalone:
430 # Stringify node with STANDALONE_COMMENT leaves having trailing newlines
431 def stringify_node(n: LN) -> str:
432 if isinstance(n, Leaf):
433 if n.type == STANDALONE_COMMENT:
434 result = n.prefix + n.value
435 if not result.endswith("\n"):
436 result += "\n"
437 return result
438 return str(n)
439 else:
440 # For nested nodes, recursively process children
441 return "".join(stringify_node(child) for child in n.children)
443 parts.append(stringify_node(node))
444 else:
445 parts.append(str(node))
446 else:
447 parts.append(str(node))
449 hidden_value = "".join(parts)
450 comment_lineno = leaf.lineno - comment.newlines
452 if contains_fmt_directive(comment.value, FMT_OFF):
453 fmt_off_prefix = ""
454 if len(lines) > 0 and not any(
455 line[0] <= comment_lineno <= line[1] for line in lines
456 ):
457 # keeping indentation of comment by preserving original whitespaces.
458 fmt_off_prefix = prefix.split(comment.value)[0]
459 if "\n" in fmt_off_prefix:
460 fmt_off_prefix = fmt_off_prefix.split("\n")[-1]
461 standalone_comment_prefix += fmt_off_prefix
462 hidden_value = comment.value + "\n" + hidden_value
464 if is_fmt_skip:
465 hidden_value += comment.leading_whitespace + comment.value
467 if hidden_value.endswith("\n"):
468 # That happens when one of the `ignored_nodes` ended with a NEWLINE
469 # leaf (possibly followed by a DEDENT).
470 hidden_value = hidden_value[:-1]
472 first_idx: int | None = None
473 for ignored in ignored_nodes:
474 index = ignored.remove()
475 if first_idx is None:
476 first_idx = index
478 assert parent is not None, "INTERNAL ERROR: fmt: on/off handling (1)"
479 assert first_idx is not None, "INTERNAL ERROR: fmt: on/off handling (2)"
481 parent.insert_child(
482 first_idx,
483 Leaf(
484 STANDALONE_COMMENT,
485 hidden_value,
486 prefix=standalone_comment_prefix,
487 fmt_pass_converted_first_leaf=first_leaf_of(first),
488 ),
489 )
492def generate_ignored_nodes(
493 leaf: Leaf, comment: ProtoComment, mode: Mode
494) -> Iterator[LN]:
495 """Starting from the container of `leaf`, generate all leaves until `# fmt: on`.
497 If comment is skip, returns leaf only.
498 Stops at the end of the block.
499 """
500 if contains_fmt_directive(comment.value, FMT_SKIP):
501 yield from _generate_ignored_nodes_from_fmt_skip(leaf, comment, mode)
502 return
503 container: LN | None = container_of(leaf)
504 while container is not None and container.type != token.ENDMARKER:
505 if is_fmt_on(container, mode=mode):
506 return
508 # fix for fmt: on in children
509 if children_contains_fmt_on(container, mode=mode):
510 for index, child in enumerate(container.children):
511 if isinstance(child, Leaf) and is_fmt_on(child, mode=mode):
512 if child.type in CLOSING_BRACKETS:
513 # This means `# fmt: on` is placed at a different bracket level
514 # than `# fmt: off`. This is an invalid use, but as a courtesy,
515 # we include this closing bracket in the ignored nodes.
516 # The alternative is to fail the formatting.
517 yield child
518 return
519 if (
520 child.type == token.INDENT
521 and index < len(container.children) - 1
522 and children_contains_fmt_on(
523 container.children[index + 1], mode=mode
524 )
525 ):
526 # This means `# fmt: on` is placed right after an indentation
527 # level, and we shouldn't swallow the previous INDENT token.
528 return
529 if children_contains_fmt_on(child, mode=mode):
530 return
531 yield child
532 else:
533 if container.type == token.DEDENT and container.next_sibling is None:
534 # This can happen when there is no matching `# fmt: on` comment at the
535 # same level as `# fmt: on`. We need to keep this DEDENT.
536 return
537 yield container
538 container = container.next_sibling
541def _find_compound_statement_context(parent: Node) -> Node | None:
542 """Return the body node of a compound statement if we should respect fmt: skip.
544 This handles one-line compound statements like:
545 if condition: body # fmt: skip
547 When Black expands such statements, they temporarily look like:
548 if condition:
549 body # fmt: skip
551 In both cases, we want to return the body node (either the simple_stmt directly
552 or the suite containing it).
553 """
554 if parent.type != syms.simple_stmt:
555 return None
557 if not isinstance(parent.parent, Node):
558 return None
560 # Case 1: Expanded form after Black's initial formatting pass.
561 # The one-liner has been split across multiple lines:
562 # if True:
563 # print("a"); print("b") # fmt: skip
564 # Structure: compound_stmt -> suite -> simple_stmt
565 if (
566 parent.parent.type == syms.suite
567 and isinstance(parent.parent.parent, Node)
568 and parent.parent.parent.type in _COMPOUND_STATEMENTS
569 ):
570 return parent.parent
572 # Case 2: Original one-line form from the input source.
573 # The statement is still on a single line:
574 # if True: print("a"); print("b") # fmt: skip
575 # Structure: compound_stmt -> simple_stmt
576 if parent.parent.type in _COMPOUND_STATEMENTS:
577 return parent
579 return None
582def _should_keep_compound_statement_inline(
583 body_node: Node, simple_stmt_parent: Node
584) -> bool:
585 """Check if a compound statement should be kept on one line.
587 Returns True only for compound statements with semicolon-separated bodies,
588 like: if True: print("a"); print("b") # fmt: skip
589 """
590 # Check if there are semicolons in the body
591 for leaf in body_node.leaves():
592 if leaf.type == token.SEMI:
593 # Verify it's a single-line body (one simple_stmt)
594 if body_node.type == syms.suite:
595 # After formatting: check suite has one simple_stmt child
596 simple_stmts = [
597 child
598 for child in body_node.children
599 if child.type == syms.simple_stmt
600 ]
601 return len(simple_stmts) == 1 and simple_stmts[0] is simple_stmt_parent
602 else:
603 # Original form: body_node IS the simple_stmt
604 return body_node is simple_stmt_parent
605 return False
608def _get_compound_statement_header(
609 body_node: Node, simple_stmt_parent: Node
610) -> list[LN]:
611 """Get header nodes for a compound statement that should be preserved inline."""
612 if not _should_keep_compound_statement_inline(body_node, simple_stmt_parent):
613 return []
615 # Get the compound statement (parent of body)
616 compound_stmt = body_node.parent
617 if compound_stmt is None or compound_stmt.type not in _COMPOUND_STATEMENTS:
618 return []
620 # Collect all header leaves before the body
621 header_leaves: list[LN] = []
622 for child in compound_stmt.children:
623 if child is body_node:
624 break
625 if isinstance(child, Leaf):
626 if child.type not in (token.NEWLINE, token.INDENT):
627 header_leaves.append(child)
628 else:
629 header_leaves.extend(child.leaves())
630 return header_leaves
633def _generate_ignored_nodes_from_fmt_skip(
634 leaf: Leaf, comment: ProtoComment, mode: Mode
635) -> Iterator[LN]:
636 """Generate all leaves that should be ignored by the `# fmt: skip` from `leaf`."""
637 prev_sibling = leaf.prev_sibling
638 parent = leaf.parent
639 ignored_nodes: list[LN] = []
640 # Need to properly format the leaf prefix to compare it to comment.value,
641 # which is also formatted
642 comments = list_comments(leaf.prefix, is_endmarker=False, mode=mode)
643 if not comments or comment.value != comments[0].value:
644 return
646 if not prev_sibling and parent:
647 prev_sibling = parent.prev_sibling
649 if prev_sibling is not None:
650 leaf.prefix = leaf.prefix[comment.consumed :]
652 # Generates the nodes to be ignored by `fmt: skip`.
654 # Nodes to ignore are the ones on the same line as the
655 # `# fmt: skip` comment, excluding the `# fmt: skip`
656 # node itself.
658 # Traversal process (starting at the `# fmt: skip` node):
659 # 1. Move to the `prev_sibling` of the current node.
660 # 2. If `prev_sibling` has children, go to its rightmost leaf.
661 # 3. If there's no `prev_sibling`, move up to the parent
662 # node and repeat.
663 # 4. Continue until:
664 # a. You encounter an `INDENT` or `NEWLINE` node (indicates
665 # start of the line).
666 # b. You reach the root node.
668 # Include all visited LEAVES in the ignored list, except INDENT
669 # or NEWLINE leaves.
671 current_node = prev_sibling
672 ignored_nodes = [current_node]
673 if current_node.prev_sibling is None and current_node.parent is not None:
674 current_node = current_node.parent
676 # Track seen nodes to detect cycles that can occur after tree modifications
677 seen_nodes = {id(current_node)}
679 while "\n" not in current_node.prefix and current_node.prev_sibling is not None:
680 leaf_nodes = list(current_node.prev_sibling.leaves())
681 next_node = leaf_nodes[-1] if leaf_nodes else current_node
683 # Detect infinite loop - if we've seen this node before, stop
684 # This can happen when STANDALONE_COMMENT nodes are inserted
685 # during processing
686 if id(next_node) in seen_nodes:
687 break
689 current_node = next_node
690 seen_nodes.add(id(current_node))
692 # Stop if we encounter a STANDALONE_COMMENT created by fmt processing
693 if (
694 isinstance(current_node, Leaf)
695 and current_node.type == STANDALONE_COMMENT
696 and hasattr(current_node, "fmt_pass_converted_first_leaf")
697 ):
698 break
700 if (
701 current_node.type in CLOSING_BRACKETS
702 and current_node.parent
703 and current_node.parent.type == syms.atom
704 ):
705 current_node = current_node.parent
707 if current_node.type in (token.NEWLINE, token.INDENT):
708 current_node.prefix = ""
709 break
711 if current_node.type == token.DEDENT:
712 break
714 # Special case for with expressions
715 # Without this, we can stuck inside the asexpr_test's children's children
716 if (
717 current_node.parent
718 and current_node.parent.type == syms.asexpr_test
719 and current_node.parent.parent
720 and current_node.parent.parent.type == syms.with_stmt
721 ):
722 current_node = current_node.parent
724 ignored_nodes.insert(0, current_node)
726 if current_node.prev_sibling is None and current_node.parent is not None:
727 current_node = current_node.parent
729 # Special handling for compound statements with semicolon-separated bodies
730 if isinstance(parent, Node):
731 body_node = _find_compound_statement_context(parent)
732 if body_node is not None:
733 header_nodes = _get_compound_statement_header(body_node, parent)
734 if header_nodes:
735 ignored_nodes = header_nodes + ignored_nodes
737 yield from ignored_nodes
738 elif (
739 parent is not None and parent.type == syms.suite and leaf.type == token.NEWLINE
740 ):
741 # The `# fmt: skip` is on the colon line of the if/while/def/class/...
742 # statements. The ignored nodes should be previous siblings of the
743 # parent suite node.
744 leaf.prefix = ""
745 parent_sibling = parent.prev_sibling
746 while parent_sibling is not None and parent_sibling.type != syms.suite:
747 ignored_nodes.insert(0, parent_sibling)
748 parent_sibling = parent_sibling.prev_sibling
749 # Special case for `async_stmt` where the ASYNC token is on the
750 # grandparent node.
751 grandparent = parent.parent
752 if (
753 grandparent is not None
754 and grandparent.prev_sibling is not None
755 and grandparent.prev_sibling.type == token.ASYNC
756 ):
757 ignored_nodes.insert(0, grandparent.prev_sibling)
758 yield from iter(ignored_nodes)
761def is_fmt_on(container: LN, mode: Mode) -> bool:
762 """Determine whether formatting is switched on within a container.
763 Determined by whether the last `# fmt:` comment is `on` or `off`.
764 """
765 fmt_on = False
766 for comment in list_comments(container.prefix, is_endmarker=False, mode=mode):
767 if contains_fmt_directive(comment.value, FMT_ON):
768 fmt_on = True
769 elif contains_fmt_directive(comment.value, FMT_OFF):
770 fmt_on = False
771 return fmt_on
774def children_contains_fmt_on(container: LN, mode: Mode) -> bool:
775 """Determine if children have formatting switched on."""
776 for child in container.children:
777 leaf = first_leaf_of(child)
778 if leaf is not None and is_fmt_on(leaf, mode=mode):
779 return True
781 return False
784def contains_pragma_comment(comment_list: list[Leaf]) -> bool:
785 """
786 Returns:
787 True iff one of the comments in @comment_list is a pragma used by one
788 of the more common static analysis tools for python (e.g. mypy, flake8,
789 pylint).
790 """
791 for comment in comment_list:
792 if comment.value.startswith(("# type:", "# noqa", "# pylint:")):
793 return True
795 return False
798def contains_fmt_directive(
799 comment_line: str, directives: set[str] = FMT_OFF | FMT_ON | FMT_SKIP
800) -> bool:
801 """
802 Checks if the given comment contains format directives, alone or paired with
803 other comments.
805 Defaults to checking all directives (skip, off, on, yapf), but can be
806 narrowed to specific ones.
808 Matching styles:
809 # foobar <-- single comment
810 # foobar # foobar # foobar <-- multiple comments
811 # foobar; foobar <-- list of comments (; separated)
812 """
813 semantic_comment_blocks = [
814 comment_line,
815 *[
816 _COMMENT_PREFIX + comment.strip()
817 for comment in comment_line.split(_COMMENT_PREFIX)[1:]
818 ],
819 *[
820 _COMMENT_PREFIX + comment.strip()
821 for comment in comment_line.strip(_COMMENT_PREFIX).split(
822 _COMMENT_LIST_SEPARATOR
823 )
824 ],
825 ]
827 return any(comment in directives for comment in semantic_comment_blocks)