From 1a499b2e247d5a132de943636bbaab621745a60b Mon Sep 17 00:00:00 2001 From: jpy-git Date: Tue, 15 Mar 2022 17:14:17 +0000 Subject: [PATCH 01/16] Remove unnecessary parentheses from WITH statements --- CHANGES.md | 1 + src/black/linegen.py | 22 +++++++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index edca0dcdad4..937e4afbd1d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -89,6 +89,7 @@ and the first release covered by our new - Treat blank lines in stubs the same inside top-level `if` statements (#2820) - Fix unstable formatting with semicolons and arithmetic expressions (#2817) - Fix unstable formatting around magic trailing comma (#2572) +- Remove unnecessary parentheses from `with` statements (#2926) ### Parser diff --git a/src/black/linegen.py b/src/black/linegen.py index 4dc242a1dfe..f188044291a 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -314,7 +314,7 @@ def __post_init__(self) -> None: v, keywords={"try", "except", "else", "finally"}, parens=Ø ) self.visit_except_clause = partial(v, keywords={"except"}, parens=Ø) - self.visit_with_stmt = partial(v, keywords={"with"}, parens=Ø) + self.visit_with_stmt = partial(v, keywords={"with"}, parens={"with"}) self.visit_funcdef = partial(v, keywords={"def"}, parens=Ø) self.visit_classdef = partial(v, keywords={"class"}, parens=Ø) self.visit_expr_stmt = partial(v, keywords=Ø, parens=ASSIGNMENTS) @@ -834,6 +834,15 @@ def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None: if child.type == syms.atom: if maybe_make_parens_invisible_in_atom(child, parent=node): wrap_in_parentheses(node, child, visible=False) + elif ( + isinstance(child, Node) + and child.type == syms.asexpr_test + and not any(leaf.type == token.COLONEQUAL for leaf in child.leaves()) + ): + # make parentheses invisible, + # unless the asexpr contains an assignment expression. + if maybe_make_parens_invisible_in_atom(child.children[0], parent=child): + wrap_in_parentheses(child, child.children[0], visible=False) elif is_one_tuple(child): wrap_in_parentheses(node, child, visible=True) elif node.type == syms.import_from: @@ -853,7 +862,11 @@ def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None: elif not (isinstance(child, Leaf) and is_multiline_string(child)): wrap_in_parentheses(node, child, visible=False) - check_lpar = isinstance(child, Leaf) and child.value in parens_after + check_lpar = ( + isinstance(child, Leaf) + and child.value in parens_after + or child.type == token.COMMA + ) def maybe_make_parens_invisible_in_atom(node: LN, parent: LN) -> bool: @@ -870,7 +883,10 @@ def maybe_make_parens_invisible_in_atom(node: LN, parent: LN) -> bool: or is_empty_tuple(node) or is_one_tuple(node) or (is_yield(node) and parent.type != syms.expr_stmt) - or max_delimiter_priority_in_atom(node) >= COMMA_PRIORITY + or ( + max_delimiter_priority_in_atom(node) >= COMMA_PRIORITY + and parent.type != syms.with_stmt + ) ): return False From 1a2c13b5a273ca13f52af13b3cf01aca5cbbce4a Mon Sep 17 00:00:00 2001 From: jpy-git Date: Tue, 15 Mar 2022 17:58:58 +0000 Subject: [PATCH 02/16] Add unit tests --- tests/data/parenthesized_context_managers.py | 23 ++++++ tests/data/with_brackets.py | 79 ++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 tests/data/with_brackets.py diff --git a/tests/data/parenthesized_context_managers.py b/tests/data/parenthesized_context_managers.py index ccf1f94883e..97325252012 100644 --- a/tests/data/parenthesized_context_managers.py +++ b/tests/data/parenthesized_context_managers.py @@ -19,3 +19,26 @@ CtxManager3() as example3, ): ... + +# output +with CtxManager() as example: + ... + +with CtxManager1(), CtxManager2(): + ... + +with CtxManager1() as example, CtxManager2(): + ... + +with CtxManager1(), CtxManager2() as example: + ... + +with CtxManager1() as example1, CtxManager2() as example2: + ... + +with ( + CtxManager1() as example1, + CtxManager2() as example2, + CtxManager3() as example3, +): + ... diff --git a/tests/data/with_brackets.py b/tests/data/with_brackets.py new file mode 100644 index 00000000000..56538ea38a8 --- /dev/null +++ b/tests/data/with_brackets.py @@ -0,0 +1,79 @@ +with (open("bla.txt")): + pass + +with (open("bla.txt")), (open("bla.txt")): + pass + +with (open("bla.txt") as f): + pass + +# Remove brackets within alias expression +with (open("bla.txt")) as f: + pass + +# Remove brackets around one-line context managers +with (open("bla.txt") as f, (open("x"))): + pass + +with ((open("bla.txt")) as f, open("x")): + pass + +with (CtxManager1() as example1, CtxManager2() as example2): + ... + +# Brackets remain when using magic comma +with (CtxManager1() as example1, CtxManager2() as example2,): + ... + +# Brackets remain for multi-line context managers +with (CtxManager1() as example1, CtxManager2() as example2, CtxManager2() as example2, CtxManager2() as example2, CtxManager2() as example2): + ... + +# Don't touch assignment expressions +with (y := open("./test.py")) as f: + pass + +# output +with open("bla.txt"): + pass + +with open("bla.txt"), open("bla.txt"): + pass + +with open("bla.txt") as f: + pass + +# Remove brackets within alias expression +with open("bla.txt") as f: + pass + +# Remove brackets around one-line context managers +with open("bla.txt") as f, open("x"): + pass + +with open("bla.txt") as f, open("x"): + pass + +with CtxManager1() as example1, CtxManager2() as example2: + ... + +# Brackets remain when using magic comma +with ( + CtxManager1() as example1, + CtxManager2() as example2, +): + ... + +# Brackets remain for multi-line context managers +with ( + CtxManager1() as example1, + CtxManager2() as example2, + CtxManager2() as example2, + CtxManager2() as example2, + CtxManager2() as example2, +): + ... + +# Don't touch assignment expressions +with (y := open("./test.py")) as f: + pass From 27a4bfda8a4b94789d91c3f085294306b53e846a Mon Sep 17 00:00:00 2001 From: jpy-git Date: Tue, 15 Mar 2022 19:15:09 +0000 Subject: [PATCH 03/16] Move functionality to preview mode --- CHANGES.md | 3 +- src/black/linegen.py | 88 +++++++++++++++---- src/black/mode.py | 1 + tests/data/parenthesized_context_managers.py | 23 ----- ...th_brackets.py => remove_with_brackets.py} | 0 tests/test_format.py | 1 + 6 files changed, 72 insertions(+), 44 deletions(-) rename tests/data/{with_brackets.py => remove_with_brackets.py} (100%) diff --git a/CHANGES.md b/CHANGES.md index 937e4afbd1d..c9dd39f2862 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,7 +12,7 @@ ### Preview style - +- Remove unnecessary parentheses from `with` statements (#2926) ### _Blackd_ @@ -89,7 +89,6 @@ and the first release covered by our new - Treat blank lines in stubs the same inside top-level `if` statements (#2820) - Fix unstable formatting with semicolons and arithmetic expressions (#2817) - Fix unstable formatting around magic trailing comma (#2572) -- Remove unnecessary parentheses from `with` statements (#2926) ### Parser diff --git a/src/black/linegen.py b/src/black/linegen.py index f188044291a..4246bd6707d 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -132,7 +132,10 @@ def visit_stmt( `parens` holds a set of string leaf values immediately after which invisible parens should be put. """ - normalize_invisible_parens(node, parens_after=parens) + remove_with_parens = Preview.remove_with_parens in self.mode + normalize_invisible_parens( + node, parens_after=parens, remove_with_parens=remove_with_parens + ) for child in node.children: if is_name_token(child) and child.value in keywords: yield from self.line() @@ -141,7 +144,12 @@ def visit_stmt( def visit_match_case(self, node: Node) -> Iterator[Line]: """Visit either a match or case statement.""" - normalize_invisible_parens(node, parens_after=set()) + remove_with_parens = Preview.remove_with_parens in self.mode + normalize_invisible_parens( + node, + parens_after=set(), + remove_with_parens=remove_with_parens, + ) yield from self.line() for child in node.children: @@ -314,7 +322,10 @@ def __post_init__(self) -> None: v, keywords={"try", "except", "else", "finally"}, parens=Ø ) self.visit_except_clause = partial(v, keywords={"except"}, parens=Ø) - self.visit_with_stmt = partial(v, keywords={"with"}, parens={"with"}) + if Preview.remove_with_parens in self.mode: + self.visit_with_stmt = partial(v, keywords={"with"}, parens={"with"}) + else: + self.visit_with_stmt = partial(v, keywords={"with"}, parens=Ø) self.visit_funcdef = partial(v, keywords={"def"}, parens=Ø) self.visit_classdef = partial(v, keywords={"class"}, parens=Ø) self.visit_expr_stmt = partial(v, keywords=Ø, parens=ASSIGNMENTS) @@ -802,7 +813,11 @@ def normalize_prefix(leaf: Leaf, *, inside_brackets: bool) -> None: leaf.prefix = "" -def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None: +def normalize_invisible_parens( + node: Node, + parens_after: Set[str], + remove_with_parens: bool = False, +) -> None: """Make existing optional parentheses invisible or create new ones. `parens_after` is a set of string leaf values immediately after which parens @@ -810,6 +825,9 @@ def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None: Standardizes on visible parentheses for single-element tuples, and keeps existing visible parentheses for other tuples and generator expressions. + + `remove_with_parens` enables the preview feature for removing redundant + parentheses from `with` statements. """ for pc in list_comments(node.prefix, is_endmarker=False): if pc.value in FMT_OFF: @@ -820,7 +838,11 @@ def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None: # Fixes a bug where invisible parens are not properly stripped from # assignment statements that contain type annotations. if isinstance(child, Node) and child.type == syms.annassign: - normalize_invisible_parens(child, parens_after=parens_after) + normalize_invisible_parens( + child, + parens_after=parens_after, + remove_with_parens=remove_with_parens, + ) # Add parentheses around long tuple unpacking in assignments. if ( @@ -832,16 +854,25 @@ def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None: if check_lpar: if child.type == syms.atom: - if maybe_make_parens_invisible_in_atom(child, parent=node): + if maybe_make_parens_invisible_in_atom( + child, + parent=node, + remove_with_parens=remove_with_parens, + ): wrap_in_parentheses(node, child, visible=False) elif ( - isinstance(child, Node) + remove_with_parens + and isinstance(child, Node) and child.type == syms.asexpr_test and not any(leaf.type == token.COLONEQUAL for leaf in child.leaves()) ): # make parentheses invisible, # unless the asexpr contains an assignment expression. - if maybe_make_parens_invisible_in_atom(child.children[0], parent=child): + if maybe_make_parens_invisible_in_atom( + child.children[0], + parent=child, + remove_with_parens=remove_with_parens, + ): wrap_in_parentheses(child, child.children[0], visible=False) elif is_one_tuple(child): wrap_in_parentheses(node, child, visible=True) @@ -862,31 +893,46 @@ def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None: elif not (isinstance(child, Leaf) and is_multiline_string(child)): wrap_in_parentheses(node, child, visible=False) - check_lpar = ( - isinstance(child, Leaf) - and child.value in parens_after - or child.type == token.COMMA - ) + if remove_with_parens: + check_lpar = ( + isinstance(child, Leaf) + and child.value in parens_after + or child.type == token.COMMA + ) + else: + check_lpar = isinstance(child, Leaf) and child.value in parens_after -def maybe_make_parens_invisible_in_atom(node: LN, parent: LN) -> bool: +def maybe_make_parens_invisible_in_atom( + node: LN, + parent: LN, + remove_with_parens: 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` as they are redundant. Returns whether the node should itself be wrapped in invisible parentheses. + `remove_with_parens` enables the preview feature for removing redundant + parentheses from `with` statements. """ + if remove_with_parens: + max_delimiter_priority_condition = ( + max_delimiter_priority_in_atom(node) >= COMMA_PRIORITY + and parent.type != syms.with_stmt + ) + else: + max_delimiter_priority_condition = ( + max_delimiter_priority_in_atom(node) >= COMMA_PRIORITY + ) if ( node.type != syms.atom or is_empty_tuple(node) or is_one_tuple(node) or (is_yield(node) and parent.type != syms.expr_stmt) - or ( - max_delimiter_priority_in_atom(node) >= COMMA_PRIORITY - and parent.type != syms.with_stmt - ) + or max_delimiter_priority_condition ): return False @@ -909,7 +955,11 @@ def maybe_make_parens_invisible_in_atom(node: LN, parent: LN) -> bool: # make parentheses invisible first.value = "" last.value = "" - maybe_make_parens_invisible_in_atom(middle, parent=parent) + maybe_make_parens_invisible_in_atom( + middle, + parent=parent, + remove_with_parens=remove_with_parens, + ) if is_atom_with_invisible_parens(middle): # Strip the invisible parens from `middle` by replacing diff --git a/src/black/mode.py b/src/black/mode.py index 455ed36e27e..e662b0876b5 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -128,6 +128,7 @@ class Preview(Enum): string_processing = auto() hug_simple_powers = auto() + remove_with_parens = auto() class Deprecated(UserWarning): diff --git a/tests/data/parenthesized_context_managers.py b/tests/data/parenthesized_context_managers.py index 97325252012..ccf1f94883e 100644 --- a/tests/data/parenthesized_context_managers.py +++ b/tests/data/parenthesized_context_managers.py @@ -19,26 +19,3 @@ CtxManager3() as example3, ): ... - -# output -with CtxManager() as example: - ... - -with CtxManager1(), CtxManager2(): - ... - -with CtxManager1() as example, CtxManager2(): - ... - -with CtxManager1(), CtxManager2() as example: - ... - -with CtxManager1() as example1, CtxManager2() as example2: - ... - -with ( - CtxManager1() as example1, - CtxManager2() as example2, - CtxManager3() as example3, -): - ... diff --git a/tests/data/with_brackets.py b/tests/data/remove_with_brackets.py similarity index 100% rename from tests/data/with_brackets.py rename to tests/data/remove_with_brackets.py diff --git a/tests/test_format.py b/tests/test_format.py index 04eda43d5cf..7c2e6b0e1ab 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -79,6 +79,7 @@ "long_strings__edge_case", "long_strings__regression", "percent_precedence", + "remove_with_brackets", ] SOURCES: List[str] = [ From 240a6f8c0b848ba068eea94f8f5c9693cc442709 Mon Sep 17 00:00:00 2001 From: jpy-git Date: Wed, 16 Mar 2022 10:48:49 +0000 Subject: [PATCH 04/16] Make logic more DRY --- src/black/linegen.py | 25 +++++++------------------ src/black/mode.py | 4 +--- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/src/black/linegen.py b/src/black/linegen.py index 4246bd6707d..c4ab293fc28 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -893,14 +893,11 @@ def normalize_invisible_parens( elif not (isinstance(child, Leaf) and is_multiline_string(child)): wrap_in_parentheses(node, child, visible=False) - if remove_with_parens: - check_lpar = ( - isinstance(child, Leaf) - and child.value in parens_after - or child.type == token.COMMA - ) - else: - check_lpar = isinstance(child, Leaf) and child.value in parens_after + comma_check = child.type == token.COMMA if remove_with_parens else True + + check_lpar = ( + isinstance(child, Leaf) and child.value in parens_after and comma_check + ) def maybe_make_parens_invisible_in_atom( @@ -917,22 +914,14 @@ def maybe_make_parens_invisible_in_atom( `remove_with_parens` enables the preview feature for removing redundant parentheses from `with` statements. """ - if remove_with_parens: - max_delimiter_priority_condition = ( - max_delimiter_priority_in_atom(node) >= COMMA_PRIORITY - and parent.type != syms.with_stmt - ) - else: - max_delimiter_priority_condition = ( - max_delimiter_priority_in_atom(node) >= COMMA_PRIORITY - ) + with_stmt_check = parent.type != syms.with_stmt if remove_with_parens else True if ( node.type != syms.atom or is_empty_tuple(node) or is_one_tuple(node) or (is_yield(node) and parent.type != syms.expr_stmt) - or max_delimiter_priority_condition + or (max_delimiter_priority_in_atom(node) >= COMMA_PRIORITY and with_stmt_check) ): return False diff --git a/src/black/mode.py b/src/black/mode.py index 7b641396905..229bb3d92bc 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -163,9 +163,7 @@ def __contains__(self, feature: Preview) -> bool: """ if feature is Preview.string_processing: return self.preview or self.experimental_string_processing - # TODO: Remove type ignore comment once preview contains more features - # than just ESP - return self.preview # type: ignore + return self.preview def get_cache_key(self) -> str: if self.target_versions: From cd0695d6851e7cba1166dae716e592a7b71f401d Mon Sep 17 00:00:00 2001 From: jpy-git Date: Wed, 16 Mar 2022 10:57:39 +0000 Subject: [PATCH 05/16] Use or instead of and --- 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 c4ab293fc28..f2ee75fafd3 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -893,10 +893,10 @@ def normalize_invisible_parens( elif not (isinstance(child, Leaf) and is_multiline_string(child)): wrap_in_parentheses(node, child, visible=False) - comma_check = child.type == token.COMMA if remove_with_parens else True + comma_check = child.type == token.COMMA if remove_with_parens else False check_lpar = ( - isinstance(child, Leaf) and child.value in parens_after and comma_check + isinstance(child, Leaf) and child.value in parens_after or comma_check ) From 0e0d9f803236e11a92f0a167a144f20a0b89e6dc Mon Sep 17 00:00:00 2001 From: jpy-git Date: Thu, 17 Mar 2022 09:25:11 +0000 Subject: [PATCH 06/16] Make unit test py>=3.9 --- tests/test_format.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/test_format.py b/tests/test_format.py index f98e73a0a07..e75590aacf1 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -79,7 +79,6 @@ "long_strings__edge_case", "long_strings__regression", "percent_precedence", - "remove_with_brackets", ] SOURCES: List[str] = [ @@ -189,6 +188,16 @@ def test_pep_570() -> None: assert_format(source, expected, minimum_version=(3, 8)) +def test_remove_with_brackets() -> None: + source, expected = read_data("remove_with_brackets") + assert_format( + source, + expected, + black.Mode(preview=True), + minimum_version=(3, 9), + ) + + @pytest.mark.parametrize("filename", PY310_CASES) def test_python_310(filename: str) -> None: source, expected = read_data(filename) From 3c14ab24f0b1542976b92139d4a3b18a338ad35a Mon Sep 17 00:00:00 2001 From: jpy-git Date: Fri, 18 Mar 2022 14:54:57 +0000 Subject: [PATCH 07/16] re-add preview style comment in CHANGES.md --- CHANGES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 4046234ca48..23b37430651 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,8 @@ ### Preview style + + - Remove unnecessary parentheses from `with` statements (#2926) ### _Blackd_ From d5db697df8f1cdc397c6ba2fbe790f5f4d987128 Mon Sep 17 00:00:00 2001 From: jpy-git Date: Mon, 21 Mar 2022 23:47:13 +0000 Subject: [PATCH 08/16] Update for latest master --- src/black/linegen.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/black/linegen.py b/src/black/linegen.py index 89981c51458..284719282aa 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -141,7 +141,6 @@ def visit_stmt( def visit_match_case(self, node: Node) -> Iterator[Line]: """Visit either a match or case statement.""" - remove_with_parens = Preview.remove_with_parens in self.mode normalize_invisible_parens(node, parens_after=set(), preview=self.mode.preview) yield from self.line() @@ -315,7 +314,7 @@ def __post_init__(self) -> None: v, keywords={"try", "except", "else", "finally"}, parens=Ø ) self.visit_except_clause = partial(v, keywords={"except"}, parens=Ø) - if Preview.remove_with_parens in self.mode: + if self.mode.preview: self.visit_with_stmt = partial(v, keywords={"with"}, parens={"with"}) else: self.visit_with_stmt = partial(v, keywords={"with"}, parens=Ø) @@ -816,9 +815,6 @@ def normalize_invisible_parens( Standardizes on visible parentheses for single-element tuples, and keeps existing visible parentheses for other tuples and generator expressions. - - `remove_with_parens` enables the preview feature for removing redundant - parentheses from `with` statements. """ for pc in list_comments(node.prefix, is_endmarker=False, preview=preview): if pc.value in FMT_OFF: @@ -846,11 +842,11 @@ def normalize_invisible_parens( if maybe_make_parens_invisible_in_atom( child, parent=node, - remove_with_parens=remove_with_parens, + preview=preview, ): wrap_in_parentheses(node, child, visible=False) elif ( - remove_with_parens + preview and isinstance(child, Node) and child.type == syms.asexpr_test and not any(leaf.type == token.COLONEQUAL for leaf in child.leaves()) @@ -860,7 +856,7 @@ def normalize_invisible_parens( if maybe_make_parens_invisible_in_atom( child.children[0], parent=child, - remove_with_parens=remove_with_parens, + preview=preview, ): wrap_in_parentheses(child, child.children[0], visible=False) elif is_one_tuple(child): @@ -882,7 +878,7 @@ def normalize_invisible_parens( elif not (isinstance(child, Leaf) and is_multiline_string(child)): wrap_in_parentheses(node, child, visible=False) - comma_check = child.type == token.COMMA if remove_with_parens else False + comma_check = child.type == token.COMMA if preview else False check_lpar = ( isinstance(child, Leaf) and child.value in parens_after or comma_check @@ -892,7 +888,7 @@ def normalize_invisible_parens( def maybe_make_parens_invisible_in_atom( node: LN, parent: LN, - remove_with_parens: bool = False, + preview: 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` @@ -900,10 +896,9 @@ def maybe_make_parens_invisible_in_atom( Returns whether the node should itself be wrapped in invisible parentheses. - `remove_with_parens` enables the preview feature for removing redundant - parentheses from `with` statements. + `preview` enables the preview feature for removing redundant parentheses. """ - with_stmt_check = parent.type != syms.with_stmt if remove_with_parens else True + with_stmt_check = parent.type != syms.with_stmt if preview else True if ( node.type != syms.atom @@ -936,7 +931,7 @@ def maybe_make_parens_invisible_in_atom( maybe_make_parens_invisible_in_atom( middle, parent=parent, - remove_with_parens=remove_with_parens, + preview=preview, ) if is_atom_with_invisible_parens(middle): From 87aa74d74e033a354eb7a54afb89eef6eb3d03b2 Mon Sep 17 00:00:00 2001 From: jpy-git Date: Mon, 21 Mar 2022 23:56:11 +0000 Subject: [PATCH 09/16] make redundant parens enum entry consistent --- src/black/mode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/black/mode.py b/src/black/mode.py index 229bb3d92bc..3c3d94de186 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -127,7 +127,7 @@ class Preview(Enum): """Individual preview style features.""" string_processing = auto() - remove_with_parens = auto() + remove_redundant_parens = auto() class Deprecated(UserWarning): From aaa902a15fc3cac9a83cd4a25eb78d902c63c399 Mon Sep 17 00:00:00 2001 From: jpy-git Date: Wed, 23 Mar 2022 19:59:39 +0000 Subject: [PATCH 10/16] add brackets for clarity --- 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 284719282aa..de8e87edb81 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -880,8 +880,8 @@ def normalize_invisible_parens( comma_check = child.type == token.COMMA if preview else False - check_lpar = ( - isinstance(child, Leaf) and child.value in parens_after or comma_check + check_lpar = isinstance(child, Leaf) and ( + child.value in parens_after or comma_check ) From 3f8db3dc6ab98d03ddf5fb360da8b3f4afc13e35 Mon Sep 17 00:00:00 2001 From: jpy-git Date: Thu, 24 Mar 2022 23:50:09 +0000 Subject: [PATCH 11/16] Fix condition after merge --- src/black/linegen.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/black/linegen.py b/src/black/linegen.py index cbf6f8afaf6..6cadeb6a128 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -920,8 +920,11 @@ def maybe_make_parens_invisible_in_atom( or is_empty_tuple(node) or is_one_tuple(node) or (is_yield(node) and parent.type != syms.expr_stmt) - or (max_delimiter_priority_in_atom(node) >= COMMA_PRIORITY and with_stmt_check) - or (max_delimiter_priority_in_atom(node) >= COMMA_PRIORITY and for_stmt_check) + or ( + max_delimiter_priority_in_atom(node) >= COMMA_PRIORITY + and with_stmt_check + and for_stmt_check + ) ): return False From ac698f8e513ee1de2418e2e22c0161fba77fb2a1 Mon Sep 17 00:00:00 2001 From: jpy-git Date: Fri, 25 Mar 2022 09:20:09 +0000 Subject: [PATCH 12/16] One-pass fix for removing brackets in for/with --- src/black/linegen.py | 85 +++++++++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 33 deletions(-) diff --git a/src/black/linegen.py b/src/black/linegen.py index 6cadeb6a128..69e2aee3ea3 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -843,27 +843,28 @@ def normalize_invisible_parens( check_lpar = True if check_lpar: - if child.type == syms.atom: + if ( + preview + and child.type == syms.atom + and node.type == syms.for_stmt + and isinstance(child.prev_sibling, Leaf) + and child.prev_sibling.type == token.NAME + and child.prev_sibling.value == "for" + ): if maybe_make_parens_invisible_in_atom( child, parent=node, - preview=preview, + remove_brackets_around_comma=True, ): wrap_in_parentheses(node, child, visible=False) - elif ( - preview - and isinstance(child, Node) - and child.type == syms.asexpr_test - and not any(leaf.type == token.COLONEQUAL for leaf in child.leaves()) - ): - # make parentheses invisible, - # unless the asexpr contains an assignment expression. + elif preview and isinstance(child, Node) and node.type == syms.with_stmt: + remove_with_parens(child, node) + elif child.type == syms.atom: if maybe_make_parens_invisible_in_atom( - child.children[0], - parent=child, - preview=preview, + child, + parent=node, ): - wrap_in_parentheses(child, child.children[0], visible=False) + wrap_in_parentheses(node, child, visible=False) elif is_one_tuple(child): wrap_in_parentheses(node, child, visible=True) elif node.type == syms.import_from: @@ -890,40 +891,54 @@ def normalize_invisible_parens( ) +def remove_with_parens(node: Node, parent: Node) -> None: + """Recursively hide optional parens in `with` statements.""" + if node.type == syms.atom: + if maybe_make_parens_invisible_in_atom( + node, + parent=parent, + remove_brackets_around_comma=True, + ): + wrap_in_parentheses(parent, node, visible=False) + if isinstance(node.children[1], Node): + remove_with_parens(node.children[1], node) + elif node.type == syms.testlist_gexp: + for child in node.children: + if isinstance(child, Node): + remove_with_parens(child, node) + elif node.type == syms.asexpr_test and not any( + leaf.type == token.COLONEQUAL for leaf in node.leaves() + ): + if maybe_make_parens_invisible_in_atom( + node.children[0], + parent=parent, + remove_brackets_around_comma=True, + ): + wrap_in_parentheses(parent, node.children[0], visible=False) + + def maybe_make_parens_invisible_in_atom( node: LN, parent: LN, - preview: bool = False, + remove_brackets_around_comma: 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` as they are redundant. Returns whether the node should itself be wrapped in invisible parentheses. - - `preview` enables the preview feature for removing redundant parentheses. """ - with_stmt_check = parent.type != syms.with_stmt if preview else True - if ( - preview - and parent.type == syms.for_stmt - and isinstance(node.prev_sibling, Leaf) - and node.prev_sibling.type == token.NAME - and node.prev_sibling.value == "for" - ): - for_stmt_check = False - else: - for_stmt_check = True - if ( node.type != syms.atom or is_empty_tuple(node) or is_one_tuple(node) or (is_yield(node) and parent.type != syms.expr_stmt) or ( - max_delimiter_priority_in_atom(node) >= COMMA_PRIORITY - and with_stmt_check - and for_stmt_check + # This condition tries to prevent removing non-optional brackets + # around a tuple, however, can be a bit overzealous so we provide + # and option to skip this check for `for` and `with` statements. + not remove_brackets_around_comma + and max_delimiter_priority_in_atom(node) >= COMMA_PRIORITY ) ): return False @@ -947,7 +962,11 @@ def maybe_make_parens_invisible_in_atom( # make parentheses invisible first.value = "" last.value = "" - maybe_make_parens_invisible_in_atom(middle, parent=parent, preview=preview) + maybe_make_parens_invisible_in_atom( + middle, + parent=parent, + remove_brackets_around_comma=remove_brackets_around_comma, + ) if is_atom_with_invisible_parens(middle): # Strip the invisible parens from `middle` by replacing From e61783a46bcc76b41b3a134a8744168aa0be9fab Mon Sep 17 00:00:00 2001 From: jpy-git Date: Sat, 26 Mar 2022 18:01:22 +0000 Subject: [PATCH 13/16] fix parent in as_expr logic --- 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 6d49164e677..f1db761bc57 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -914,10 +914,10 @@ def remove_with_parens(node: Node, parent: Node) -> None: ): if maybe_make_parens_invisible_in_atom( node.children[0], - parent=parent, + parent=node, remove_brackets_around_comma=True, ): - wrap_in_parentheses(parent, node.children[0], visible=False) + wrap_in_parentheses(node, node.children[0], visible=False) def maybe_make_parens_invisible_in_atom( From 1540bfad73119c880a6fa6470ae3355c455fbbfa Mon Sep 17 00:00:00 2001 From: jpy-git Date: Sat, 2 Apr 2022 21:21:45 +0100 Subject: [PATCH 14/16] Add deeply nested with bracket examples --- tests/data/remove_with_brackets.py | 40 ++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/data/remove_with_brackets.py b/tests/data/remove_with_brackets.py index 56538ea38a8..ea58ab93a16 100644 --- a/tests/data/remove_with_brackets.py +++ b/tests/data/remove_with_brackets.py @@ -33,6 +33,26 @@ with (y := open("./test.py")) as f: pass +# Deeply nested examples +# N.B. Multiple brackets are only possible +# around the context manager itself. +# Only one brackets is allowed around the +# alias expression or comma-delimited context managers. +with (((open("bla.txt")))): + pass + +with (((open("bla.txt")))), (((open("bla.txt")))): + pass + +with (((open("bla.txt")))) as f: + pass + +with ((((open("bla.txt")))) as f): + pass + +with ((((CtxManager1()))) as example1, (((CtxManager2()))) as example2): + ... + # output with open("bla.txt"): pass @@ -77,3 +97,23 @@ # Don't touch assignment expressions with (y := open("./test.py")) as f: pass + +# Deeply nested examples +# N.B. Multiple brackets are only possible +# around the context manager itself. +# Only one brackets is allowed around the +# alias expression or comma-delimited context managers. +with open("bla.txt"): + pass + +with open("bla.txt"), open("bla.txt"): + pass + +with open("bla.txt") as f: + pass + +with open("bla.txt") as f: + pass + +with CtxManager1() as example1, CtxManager2() as example2: + ... From 0f0a01a02ec7434e0fb6f4ae467b108cfa324ce6 Mon Sep 17 00:00:00 2001 From: jpy-git Date: Sat, 2 Apr 2022 21:30:02 +0100 Subject: [PATCH 15/16] Add deeply nested for bracket examples --- tests/data/remove_for_brackets.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/data/remove_for_brackets.py b/tests/data/remove_for_brackets.py index c8d88abcc50..cd5340462da 100644 --- a/tests/data/remove_for_brackets.py +++ b/tests/data/remove_for_brackets.py @@ -14,6 +14,10 @@ for (k, v) in dfkasdjfldsjflkdsjflkdsjfdslkfjldsjfgkjdshgkljjdsfldgkhsdofudsfudsofajdslkfjdslkfjldisfjdffjsdlkfjdlkjjkdflskadjldkfjsalkfjdasj.items(): print(k, v) +# Test deeply nested brackets +for (((((k, v))))) in d.items(): + print(k, v) + # output # Only remove tuple brackets after `for` for k, v in d.items(): @@ -38,3 +42,7 @@ dfkasdjfldsjflkdsjflkdsjfdslkfjldsjfgkjdshgkljjdsfldgkhsdofudsfudsofajdslkfjdslkfjldisfjdffjsdlkfjdlkjjkdflskadjldkfjsalkfjdasj.items() ): print(k, v) + +# Test deeply nested brackets +for k, v in d.items(): + print(k, v) From 99167c2c9b2e2c9e055f1ef406c8bd15643a51a3 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Sat, 2 Apr 2022 18:59:40 -0400 Subject: [PATCH 16/16] Moar code comments 'cause ASTs are no joke --- src/black/linegen.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/black/linegen.py b/src/black/linegen.py index f1db761bc57..2cf9cf3130a 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -896,6 +896,22 @@ def normalize_invisible_parens( def remove_with_parens(node: Node, parent: Node) -> None: """Recursively hide optional parens in `with` statements.""" + # Removing all unnecessary parentheses in with statements in one pass is a tad + # complex as different variations of bracketed statements result in pretty + # different parse trees: + # + # with (open("file")) as f: # this is an asexpr_test + # ... + # + # with (open("file") as f): # this is an atom containing an + # ... # asexpr_test + # + # with (open("file")) as f, (open("file")) as f: # this is asexpr_test, COMMA, + # ... # asexpr_test + # + # with (open("file") as f, open("file") as f): # an atom containing a + # ... # testlist_gexp which then + # # contains multiple asexpr_test(s) if node.type == syms.atom: if maybe_make_parens_invisible_in_atom( node,