From 66b09b8dad3e20a7f0dae63fc59038271788a0bd Mon Sep 17 00:00:00 2001 From: Sarath Francis Date: Mon, 1 Jun 2026 20:16:16 -0400 Subject: [PATCH] Fix crash on fmt: skip after opening bracket with a standalone comment A `# fmt: skip` line placed directly after an opening bracket, combined with a standalone comment among the bracket's contents, crashed with `AttributeError: 'Leaf' object has no attribute 'bracket_depth'`. The crash happened in `is_line_short_enough`. When the rendered line spans multiple physical lines, the function walks the leaves and reads `leaf.bracket_depth` on each of them. However, a closing bracket that is continued from a previous line break (a leftover `)` / `]` left on its own line) is intentionally skipped by `BracketTracker.mark` and never receives a `bracket_depth` attribute. Accessing it therefore raised. Such a leaf is not inside any bracket that opened on the current line, so its effective depth is 0. Read the attribute defensively with that default, which leaves behaviour unchanged for all already-marked leaves and stops the crash, producing valid and stable output instead. Added a regression test covering the import, call, subscript and inline `# fmt: skip` variants. --- CHANGES.md | 2 + src/black/lines.py | 21 +++++--- .../fmtskip_after_bracket_with_comment.py | 50 +++++++++++++++++++ 3 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 tests/data/cases/fmtskip_after_bracket_with_comment.py diff --git a/CHANGES.md b/CHANGES.md index 84421655c64..1eb62418e32 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,6 +21,8 @@ lengths (#5147) - Fix multiline docstring indentation when leading tabs are used inside indented docstrings (#5148) +- Fix a crash on a `# fmt: skip` line placed directly after an opening bracket when a + standalone comment is also present among the contents (#5161) ### Preview style diff --git a/src/black/lines.py b/src/black/lines.py index 4012781e195..3cdfe4555b6 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -1271,34 +1271,39 @@ def is_line_short_enough(line: Line, *, mode: Mode, line_str: str = "") -> bool: max_level_to_update: int | float = math.inf # track the depth of the MLS for i, leaf in enumerate(line.leaves): + # A closing bracket continued from a previous line break (e.g. a leftover + # `)` left on its own line) is never marked by the bracket tracker, so it + # has no `bracket_depth`. Such a leaf is not inside any bracket that opened + # on this line, so treat it as depth 0. + bracket_depth = getattr(leaf, "bracket_depth", 0) if max_level_to_update == math.inf: had_comma: int | None = None - if leaf.bracket_depth + 1 > len(commas): + if bracket_depth + 1 > len(commas): commas.append(0) - elif leaf.bracket_depth + 1 < len(commas): + elif bracket_depth + 1 < len(commas): had_comma = commas.pop() if ( had_comma is not None and multiline_string is not None - and multiline_string.bracket_depth == leaf.bracket_depth + 1 + and multiline_string.bracket_depth == bracket_depth + 1 ): # Have left the level with the MLS, stop tracking commas - max_level_to_update = leaf.bracket_depth + max_level_to_update = bracket_depth if had_comma > 0: # MLS was in parens with at least one comma - force split return False - if leaf.bracket_depth <= max_level_to_update and leaf.type == token.COMMA: + if bracket_depth <= max_level_to_update and leaf.type == token.COMMA: # Inside brackets, ignore trailing comma # directly after MLS/MLS-containing expression ignore_ctxs: list[LN | None] = [None] ignore_ctxs += multiline_string_contexts - if (line.inside_brackets or leaf.bracket_depth > 0) and ( + if (line.inside_brackets or bracket_depth > 0) and ( i != len(line.leaves) - 1 or leaf.prev_sibling not in ignore_ctxs ): - commas[leaf.bracket_depth] += 1 + commas[bracket_depth] += 1 if max_level_to_update != math.inf: - max_level_to_update = min(max_level_to_update, leaf.bracket_depth) + max_level_to_update = min(max_level_to_update, bracket_depth) if is_multiline_string(leaf): if leaf.parent and ( diff --git a/tests/data/cases/fmtskip_after_bracket_with_comment.py b/tests/data/cases/fmtskip_after_bracket_with_comment.py new file mode 100644 index 00000000000..6c642574e14 --- /dev/null +++ b/tests/data/cases/fmtskip_after_bracket_with_comment.py @@ -0,0 +1,50 @@ +# Regression tests for a crash with `AttributeError: 'Leaf' object has no +# attribute 'bracket_depth'`. A `# fmt: skip` line directly after an opening +# bracket combined with a standalone comment among the contents used to crash +# inside `is_line_short_enough`. + +from m import ( +# fmt: skip + # comment + a +) + +f( +# fmt: skip + # comment + a +) + +x[ +# fmt: skip + # comment + a +] + +from m import ( # fmt: skip + # comment + a +) + +# output + +# Regression tests for a crash with `AttributeError: 'Leaf' object has no +# attribute 'bracket_depth'`. A `# fmt: skip` line directly after an opening +# bracket combined with a standalone comment among the contents used to crash +# inside `is_line_short_enough`. + +from m import (# fmt: skip # comment + a +) + +f(# fmt: skip # comment + a +) + +x[# fmt: skip # comment + a +] + +from m import ( # fmt: skip # comment + a +)