• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

psf / black / 19746987638

27 Nov 2025 08:13PM UTC coverage: 95.815% (-0.1%) from 95.915%
19746987638

Pull #4749

github

web-flow
Merge ad111d890 into 6ea3ef87b
Pull Request #4749: Upgrade mypy

5149 of 5425 branches covered (94.91%)

7876 of 8220 relevant lines covered (95.82%)

4.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.27
/src/black/comments.py
1
import re
5✔
2
from collections.abc import Collection, Iterator
5✔
3
from dataclasses import dataclass
5✔
4
from functools import lru_cache
5✔
5
from typing import Final, Union
5✔
6

7
from black.mode import Mode, Preview
5✔
8
from black.nodes import (
5✔
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
)
20
from blib2to3.pgen2 import token
5✔
21
from blib2to3.pytree import Leaf, Node
5✔
22

23
# types
24
LN = Union[Leaf, Node]
5✔
25

26
FMT_OFF: Final = {"# fmt: off", "# fmt:off", "# yapf: disable"}
5✔
27
FMT_SKIP: Final = {"# fmt: skip", "# fmt:skip"}
5✔
28
FMT_ON: Final = {"# fmt: on", "# fmt:on", "# yapf: enable"}
5✔
29

30
# Compound statements we care about for fmt: skip handling
31
# (excludes except_clause and case_block which aren't standalone compound statements)
32
_COMPOUND_STATEMENTS: Final = STATEMENT - {syms.except_clause, syms.case_block}
5✔
33

34
COMMENT_EXCEPTIONS = " !:#'"
5✔
35
_COMMENT_PREFIX = "# "
5✔
36
_COMMENT_LIST_SEPARATOR = ";"
5✔
37

38

39
@dataclass
5✔
40
class ProtoComment:
5✔
41
    """Describes a piece of syntax that is a comment.
42

43
    It's not a :class:`blib2to3.pytree.Leaf` so that:
44

45
    * it can be cached (`Leaf` objects should not be reused more than once as
46
      they store their lineno, column, prefix, and parent information);
47
    * `newlines` and `consumed` fields are kept separate from the `value`. This
48
      simplifies handling of special marker comments like ``# fmt: off/on``.
49
    """
50

51
    type: int  # token.COMMENT or STANDALONE_COMMENT
5✔
52
    value: str  # content of the comment
5✔
53
    newlines: int  # how many newlines before the comment
5✔
54
    consumed: int  # how many characters of the original leaf's prefix did we consume
5✔
55
    form_feed: bool  # is there a form feed before the comment
5✔
56
    leading_whitespace: str  # leading whitespace before the comment, if any
5✔
57

58

59
def generate_comments(leaf: LN, mode: Mode) -> Iterator[Leaf]:
5✔
60
    """Clean the prefix of the `leaf` and generate comments from it, if any.
61

62
    Comments in lib2to3 are shoved into the whitespace prefix.  This happens
63
    in `pgen2/driver.py:Driver.parse_tokens()`.  This was a brilliant implementation
64
    move because it does away with modifying the grammar to include all the
65
    possible places in which comments can be placed.
66

67
    The sad consequence for us though is that comments don't "belong" anywhere.
68
    This is why this function generates simple parentless Leaf objects for
69
    comments.  We simply don't know what the correct parent should be.
70

71
    No matter though, we can live without this.  We really only need to
72
    differentiate between inline and standalone comments.  The latter don't
73
    share the line with any code.
74

75
    Inline comments are emitted as regular token.COMMENT leaves.  Standalone
76
    are emitted with a fake STANDALONE_COMMENT token identifier.
77
    """
78
    total_consumed = 0
5✔
79
    for pc in list_comments(
5✔
80
        leaf.prefix, is_endmarker=leaf.type == token.ENDMARKER, mode=mode
81
    ):
82
        total_consumed = pc.consumed
5✔
83
        prefix = make_simple_prefix(pc.newlines, pc.form_feed)
5✔
84
        yield Leaf(pc.type, pc.value, prefix=prefix)
5✔
85
    normalize_trailing_prefix(leaf, total_consumed)
5✔
86

87

88
@lru_cache(maxsize=4096)
5✔
89
def list_comments(prefix: str, *, is_endmarker: bool, mode: Mode) -> list[ProtoComment]:
5✔
90
    """Return a list of :class:`ProtoComment` objects parsed from the given `prefix`."""
91
    result: list[ProtoComment] = []
5✔
92
    if not prefix or "#" not in prefix:
5✔
93
        return result
5✔
94

95
    consumed = 0
5✔
96
    nlines = 0
5✔
97
    ignored_lines = 0
5✔
98
    form_feed = False
5✔
99
    for index, full_line in enumerate(re.split("\r?\n|\r", prefix)):
5✔
100
        consumed += len(full_line) + 1  # adding the length of the split '\n'
5✔
101
        match = re.match(r"^(\s*)(\S.*|)$", full_line)
5✔
102
        assert match
5✔
103
        whitespace, line = match.groups()
5✔
104
        if not line:
5✔
105
            nlines += 1
5✔
106
            if "\f" in full_line:
5✔
107
                form_feed = True
5✔
108
        if not line.startswith("#"):
5✔
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("\\"):
5✔
113
                ignored_lines += 1
5✔
114
            continue
5✔
115

116
        if index == ignored_lines and not is_endmarker:
5✔
117
            comment_type = token.COMMENT  # simple trailing comment
5✔
118
        else:
119
            comment_type = STANDALONE_COMMENT
5✔
120
        comment = make_comment(line, mode=mode)
5✔
121
        result.append(
5✔
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
5✔
132
        nlines = 0
5✔
133
    return result
5✔
134

135

136
def normalize_trailing_prefix(leaf: LN, total_consumed: int) -> None:
5✔
137
    """Normalize the prefix that's left over after generating comments.
138

139
    Note: don't use backslashes for formatting or you'll lose your voting rights.
140
    """
141
    remainder = leaf.prefix[total_consumed:]
5✔
142
    if "\\" not in remainder:
5✔
143
        nl_count = remainder.count("\n")
5✔
144
        form_feed = "\f" in remainder and remainder.endswith("\n")
5✔
145
        leaf.prefix = make_simple_prefix(nl_count, form_feed)
5✔
146
        return
5✔
147

148
    leaf.prefix = ""
5✔
149

150

151
def make_comment(content: str, mode: Mode) -> str:
5✔
152
    """Return a consistently formatted comment from the given `content` string.
153

154
    All comments (except for "##", "#!", "#:", '#'") should have a single
155
    space between the hash sign and the content.
156

157
    If `content` didn't start with a hash sign, one is provided.
158

159
    Comments containing fmt directives are preserved exactly as-is to respect
160
    user intent (e.g., `#no space # fmt: skip` stays as-is).
161
    """
162
    content = content.rstrip()
5✔
163
    if not content:
5!
164
        return "#"
×
165

166
    # Preserve comments with fmt directives exactly as-is
167
    if content.startswith("#") and _contains_fmt_directive(content):
5✔
168
        return content
5✔
169

170
    if content[0] == "#":
5!
171
        content = content[1:]
5✔
172
    if (
5✔
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
5✔
178
    if (
5✔
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)
5✔
185
        content = type_part.strip() + ": " + value_part.strip()
5✔
186

187
    if content and content[0] not in COMMENT_EXCEPTIONS:
5✔
188
        content = " " + content
5✔
189
    return "#" + content
5✔
190

191

192
def normalize_fmt_off(
5✔
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
5✔
197
    while try_again:
5✔
198
        try_again = convert_one_fmt_off_pair(node, mode, lines)
5✔
199

200

201
def _should_process_fmt_comment(
5✔
202
    comment: ProtoComment, leaf: Leaf
203
) -> tuple[bool, bool, bool]:
204
    """Check if comment should be processed for fmt handling.
205

206
    Returns (should_process, is_fmt_off, is_fmt_skip).
207
    """
208
    is_fmt_off = _contains_fmt_directive(comment.value, FMT_OFF)
5✔
209
    is_fmt_skip = _contains_fmt_directive(comment.value, FMT_SKIP)
5✔
210

211
    if not is_fmt_off and not is_fmt_skip:
5✔
212
        return False, False, False
5✔
213

214
    # Invalid use when `# fmt: off` is applied before a closing bracket
215
    if is_fmt_off and leaf.type in CLOSING_BRACKETS:
5✔
216
        return False, False, False
5✔
217

218
    return True, is_fmt_off, is_fmt_skip
5✔
219

220

221
def _is_valid_standalone_fmt_comment(
5✔
222
    comment: ProtoComment, leaf: Leaf, is_fmt_off: bool, is_fmt_skip: bool
223
) -> bool:
224
    """Check if comment is a valid standalone fmt directive.
225

226
    We only want standalone comments. If there's no previous leaf or if
227
    the previous leaf is indentation, it's a standalone comment in disguise.
228
    """
229
    if comment.type == STANDALONE_COMMENT:
5✔
230
        return True
5✔
231

232
    prev = preceding_leaf(leaf)
5✔
233
    if not prev:
5✔
234
        return True
5✔
235

236
    # Treat STANDALONE_COMMENT nodes as whitespace for check
237
    if is_fmt_off and prev.type not in WHITESPACE and prev.type != STANDALONE_COMMENT:
5✔
238
        return False
5✔
239
    if is_fmt_skip and prev.type in WHITESPACE:
5✔
240
        return False
5✔
241

242
    return True
5✔
243

244

245
def _handle_comment_only_fmt_block(
5✔
246
    leaf: Leaf,
247
    comment: ProtoComment,
248
    previous_consumed: int,
249
    mode: Mode,
250
) -> bool:
251
    """Handle fmt:off/on blocks that contain only comments.
252

253
    Returns True if a block was converted, False otherwise.
254
    """
255
    all_comments = list_comments(leaf.prefix, is_endmarker=False, mode=mode)
5✔
256

257
    # Find the first fmt:off and its matching fmt:on
258
    fmt_off_idx = None
5✔
259
    fmt_on_idx = None
5✔
260
    for idx, c in enumerate(all_comments):
5!
261
        if fmt_off_idx is None and c.value in FMT_OFF:
5✔
262
            fmt_off_idx = idx
5✔
263
        if fmt_off_idx is not None and idx > fmt_off_idx and c.value in FMT_ON:
5✔
264
            fmt_on_idx = idx
5✔
265
            break
5✔
266

267
    # Only proceed if we found both directives
268
    if fmt_on_idx is None or fmt_off_idx is None:
5!
269
        return False
×
270

271
    comment = all_comments[fmt_off_idx]
5✔
272
    fmt_on_comment = all_comments[fmt_on_idx]
5✔
273
    original_prefix = leaf.prefix
5✔
274

275
    # Build the hidden value
276
    start_pos = comment.consumed
5✔
277
    end_pos = fmt_on_comment.consumed
5✔
278
    content_between_and_fmt_on = original_prefix[start_pos:end_pos]
5✔
279
    hidden_value = comment.value + "\n" + content_between_and_fmt_on
5✔
280

281
    if hidden_value.endswith("\n"):
5!
282
        hidden_value = hidden_value[:-1]
5✔
283

284
    # Build the standalone comment prefix - preserve all content before fmt:off
285
    # including any comments that precede it
286
    if fmt_off_idx == 0:
5✔
287
        # No comments before fmt:off, use previous_consumed
288
        pre_fmt_off_consumed = previous_consumed
5✔
289
    else:
290
        # Use the consumed position of the last comment before fmt:off
291
        # This preserves all comments and content before the fmt:off directive
292
        pre_fmt_off_consumed = all_comments[fmt_off_idx - 1].consumed
5✔
293

294
    standalone_comment_prefix = (
5✔
295
        original_prefix[:pre_fmt_off_consumed] + "\n" * comment.newlines
296
    )
297

298
    fmt_off_prefix = original_prefix.split(comment.value)[0]
5✔
299
    if "\n" in fmt_off_prefix:
5✔
300
        fmt_off_prefix = fmt_off_prefix.split("\n")[-1]
5✔
301
    standalone_comment_prefix += fmt_off_prefix
5✔
302

303
    # Update leaf prefix
304
    leaf.prefix = original_prefix[fmt_on_comment.consumed :]
5✔
305

306
    # Insert the STANDALONE_COMMENT
307
    parent = leaf.parent
5✔
308
    assert parent is not None, "INTERNAL ERROR: fmt: on/off handling (prefix only)"
5✔
309

310
    leaf_idx = None
5✔
311
    for idx, child in enumerate(parent.children):
5!
312
        if child is leaf:
5✔
313
            leaf_idx = idx
5✔
314
            break
5✔
315

316
    assert leaf_idx is not None, "INTERNAL ERROR: fmt: on/off handling (leaf index)"
5✔
317

318
    parent.insert_child(
5✔
319
        leaf_idx,
320
        Leaf(
321
            STANDALONE_COMMENT,
322
            hidden_value,
323
            prefix=standalone_comment_prefix,
324
            fmt_pass_converted_first_leaf=None,
325
        ),
326
    )
327
    return True
5✔
328

329

330
def convert_one_fmt_off_pair(
5✔
331
    node: Node, mode: Mode, lines: Collection[tuple[int, int]]
332
) -> bool:
333
    """Convert content of a single `# fmt: off`/`# fmt: on` into a standalone comment.
334

335
    Returns True if a pair was converted.
336
    """
337
    for leaf in node.leaves():
5✔
338
        # Skip STANDALONE_COMMENT nodes that were created by fmt:off/on processing
339
        # to avoid reprocessing them in subsequent iterations
340
        if (
5✔
341
            leaf.type == STANDALONE_COMMENT
342
            and hasattr(leaf, "fmt_pass_converted_first_leaf")
343
            and leaf.fmt_pass_converted_first_leaf is None
344
        ):
345
            continue
5✔
346

347
        previous_consumed = 0
5✔
348
        for comment in list_comments(leaf.prefix, is_endmarker=False, mode=mode):
5✔
349
            should_process, is_fmt_off, is_fmt_skip = _should_process_fmt_comment(
5✔
350
                comment, leaf
351
            )
352
            if not should_process:
5✔
353
                previous_consumed = comment.consumed
5✔
354
                continue
5✔
355

356
            if not _is_valid_standalone_fmt_comment(
5✔
357
                comment, leaf, is_fmt_off, is_fmt_skip
358
            ):
359
                previous_consumed = comment.consumed
5✔
360
                continue
5✔
361

362
            ignored_nodes = list(generate_ignored_nodes(leaf, comment, mode))
5✔
363

364
            # Handle comment-only blocks
365
            if not ignored_nodes and is_fmt_off:
5✔
366
                if _handle_comment_only_fmt_block(
5!
367
                    leaf, comment, previous_consumed, mode
368
                ):
369
                    return True
5✔
370
                continue
×
371

372
            # Need actual nodes to process
373
            if not ignored_nodes:
5!
374
                continue
×
375

376
            # Handle regular fmt blocks
377

378
            _handle_regular_fmt_block(
5✔
379
                ignored_nodes,
380
                comment,
381
                previous_consumed,
382
                is_fmt_skip,
383
                lines,
384
                leaf,
385
            )
386
            return True
5✔
387

388
    return False
5✔
389

390

391
def _handle_regular_fmt_block(
5✔
392
    ignored_nodes: list[LN],
393
    comment: ProtoComment,
394
    previous_consumed: int,
395
    is_fmt_skip: bool,
396
    lines: Collection[tuple[int, int]],
397
    leaf: Leaf,
398
) -> None:
399
    """Handle fmt blocks with actual AST nodes."""
400
    first = ignored_nodes[0]  # Can be a container node with the `leaf`.
5✔
401
    parent = first.parent
5✔
402
    prefix = first.prefix
5✔
403

404
    if comment.value in FMT_OFF:
5✔
405
        first.prefix = prefix[comment.consumed :]
5✔
406
    if is_fmt_skip:
5✔
407
        first.prefix = ""
5✔
408
        standalone_comment_prefix = prefix
5✔
409
    else:
410
        standalone_comment_prefix = prefix[:previous_consumed] + "\n" * comment.newlines
5✔
411

412
    hidden_value = "".join(str(n) for n in ignored_nodes)
5✔
413
    comment_lineno = leaf.lineno - comment.newlines
5✔
414

415
    if comment.value in FMT_OFF:
5✔
416
        fmt_off_prefix = ""
5✔
417
        if len(lines) > 0 and not any(
5✔
418
            line[0] <= comment_lineno <= line[1] for line in lines
419
        ):
420
            # keeping indentation of comment by preserving original whitespaces.
421
            fmt_off_prefix = prefix.split(comment.value)[0]
5✔
422
            if "\n" in fmt_off_prefix:
5✔
423
                fmt_off_prefix = fmt_off_prefix.split("\n")[-1]
5✔
424
        standalone_comment_prefix += fmt_off_prefix
5✔
425
        hidden_value = comment.value + "\n" + hidden_value
5✔
426

427
    if is_fmt_skip:
5✔
428
        hidden_value += comment.leading_whitespace + comment.value
5✔
429

430
    if hidden_value.endswith("\n"):
5✔
431
        # That happens when one of the `ignored_nodes` ended with a NEWLINE
432
        # leaf (possibly followed by a DEDENT).
433
        hidden_value = hidden_value[:-1]
5✔
434

435
    first_idx: int | None = None
5✔
436
    for ignored in ignored_nodes:
5✔
437
        index = ignored.remove()
5✔
438
        if first_idx is None:
5✔
439
            first_idx = index
5✔
440

441
    assert parent is not None, "INTERNAL ERROR: fmt: on/off handling (1)"
5✔
442
    assert first_idx is not None, "INTERNAL ERROR: fmt: on/off handling (2)"
5✔
443

444
    parent.insert_child(
5✔
445
        first_idx,
446
        Leaf(
447
            STANDALONE_COMMENT,
448
            hidden_value,
449
            prefix=standalone_comment_prefix,
450
            fmt_pass_converted_first_leaf=first_leaf_of(first),
451
        ),
452
    )
453

454

455
def generate_ignored_nodes(
5✔
456
    leaf: Leaf, comment: ProtoComment, mode: Mode
457
) -> Iterator[LN]:
458
    """Starting from the container of `leaf`, generate all leaves until `# fmt: on`.
459

460
    If comment is skip, returns leaf only.
461
    Stops at the end of the block.
462
    """
463
    if _contains_fmt_directive(comment.value, FMT_SKIP):
5✔
464
        yield from _generate_ignored_nodes_from_fmt_skip(leaf, comment, mode)
5✔
465
        return
5✔
466
    container: LN | None = container_of(leaf)
5✔
467
    while container is not None and container.type != token.ENDMARKER:
5✔
468
        if is_fmt_on(container, mode=mode):
5✔
469
            return
5✔
470

471
        # fix for fmt: on in children
472
        if children_contains_fmt_on(container, mode=mode):
5✔
473
            for index, child in enumerate(container.children):
5!
474
                if isinstance(child, Leaf) and is_fmt_on(child, mode=mode):
5✔
475
                    if child.type in CLOSING_BRACKETS:
5✔
476
                        # This means `# fmt: on` is placed at a different bracket level
477
                        # than `# fmt: off`. This is an invalid use, but as a courtesy,
478
                        # we include this closing bracket in the ignored nodes.
479
                        # The alternative is to fail the formatting.
480
                        yield child
5✔
481
                    return
5✔
482
                if (
5✔
483
                    child.type == token.INDENT
484
                    and index < len(container.children) - 1
485
                    and children_contains_fmt_on(
486
                        container.children[index + 1], mode=mode
487
                    )
488
                ):
489
                    # This means `# fmt: on` is placed right after an indentation
490
                    # level, and we shouldn't swallow the previous INDENT token.
491
                    return
5✔
492
                if children_contains_fmt_on(child, mode=mode):
5✔
493
                    return
5✔
494
                yield child
5✔
495
        else:
496
            if container.type == token.DEDENT and container.next_sibling is None:
5✔
497
                # This can happen when there is no matching `# fmt: on` comment at the
498
                # same level as `# fmt: on`. We need to keep this DEDENT.
499
                return
5✔
500
            yield container
5✔
501
            container = container.next_sibling
5✔
502

503

504
def _find_compound_statement_context(parent: Node) -> Node | None:
5✔
505
    """Return the body node of a compound statement if we should respect fmt: skip.
506

507
    This handles one-line compound statements like:
508
        if condition: body  # fmt: skip
509

510
    When Black expands such statements, they temporarily look like:
511
        if condition:
512
            body  # fmt: skip
513

514
    In both cases, we want to return the body node (either the simple_stmt directly
515
    or the suite containing it).
516
    """
517
    if parent.type != syms.simple_stmt:
5✔
518
        return None
5✔
519

520
    if not isinstance(parent.parent, Node):
5!
521
        return None
×
522

523
    # Case 1: Expanded form after Black's initial formatting pass.
524
    # The one-liner has been split across multiple lines:
525
    #     if True:
526
    #         print("a"); print("b")  # fmt: skip
527
    # Structure: compound_stmt -> suite -> simple_stmt
528
    if (
5✔
529
        parent.parent.type == syms.suite
530
        and isinstance(parent.parent.parent, Node)
531
        and parent.parent.parent.type in _COMPOUND_STATEMENTS
532
    ):
533
        return parent.parent
5✔
534

535
    # Case 2: Original one-line form from the input source.
536
    # The statement is still on a single line:
537
    #     if True: print("a"); print("b")  # fmt: skip
538
    # Structure: compound_stmt -> simple_stmt
539
    if parent.parent.type in _COMPOUND_STATEMENTS:
5✔
540
        return parent
5✔
541

542
    return None
5✔
543

544

545
def _should_keep_compound_statement_inline(
5✔
546
    body_node: Node, simple_stmt_parent: Node
547
) -> bool:
548
    """Check if a compound statement should be kept on one line.
549

550
    Returns True only for compound statements with semicolon-separated bodies,
551
    like: if True: print("a"); print("b")  # fmt: skip
552
    """
553
    # Check if there are semicolons in the body
554
    for leaf in body_node.leaves():
5✔
555
        if leaf.type == token.SEMI:
5✔
556
            # Verify it's a single-line body (one simple_stmt)
557
            if body_node.type == syms.suite:
5!
558
                # After formatting: check suite has one simple_stmt child
559
                simple_stmts = [
×
560
                    child
561
                    for child in body_node.children
562
                    if child.type == syms.simple_stmt
563
                ]
564
                return len(simple_stmts) == 1 and simple_stmts[0] is simple_stmt_parent
×
565
            else:
566
                # Original form: body_node IS the simple_stmt
567
                return body_node is simple_stmt_parent
5✔
568
    return False
5✔
569

570

571
def _get_compound_statement_header(
5✔
572
    body_node: Node, simple_stmt_parent: Node
573
) -> list[LN]:
574
    """Get header nodes for a compound statement that should be preserved inline."""
575
    if not _should_keep_compound_statement_inline(body_node, simple_stmt_parent):
5✔
576
        return []
5✔
577

578
    # Get the compound statement (parent of body)
579
    compound_stmt = body_node.parent
5✔
580
    if compound_stmt is None or compound_stmt.type not in _COMPOUND_STATEMENTS:
5!
581
        return []
×
582

583
    # Collect all header leaves before the body
584
    header_leaves: list[LN] = []
5✔
585
    for child in compound_stmt.children:
5!
586
        if child is body_node:
5✔
587
            break
5✔
588
        if isinstance(child, Leaf):
5✔
589
            if child.type not in (token.NEWLINE, token.INDENT):
5!
590
                header_leaves.append(child)
5✔
591
        else:
592
            header_leaves.extend(child.leaves())
5✔
593
    return header_leaves
5✔
594

595

596
def _generate_ignored_nodes_from_fmt_skip(
5✔
597
    leaf: Leaf, comment: ProtoComment, mode: Mode
598
) -> Iterator[LN]:
599
    """Generate all leaves that should be ignored by the `# fmt: skip` from `leaf`."""
600
    prev_sibling = leaf.prev_sibling
5✔
601
    parent = leaf.parent
5✔
602
    ignored_nodes: list[LN] = []
5✔
603
    # Need to properly format the leaf prefix to compare it to comment.value,
604
    # which is also formatted
605
    comments = list_comments(leaf.prefix, is_endmarker=False, mode=mode)
5✔
606
    if not comments or comment.value != comments[0].value:
5!
607
        return
×
608
    if prev_sibling is not None:
5✔
609
        leaf.prefix = leaf.prefix[comment.consumed :]
5✔
610

611
        if Preview.fix_fmt_skip_in_one_liners not in mode:
5✔
612
            siblings = [prev_sibling]
5✔
613
            while (
5✔
614
                "\n" not in prev_sibling.prefix
615
                and prev_sibling.prev_sibling is not None
616
            ):
617
                prev_sibling = prev_sibling.prev_sibling
5✔
618
                siblings.insert(0, prev_sibling)
5✔
619
            yield from siblings
5✔
620
            return
5✔
621

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

624
        # Nodes to ignore are the ones on the same line as the
625
        # `# fmt: skip` comment, excluding the `# fmt: skip`
626
        # node itself.
627

628
        # Traversal process (starting at the `# fmt: skip` node):
629
        # 1. Move to the `prev_sibling` of the current node.
630
        # 2. If `prev_sibling` has children, go to its rightmost leaf.
631
        # 3. If there's no `prev_sibling`, move up to the parent
632
        # node and repeat.
633
        # 4. Continue until:
634
        #    a. You encounter an `INDENT` or `NEWLINE` node (indicates
635
        #       start of the line).
636
        #    b. You reach the root node.
637

638
        # Include all visited LEAVES in the ignored list, except INDENT
639
        # or NEWLINE leaves.
640

641
        current_node = prev_sibling
5✔
642
        ignored_nodes = [current_node]
5✔
643
        if current_node.prev_sibling is None and current_node.parent is not None:
5✔
644
            current_node = current_node.parent
5✔
645
        while "\n" not in current_node.prefix and current_node.prev_sibling is not None:
5✔
646
            leaf_nodes = list(current_node.prev_sibling.leaves())
5✔
647
            current_node = leaf_nodes[-1] if leaf_nodes else current_node
5✔
648

649
            if current_node.type in (token.NEWLINE, token.INDENT):
5✔
650
                current_node.prefix = ""
5✔
651
                break
5✔
652

653
            # Special case for with expressions
654
            # Without this, we can stuck inside the asexpr_test's children's children
655
            if (
5✔
656
                current_node.parent
657
                and current_node.parent.type == syms.asexpr_test
658
                and current_node.parent.parent
659
                and current_node.parent.parent.type == syms.with_stmt
660
            ):
661
                current_node = current_node.parent
5✔
662

663
            ignored_nodes.insert(0, current_node)
5✔
664

665
            if current_node.prev_sibling is None and current_node.parent is not None:
5✔
666
                current_node = current_node.parent
5✔
667

668
        # Special handling for compound statements with semicolon-separated bodies
669
        if Preview.fix_fmt_skip_in_one_liners in mode and isinstance(parent, Node):
5!
670
            body_node = _find_compound_statement_context(parent)
5✔
671
            if body_node is not None:
5✔
672
                header_nodes = _get_compound_statement_header(body_node, parent)
5✔
673
                if header_nodes:
5✔
674
                    ignored_nodes = header_nodes + ignored_nodes
5✔
675

676
        yield from ignored_nodes
5✔
677
    elif (
5!
678
        parent is not None and parent.type == syms.suite and leaf.type == token.NEWLINE
679
    ):
680
        # The `# fmt: skip` is on the colon line of the if/while/def/class/...
681
        # statements. The ignored nodes should be previous siblings of the
682
        # parent suite node.
683
        leaf.prefix = ""
5✔
684
        parent_sibling = parent.prev_sibling
5✔
685
        while parent_sibling is not None and parent_sibling.type != syms.suite:
5✔
686
            ignored_nodes.insert(0, parent_sibling)
5✔
687
            parent_sibling = parent_sibling.prev_sibling
5✔
688
        # Special case for `async_stmt` where the ASYNC token is on the
689
        # grandparent node.
690
        grandparent = parent.parent
5✔
691
        if (
5✔
692
            grandparent is not None
693
            and grandparent.prev_sibling is not None
694
            and grandparent.prev_sibling.type == token.ASYNC
695
        ):
696
            ignored_nodes.insert(0, grandparent.prev_sibling)
5✔
697
        yield from iter(ignored_nodes)
5✔
698

699

700
def is_fmt_on(container: LN, mode: Mode) -> bool:
5✔
701
    """Determine whether formatting is switched on within a container.
702
    Determined by whether the last `# fmt:` comment is `on` or `off`.
703
    """
704
    fmt_on = False
5✔
705
    for comment in list_comments(container.prefix, is_endmarker=False, mode=mode):
5✔
706
        if comment.value in FMT_ON:
5✔
707
            fmt_on = True
5✔
708
        elif comment.value in FMT_OFF:
5✔
709
            fmt_on = False
5✔
710
    return fmt_on
5✔
711

712

713
def children_contains_fmt_on(container: LN, mode: Mode) -> bool:
5✔
714
    """Determine if children have formatting switched on."""
715
    for child in container.children:
5✔
716
        leaf = first_leaf_of(child)
5✔
717
        if leaf is not None and is_fmt_on(leaf, mode=mode):
5✔
718
            return True
5✔
719

720
    return False
5✔
721

722

723
def contains_pragma_comment(comment_list: list[Leaf]) -> bool:
5✔
724
    """
725
    Returns:
726
        True iff one of the comments in @comment_list is a pragma used by one
727
        of the more common static analysis tools for python (e.g. mypy, flake8,
728
        pylint).
729
    """
730
    for comment in comment_list:
5✔
731
        if comment.value.startswith(("# type:", "# noqa", "# pylint:")):
5✔
732
            return True
5✔
733

734
    return False
5✔
735

736

737
def _contains_fmt_directive(
5✔
738
    comment_line: str, directives: set[str] = FMT_OFF | FMT_ON | FMT_SKIP
739
) -> bool:
740
    """
741
    Checks if the given comment contains format directives, alone or paired with
742
    other comments.
743

744
    Defaults to checking all directives (skip, off, on, yapf), but can be
745
    narrowed to specific ones.
746

747
    Matching styles:
748
      # foobar                    <-- single comment
749
      # foobar # foobar # foobar  <-- multiple comments
750
      # foobar; foobar            <-- list of comments (; separated)
751
    """
752
    semantic_comment_blocks = [
5✔
753
        comment_line,
754
        *[
755
            _COMMENT_PREFIX + comment.strip()
756
            for comment in comment_line.split(_COMMENT_PREFIX)[1:]
757
        ],
758
        *[
759
            _COMMENT_PREFIX + comment.strip()
760
            for comment in comment_line.strip(_COMMENT_PREFIX).split(
761
                _COMMENT_LIST_SEPARATOR
762
            )
763
        ],
764
    ]
765

766
    return any(comment in directives for comment in semantic_comment_blocks)
5✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc