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 +)