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
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

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

- Remove unnecessary parentheses from the left-hand side of assignments while preserving
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)

Expand Down
5 changes: 5 additions & 0 deletions docs/the_black_code_style/future_style.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ Currently, the following features are included in the preview style:
- `fix_module_docstring_detection`: Fix module docstrings being treated as normal
strings if preceeded by comments.
- `fix_type_expansion_split`: Fix type expansions split in generic functions.
- `remove_parens_from_assignment_lhs`: Remove unnecessary parentheses from the left-hand
side of assignments while preserving magic trailing commas and intentional multiline
formatting. For example, `(b) = a()[0]` becomes `b = a()[0]`, and `(c, *_) = a()`
becomes `c, *_ = a()`, but `(d,) = a()` is preserved as it defines a single-element
tuple.
- `multiline_string_handling`: more compact formatting of expressions involving
multiline strings ([see below](labels/multiline-string-handling))
- `fix_module_docstring_detection`: Fix module docstrings being treated as normal
Expand Down
50 changes: 49 additions & 1 deletion src/black/linegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -1524,6 +1524,26 @@ def normalize_invisible_parens( # noqa: C901
):
check_lpar = True

# Check for assignment LHS with preview feature enabled
if (
Preview.remove_parens_from_assignment_lhs in mode
and index == 0
and isinstance(child, Node)
and child.type == syms.atom
and node.type == syms.expr_stmt
and not _atom_has_magic_trailing_comma(child, mode)
and not _is_atom_multiline(child)
):
if maybe_make_parens_invisible_in_atom(
child,
parent=node,
mode=mode,
features=features,
remove_brackets_around_comma=True,
allow_star_expr=True,
):
wrap_in_parentheses(node, child, visible=False)

if check_lpar:
if (
child.type == syms.atom
Expand Down Expand Up @@ -1729,12 +1749,40 @@ def remove_with_parens(
wrap_in_parentheses(node, node.children[0], visible=False)


def _atom_has_magic_trailing_comma(node: LN, mode: Mode) -> bool:
"""Check if an atom node has a magic trailing comma.

Returns True for single-element tuples with trailing commas like (a,),
which should be preserved to maintain their tuple type.
"""
if not mode.magic_trailing_comma:
return False

return is_one_tuple(node)


def _is_atom_multiline(node: LN) -> bool:
"""Check if an atom node is multiline (indicating intentional formatting)."""
if not isinstance(node, Node) or len(node.children) < 3:
return False

# Check the middle child (between LPAR and RPAR) for newlines in its subtree
# The first child's prefix contains blank lines/comments before the opening paren
middle = node.children[1]
for child in middle.pre_order():
if isinstance(child, Leaf) and "\n" in child.prefix:
return True

return False


def maybe_make_parens_invisible_in_atom(
node: LN,
parent: LN,
mode: Mode,
features: Collection[Feature],
remove_brackets_around_comma: bool = False,
allow_star_expr: bool = False,
) -> bool:
"""If it's safe, make the parens in the atom `node` invisible, recursively.
Additionally, remove repeated, adjacent invisible parens from the atom `node`
Expand Down Expand Up @@ -1780,7 +1828,7 @@ def maybe_make_parens_invisible_in_atom(
)
)
or is_tuple_containing_walrus(node)
or is_tuple_containing_star(node)
or (not allow_star_expr and is_tuple_containing_star(node))
or is_generator(node)
):
return False
Expand Down
3 changes: 2 additions & 1 deletion src/black/mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,11 +233,12 @@ class Preview(Enum):
standardize_type_comments = auto()
wrap_comprehension_in = auto()
# Remove parentheses around multiple exception types in except and
# except* without as. See PEP758 for details.
# except* without as. See PEP 758 for details.
remove_parens_around_except_types = auto()
normalize_cr_newlines = auto()
fix_module_docstring_detection = auto()
fix_type_expansion_split = auto()
remove_parens_from_assignment_lhs = auto()


UNSTABLE_FEATURES: set[Preview] = {
Expand Down
3 changes: 2 additions & 1 deletion src/black/resources/black.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@
"remove_parens_around_except_types",
"normalize_cr_newlines",
"fix_module_docstring_detection",
"fix_type_expansion_split"
"fix_type_expansion_split",
"remove_parens_from_assignment_lhs"
]
},
"description": "Enable specific features included in the `--unstable` style. Requires `--preview`. No compatibility guarantees are provided on the behavior or existence of any unstable features."
Expand Down
4 changes: 2 additions & 2 deletions src/blib2to3/pgen2/tokenize.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,8 @@ def tokenize(source: str, grammar: Grammar | None = None) -> Iterator[TokenInfo]
def printtoken(
type: int, token: str, srow_col: Coord, erow_col: Coord, line: str
) -> None: # for testing
(srow, scol) = srow_col
(erow, ecol) = erow_col
srow, scol = srow_col
erow, ecol = erow_col
print(f"{srow},{scol}-{erow},{ecol}:\t{tok_name[type]}\t{token!r}")


Expand Down
36 changes: 36 additions & 0 deletions tests/data/cases/remove_parens_from_lhs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# flags: --preview
# Remove unnecessary parentheses from LHS of assignments


def a():
return [1, 2, 3]


# Single variable with unnecessary parentheses
(b) = a()[0]

# Tuple unpacking with unnecessary parentheses
(c, *_) = a()

# These should not be changed - parentheses are necessary
(d,) = a() # single-element tuple
e = (1 + 2) * 3 # RHS has precedence needs

# output

# Remove unnecessary parentheses from LHS of assignments


def a():
return [1, 2, 3]


# Single variable with unnecessary parentheses
b = a()[0]

# Tuple unpacking with unnecessary parentheses
c, *_ = a()

# These should not be changed - parentheses are necessary
(d,) = a() # single-element tuple
e = (1 + 2) * 3 # RHS has precedence needs