Improve fmt:skip handling in nested expressions with checks (#4903)
* Fix #4730: honor fmt: skip in nested in-clause
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
diff --git a/CHANGES.md b/CHANGES.md
index 3924cfe..d9b50cb 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -13,6 +13,8 @@
<!-- Changes that affect Black's stable style -->
+- Fix `# fmt: skip` being ignored in nested `if` expressions with parenthesized `in`
+ clauses (#4903)
- Fix crash when an f-string follows a `# fmt: off` comment inside brackets (#5097)
- Add support for unpacking in comprehensions (PEP 798) and for lazy imports (PEP 810),
both new syntactic features in Python 3.15 (#5048)
diff --git a/src/black/comments.py b/src/black/comments.py
index b3dda1d..3487647 100644
--- a/src/black/comments.py
+++ b/src/black/comments.py
@@ -7,6 +7,7 @@
from black.mode import Mode
from black.nodes import (
CLOSING_BRACKETS,
+ OPENING_BRACKETS,
STANDALONE_COMMENT,
STATEMENT,
WHITESPACE,
@@ -448,6 +449,14 @@ def stringify_node(n: LN) -> str:
hidden_value = "".join(parts)
comment_lineno = leaf.lineno - comment.newlines
+ leaf_is_ignored = any(
+ ignored is leaf
+ or (
+ isinstance(ignored, Node)
+ and any(child is leaf for child in ignored.leaves())
+ )
+ for ignored in ignored_nodes
+ )
if contains_fmt_directive(comment.value, FMT_OFF):
fmt_off_prefix = ""
@@ -461,7 +470,7 @@ def stringify_node(n: LN) -> str:
standalone_comment_prefix += fmt_off_prefix
hidden_value = comment.value + "\n" + hidden_value
- if is_fmt_skip:
+ if is_fmt_skip and not leaf_is_ignored:
hidden_value += comment.leading_whitespace + comment.value
if hidden_value.endswith("\n"):
@@ -630,6 +639,17 @@ def _get_compound_statement_header(
return header_leaves
+def _find_closest_previous_sibling(node: LN) -> LN | None:
+ """Find the closest previous sibling by walking up the ancestor chain."""
+ current: LN | None = node
+ while current is not None:
+ prev_sibling = current.prev_sibling
+ if prev_sibling is not None:
+ return prev_sibling
+ current = current.parent
+ return None
+
+
def _generate_ignored_nodes_from_fmt_skip(
leaf: Leaf, comment: ProtoComment, mode: Mode
) -> Iterator[LN]:
@@ -643,12 +663,13 @@ def _generate_ignored_nodes_from_fmt_skip(
if not comments or comment.value != comments[0].value:
return
- if not prev_sibling and parent:
+ if prev_sibling is None and parent is not None:
prev_sibling = parent.prev_sibling
- if prev_sibling is not None:
- leaf.prefix = leaf.prefix[comment.consumed :]
+ if prev_sibling is None and comment.type == token.COMMENT:
+ prev_sibling = _find_closest_previous_sibling(leaf)
+ if prev_sibling is not None:
# Generates the nodes to be ignored by `fmt: skip`.
# Nodes to ignore are the ones on the same line as the
@@ -669,6 +690,14 @@ def _generate_ignored_nodes_from_fmt_skip(
# or NEWLINE leaves.
current_node = prev_sibling
+ if (
+ isinstance(current_node, Leaf)
+ and current_node.type in OPENING_BRACKETS
+ and current_node.parent
+ and current_node.parent.type == syms.atom
+ ):
+ current_node = current_node.parent
+
ignored_nodes = [current_node]
if current_node.prev_sibling is None and current_node.parent is not None:
current_node = current_node.parent
@@ -734,6 +763,17 @@ def _generate_ignored_nodes_from_fmt_skip(
if header_nodes:
ignored_nodes = header_nodes + ignored_nodes
+ leaf_is_ignored = any(
+ ignored is leaf
+ or (
+ isinstance(ignored, Node)
+ and any(child is leaf for child in ignored.leaves())
+ )
+ for ignored in ignored_nodes
+ )
+ if not leaf_is_ignored:
+ leaf.prefix = leaf.prefix[comment.consumed :]
+
yield from ignored_nodes
elif (
parent is not None and parent.type == syms.suite and leaf.type == token.NEWLINE
diff --git a/tests/data/cases/fmtskip_in_clause.py b/tests/data/cases/fmtskip_in_clause.py
new file mode 100644
index 0000000..fb79254
--- /dev/null
+++ b/tests/data/cases/fmtskip_in_clause.py
@@ -0,0 +1,43 @@
+# Single fmt: skip in multi-part if-clause
+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
+
+
+# Single fmt: skip in multi-part if-clause
+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