Skip to content

Commit 5809338

Browse files
Preserve inline comments inside annotation subscripts (#5130)
* Add regression tests for #4733 * Preserve inline comments inside annotation subscripts (#4733) * Move #4733 regression tests to tests/data/cases * Keep the omit-based RHS split past a subscript-comment overflow --------- Co-authored-by: cobalt <61329810+cobaltt7@users.noreply.github.com>
1 parent 61361b7 commit 5809338

3 files changed

Lines changed: 71 additions & 1 deletion

File tree

CHANGES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313

1414
<!-- Changes that affect Black's stable style -->
1515

16+
- Fix unstable formatting of annotated assignments whose subscript annotation contains
17+
an inline comment (e.g. `x: list[ # pyright: ignore[...]\n int\n] = []`). Black no
18+
longer migrates the comment outside the subscript brackets, eliminating the
19+
oscillation between formatter passes reported as #4733 (#5130)
20+
1621
### Preview style
1722

1823
<!-- Changes that affect Black's preview style -->

src/black/linegen.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
normalize_string_prefix,
8181
normalize_string_quotes,
8282
normalize_unicode_escape_sequences,
83+
str_width,
8384
)
8485
from black.trans import (
8586
CannotTransform,
@@ -770,7 +771,9 @@ def _rhs(
770771
# *current* transformation fits in the line length. This is true only
771772
# for simple cases. All others require running more transforms via
772773
# `transform_line()`. This check doesn't know if those would succeed.
773-
if is_line_short_enough(lines[0], mode=mode):
774+
if is_line_short_enough(lines[0], mode=mode) or (
775+
omit and _over_length_only_due_to_subscript_comment(lines[0], mode)
776+
):
774777
yield from lines
775778
return
776779

@@ -2013,6 +2016,37 @@ def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[set[Leaf
20132016
closing_bracket = leaf
20142017

20152018

2019+
def _over_length_only_due_to_subscript_comment(line: Line, mode: Mode) -> bool:
2020+
"""Return True if `line` only exceeds `mode.line_length` because of an inline
2021+
comment attached to a subscript opening bracket (`[`).
2022+
2023+
This is the shape produced by the original of the issue #4733 reproducer:
2024+
a comment inside the annotation's subscript brackets renders at the end of
2025+
the head line after Black splits the statement, pushing it past the limit.
2026+
Taking the FORCE_OPTIONAL_PARENTHESES "second opinion" in that case wraps
2027+
the annotation in extra parens and migrates the comment outside the
2028+
subscript, which then oscillates on the next formatter pass.
2029+
"""
2030+
if not line.leaves:
2031+
return False
2032+
# The over-length must be caused entirely by a trailing comment.
2033+
indent = " " * line.depth
2034+
leaves_iter = iter(line.leaves)
2035+
first = next(leaves_iter)
2036+
text_without_comments = f"{first.prefix}{indent}{first.value}"
2037+
text_without_comments += "".join(str(leaf) for leaf in leaves_iter)
2038+
if str_width(text_without_comments) > mode.line_length:
2039+
return False
2040+
# And the comment must be attached to a subscript opening bracket.
2041+
for leaf_id, comments in line.comments.items():
2042+
if not comments:
2043+
continue
2044+
leaf = next((lf for lf in line.leaves if id(lf) == leaf_id), None)
2045+
if leaf is None or leaf.type != token.LSQB:
2046+
return False
2047+
return True
2048+
2049+
20162050
def run_transformer(
20172051
line: Line,
20182052
transform: Transformer,
@@ -2040,6 +2074,12 @@ def run_transformer(
20402074
or result[0].contains_uncollapsable_type_comments()
20412075
or result[0].contains_unsplittable_type_ignore()
20422076
or is_line_short_enough(result[0], mode=mode)
2077+
# result[0] only exceeds the length because of a comment attached to a
2078+
# subscript opening bracket. Taking the FORCE_OPTIONAL_PARENTHESES
2079+
# "second opinion" wraps the annotation in extra invisible parens and
2080+
# migrates the comment outside the subscript, which then oscillates with
2081+
# a deeper-bracket split on the next formatter pass (issue #4733).
2082+
or _over_length_only_due_to_subscript_comment(result[0], mode)
20432083
# If any leaves have no parents (which _can_ occur since
20442084
# `transform(line)` potentially destroys the line's underlying node
20452085
# structure), then we can't proceed. Doing so would cause the below
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Regression test for https://github.com/psf/black/issues/4733.
2+
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: list[ # bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
3+
int
4+
] = []
5+
6+
7+
# Original (non-minimized) snippet from the issue report.
8+
def foo():
9+
possibly_redundant_lowlevel_checkpoints: list[ # pyright: ignore[reportUnknownVariableType]
10+
cst.BaseExpression
11+
] = field( default_factory=list)
12+
13+
# output
14+
15+
# Regression test for https://github.com/psf/black/issues/4733.
16+
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: list[ # bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
17+
int
18+
] = []
19+
20+
21+
# Original (non-minimized) snippet from the issue report.
22+
def foo():
23+
possibly_redundant_lowlevel_checkpoints: list[ # pyright: ignore[reportUnknownVariableType]
24+
cst.BaseExpression
25+
] = field(default_factory=list)

0 commit comments

Comments
 (0)