From 547285315600e5aefac3e9b97d9781cc125613dd Mon Sep 17 00:00:00 2001 From: Nikhil172913832 Date: Tue, 25 Nov 2025 03:02:48 +0530 Subject: [PATCH 1/5] Remove unnecessary parentheses from assignment LHS while preserving magic trailing commas --- src/black/linegen.py | 47 +++++++++++++++++++++- tests/data/cases/remove_parens_from_lhs.py | 35 ++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 tests/data/cases/remove_parens_from_lhs.py diff --git a/src/black/linegen.py b/src/black/linegen.py index 240a2f814e4..0d93ae523cf 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -1524,6 +1524,10 @@ def normalize_invisible_parens( # noqa: C901 ): check_lpar = True + if index == 0 and isinstance(child, Node) and child.type == syms.atom: + if node.type == syms.expr_stmt: + check_lpar = True + if check_lpar: if ( child.type == syms.atom @@ -1542,6 +1546,22 @@ def normalize_invisible_parens( # noqa: C901 wrap_in_parentheses(node, child, visible=False) elif isinstance(child, Node) and node.type == syms.with_stmt: remove_with_parens(child, node, mode=mode, features=features) + elif ( + child.type == syms.atom + and node.type == syms.expr_stmt + and index == 0 + 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) elif child.type == syms.atom and not ( "in" in parens_after and len(child.children) == 3 @@ -1729,12 +1749,37 @@ 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): + return False + + for child in node.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` @@ -1780,7 +1825,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 diff --git a/tests/data/cases/remove_parens_from_lhs.py b/tests/data/cases/remove_parens_from_lhs.py new file mode 100644 index 00000000000..a6fc653af13 --- /dev/null +++ b/tests/data/cases/remove_parens_from_lhs.py @@ -0,0 +1,35 @@ +# 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 From 3309ac6c902f7251376aea12c8a3b2e7e1326490 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 22:13:11 +0000 Subject: [PATCH 2/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/black/linegen.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/black/linegen.py b/src/black/linegen.py index 0d93ae523cf..db0325bb9d9 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -1751,13 +1751,13 @@ def remove_with_parens( 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) From 3baefb8f24e98308c56ed8367126cfffd491659a Mon Sep 17 00:00:00 2001 From: Nikhil172913832 Date: Tue, 25 Nov 2025 16:46:39 +0530 Subject: [PATCH 3/5] Add preview feature to remove unnecessary parentheses from assignment LHS --- CHANGES.md | 2 + docs/the_black_code_style/future_style.md | 5 +++ gallery/gallery.py | 4 +- src/black/linegen.py | 45 ++++++++++++---------- src/black/mode.py | 3 +- src/black/resources/black.schema.json | 3 +- src/blib2to3/pgen2/tokenize.py | 4 +- tests/data/cases/remove_parens_from_lhs.py | 5 ++- 8 files changed, 41 insertions(+), 30 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 76993ca3fe4..51f8ba85504 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -22,6 +22,8 @@ +- 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) diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 181a218d1a8..b43bf609ab5 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -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 diff --git a/gallery/gallery.py b/gallery/gallery.py index d7fb3f6441f..ae6a6a9d26f 100755 --- a/gallery/gallery.py +++ b/gallery/gallery.py @@ -103,9 +103,7 @@ def download_and_extract(package: str, version: str | None, directory: Path) -> return directory / result_dir -def get_package( - package: str, version: str | None, directory: Path -) -> Path | None: +def get_package(package: str, version: str | None, directory: Path) -> Path | None: try: return download_and_extract(package, version, directory) except Exception: diff --git a/src/black/linegen.py b/src/black/linegen.py index db0325bb9d9..cbff4aa2cac 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -1524,9 +1524,25 @@ def normalize_invisible_parens( # noqa: C901 ): check_lpar = True - if index == 0 and isinstance(child, Node) and child.type == syms.atom: - if node.type == syms.expr_stmt: - 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 ( @@ -1546,22 +1562,6 @@ def normalize_invisible_parens( # noqa: C901 wrap_in_parentheses(node, child, visible=False) elif isinstance(child, Node) and node.type == syms.with_stmt: remove_with_parens(child, node, mode=mode, features=features) - elif ( - child.type == syms.atom - and node.type == syms.expr_stmt - and index == 0 - 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) elif child.type == syms.atom and not ( "in" in parens_after and len(child.children) == 3 @@ -1763,10 +1763,13 @@ def _atom_has_magic_trailing_comma(node: LN, mode: Mode) -> bool: def _is_atom_multiline(node: LN) -> bool: """Check if an atom node is multiline (indicating intentional formatting).""" - if not isinstance(node, Node): + if not isinstance(node, Node) or len(node.children) < 3: return False - for child in node.pre_order(): + # 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 diff --git a/src/black/mode.py b/src/black/mode.py index c7be0466f0b..702f580e979 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -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 PEP 758 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] = { diff --git a/src/black/resources/black.schema.json b/src/black/resources/black.schema.json index bbb3cdfbb84..bed70a4bb22 100644 --- a/src/black/resources/black.schema.json +++ b/src/black/resources/black.schema.json @@ -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." diff --git a/src/blib2to3/pgen2/tokenize.py b/src/blib2to3/pgen2/tokenize.py index 18503973804..4e3761f3028 100644 --- a/src/blib2to3/pgen2/tokenize.py +++ b/src/blib2to3/pgen2/tokenize.py @@ -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}") diff --git a/tests/data/cases/remove_parens_from_lhs.py b/tests/data/cases/remove_parens_from_lhs.py index a6fc653af13..92d0731b87b 100644 --- a/tests/data/cases/remove_parens_from_lhs.py +++ b/tests/data/cases/remove_parens_from_lhs.py @@ -1,3 +1,4 @@ +# flags: --preview # Remove unnecessary parentheses from LHS of assignments @@ -6,10 +7,10 @@ def a(): # Single variable with unnecessary parentheses -b = a()[0] +(b) = a()[0] # Tuple unpacking with unnecessary parentheses -c, *_ = a() +(c, *_) = a() # These should not be changed - parentheses are necessary (d,) = a() # single-element tuple From a277d72c9e3aacb57b8128fee5970a09d235279a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:36:48 +0000 Subject: [PATCH 4/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- CHANGES.md | 4 ++-- docs/the_black_code_style/future_style.md | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 51f8ba85504..128d84b8ff3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -22,8 +22,8 @@ -- Remove unnecessary parentheses from the left-hand side of assignments while - preserving magic trailing commas and intentional multiline formatting (#4865) +- 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) diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index b43bf609ab5..07bc5258d92 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -31,11 +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. +- `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 From 356f9d21c21ab7762b2526e3bc8281defd3225af Mon Sep 17 00:00:00 2001 From: cobalt <61329810+cobaltt7@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:14:01 -0600 Subject: [PATCH 5/5] Apply suggestion from @cobaltt7 --- gallery/gallery.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gallery/gallery.py b/gallery/gallery.py index ae6a6a9d26f..d7fb3f6441f 100755 --- a/gallery/gallery.py +++ b/gallery/gallery.py @@ -103,7 +103,9 @@ def download_and_extract(package: str, version: str | None, directory: Path) -> return directory / result_dir -def get_package(package: str, version: str | None, directory: Path) -> Path | None: +def get_package( + package: str, version: str | None, directory: Path +) -> Path | None: try: return download_and_extract(package, version, directory) except Exception: