Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
magic trailing commas and intentional multiline formatting (#4865)
- Fix `fix_fmt_skip_in_one_liners` crashing on `with` statements (#4853)
- Fix `fix_fmt_skip_in_one_liners` crashing on annotated parameters (#4854)
- Fix `# fmt: skip` behavior for deeply nested expressions (#4883)

### Configuration

Expand Down
58 changes: 57 additions & 1 deletion src/black/comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from black.mode import Mode, Preview
from black.nodes import (
CLOSING_BRACKETS,
OPENING_BRACKETS,
STANDALONE_COMMENT,
STATEMENT,
WHITESPACE,
Expand Down Expand Up @@ -645,7 +646,46 @@ def _generate_ignored_nodes_from_fmt_skip(
return

if Preview.fix_fmt_skip_in_one_liners in mode and not prev_sibling and parent:
prev_sibling = parent.prev_sibling
# If the current leaf doesn't have a previous sibling, it might be deeply nested
# (e.g. inside a list, function call, etc.). We need to climb up the tree
# to find the previous sibling on the same line.

# First, find the ancestor node that starts the current line
# (has a newline in its prefix, or is at the root)
line_start_node = parent
while line_start_node.parent is not None:
# The comment itself is in the leaf's prefix, so we need to check
# if the current node's prefix (before the comment) has a newline
node_prefix = (
line_start_node.prefix if hasattr(line_start_node, "prefix") else ""
)
# Skip the comment part if this is the first node (parent)
if line_start_node == parent:
# The parent node has the comment in its prefix, so we check
# what's before the comment
comment_start = node_prefix.find(comment.value)
if comment_start >= 0:
prefix_before_comment = node_prefix[:comment_start]
if "\n" in prefix_before_comment:
# There's a newline before the comment, so this node
# is at the start of the line
break
elif "\n" in node_prefix:
break
elif "\n" in node_prefix:
# This node starts on a new line, so it's the line start
break
line_start_node = line_start_node.parent

# Now find the prev_sibling by climbing from parent up to line_start_node
curr = parent
while curr is not None and curr != line_start_node.parent:
if curr.prev_sibling is not None:
prev_sibling = curr.prev_sibling
break
if curr.parent is None:
break
curr = curr.parent

if prev_sibling is not None:
leaf.prefix = leaf.prefix[comment.consumed :]
Expand Down Expand Up @@ -746,6 +786,22 @@ def _generate_ignored_nodes_from_fmt_skip(
if header_nodes:
ignored_nodes = header_nodes + ignored_nodes

# If the leaf's parent is an atom (parenthesized expression) and we've
# captured the opening bracket in our ignored_nodes, we should include
# the entire atom (including the closing bracket and the leaf itself)
# to avoid partial formatting
if (
parent is not None
and parent.type == syms.atom
and len(parent.children) >= 2
and parent.children[0].type in OPENING_BRACKETS
and parent.children[0] in ignored_nodes
):
# Replace the opening bracket and any other captured children of this atom
# with the entire atom node
ignored_nodes = [node for node in ignored_nodes if node.parent != parent]
ignored_nodes.append(parent)

yield from ignored_nodes
elif (
parent is not None and parent.type == syms.suite and leaf.type == token.NEWLINE
Expand Down
31 changes: 20 additions & 11 deletions src/black/lines.py
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,10 @@ def is_line_short_enough(line: Line, *, mode: Mode, line_str: str = "") -> bool:
for i, leaf in enumerate(line.leaves):
if max_level_to_update == math.inf:
had_comma: int | None = None
# Skip multiline_string_handling logic for leaves without bracket_depth
# (e.g., newly created leaves not yet processed by bracket tracker)
if not hasattr(leaf, "bracket_depth"):
continue
if leaf.bracket_depth + 1 > len(commas):
commas.append(0)
elif leaf.bracket_depth + 1 < len(commas):
Expand All @@ -878,17 +882,22 @@ def is_line_short_enough(line: Line, *, mode: Mode, line_str: str = "") -> bool:
# 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:
# 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 (
i != len(line.leaves) - 1 or leaf.prev_sibling not in ignore_ctxs
):
commas[leaf.bracket_depth] += 1
if max_level_to_update != math.inf:
max_level_to_update = min(max_level_to_update, leaf.bracket_depth)
# Skip bracket-depth-dependent processing for leaves without the attribute
if not hasattr(leaf, "bracket_depth"):
# Still process multiline string detection below
pass
else:
if leaf.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 (
i != len(line.leaves) - 1 or leaf.prev_sibling not in ignore_ctxs
):
commas[leaf.bracket_depth] += 1
if max_level_to_update != math.inf:
max_level_to_update = min(max_level_to_update, leaf.bracket_depth)

if is_multiline_string(leaf):
if leaf.parent and (
Expand Down
39 changes: 39 additions & 0 deletions tests/data/cases/fmtskip_nested_if.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# flags: --preview --no-preview-line-length-1
class ClassWithALongName:
Constant1 = 1
Constant2 = 2
Constant3 = 3


def test():
if (
"cond1" == "cond1"
and "cond2" == "cond2"
and 1 in ( # fmt: skip
ClassWithALongName.Constant1,
ClassWithALongName.Constant2,
ClassWithALongName.Constant3,
)
):
return True
return False

# output

class ClassWithALongName:
Constant1 = 1
Constant2 = 2
Constant3 = 3


def test():
if (
"cond1" == "cond1" and "cond2" == "cond2"
and 1 in ( # fmt: skip
ClassWithALongName.Constant1,
ClassWithALongName.Constant2,
ClassWithALongName.Constant3,
)
):
return True
return False