From 9a2de75c974c1a24aefc3c7d0b66e9a110d5a6a5 Mon Sep 17 00:00:00 2001 From: jpy-git Date: Mon, 4 Apr 2022 14:13:36 +0100 Subject: [PATCH 1/5] await brackets --- CHANGES.md | 1 + src/black/linegen.py | 31 ++++++++ tests/data/remove_await_parens.py | 122 ++++++++++++++++++++++++++++++ tests/test_format.py | 1 + 4 files changed, 155 insertions(+) create mode 100644 tests/data/remove_await_parens.py diff --git a/CHANGES.md b/CHANGES.md index 30c00566b3c..449c9fb6d85 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,7 @@ +- Parentheses around awaited coroutines/tasks are now managed (#2991) - Remove unnecessary parentheses from `with` statements (#2926) ### _Blackd_ diff --git a/src/black/linegen.py b/src/black/linegen.py index 2cf9cf3130a..1e25da3bd70 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -226,6 +226,37 @@ def visit_power(self, node: Node) -> Iterator[Line]: ): wrap_in_parentheses(node, leaf) + if ( + Preview.remove_redundant_parens in self.mode + and node.children[0].type == token.AWAIT + and len(node.children) > 1 + ): + if ( + node.children[1].type == syms.atom + and node.children[1].children[0].type == token.LPAR + ): + if maybe_make_parens_invisible_in_atom( + node.children[1], + parent=node, + remove_brackets_around_comma=True, + ): + wrap_in_parentheses(node, node.children[1], visible=False) + if ( + node.children[1].children[1].type != syms.power + and isinstance(node.children[1].children[0], Leaf) + and isinstance(node.children[1].children[-1], Leaf) + ): + # Since await is an expression + # it is syntactically possible + # that someone would write `await (1 % 2)` + # (although realistically unlikely given that it's a runtime error). + # If we don't encounter a power being wrapped then we + # should not remove all brackets due to operator precedence. + # https://peps.python.org/pep-0492/#updated-operator-precedence-table + # N.B. We've still removed any redundant nested brackets though :) + ensure_visible(node.children[1].children[0]) + ensure_visible(node.children[1].children[-1]) + yield from self.visit_default(node) def visit_SEMI(self, leaf: Leaf) -> Iterator[Line]: diff --git a/tests/data/remove_await_parens.py b/tests/data/remove_await_parens.py new file mode 100644 index 00000000000..a5cf09b4c36 --- /dev/null +++ b/tests/data/remove_await_parens.py @@ -0,0 +1,122 @@ +import asyncio + +# Control example +async def main(): + await asyncio.sleep(1) + +# Remove brackets for short coroutine/task +async def main(): + await (asyncio.sleep(1)) + +async def main(): + await ( + asyncio.sleep(1) + ) + +async def main(): + await (asyncio.sleep(1) + ) + +# Check comments +async def main(): + await ( # Hello + asyncio.sleep(1) + ) + +async def main(): + await ( + asyncio.sleep(1) # Hello + ) + +async def main(): + await ( + asyncio.sleep(1) + ) # Hello + +# Long lines +async def main(): + await asyncio.gather(asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1)) + +# Same as above but with magic trailing comma in function +async def main(): + await asyncio.gather(asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1),) + +# Cr@zY Br@ck3Tz +async def main(): + await ( + ((((((((((((( + ((( ((( + ((( ((( + ((( ((( + ((( ((( + ((black(1))) + ))) ))) + ))) ))) + ))) ))) + ))) ))) + ))))))))))))) + ) + +# output +import asyncio + +# Control example +async def main(): + await asyncio.sleep(1) + + +# Remove brackets for short coroutine/task +async def main(): + await asyncio.sleep(1) + + +async def main(): + await asyncio.sleep(1) + + +async def main(): + await asyncio.sleep(1) + + +# Check comments +async def main(): + await asyncio.sleep(1) # Hello + + +async def main(): + await asyncio.sleep(1) # Hello + + +async def main(): + await asyncio.sleep(1) # Hello + + +# Long lines +async def main(): + await asyncio.gather( + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + ) + + +# Same as above but with magic trailing comma in function +async def main(): + await asyncio.gather( + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + ) + + +# Cr@zY Br@ck3Tz +async def main(): + await black(1) diff --git a/tests/test_format.py b/tests/test_format.py index d80eaa730cd..b998a71a542 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -83,6 +83,7 @@ "remove_except_parens", "remove_for_brackets", "one_element_subscript", + "remove_await_parens", ] SOURCES: List[str] = [ From a11a49af091d3c42f58bae9bbdabe99c4d3ac6b4 Mon Sep 17 00:00:00 2001 From: Joe Young <80432516+jpy-git@users.noreply.github.com> Date: Mon, 4 Apr 2022 15:41:39 +0100 Subject: [PATCH 2/5] Update CHANGES.md Co-authored-by: Jelle Zijlstra --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 449c9fb6d85..cd54928cb96 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,7 +14,7 @@ -- Parentheses around awaited coroutines/tasks are now managed (#2991) +- Remove redundant parentheses around awaited objects (#2991) - Remove unnecessary parentheses from `with` statements (#2926) ### _Blackd_ From 6ec562797d45da655085d681356d99036b5ee145 Mon Sep 17 00:00:00 2001 From: jpy-git Date: Mon, 4 Apr 2022 17:15:50 +0100 Subject: [PATCH 3/5] Extra unit tests and nested await --- src/black/linegen.py | 22 ++++++++++++++-------- tests/data/remove_await_parens.py | 16 ++++++++++++++++ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/black/linegen.py b/src/black/linegen.py index 1e25da3bd70..4eeb6ca8e29 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -242,16 +242,22 @@ def visit_power(self, node: Node) -> Iterator[Line]: ): wrap_in_parentheses(node, node.children[1], visible=False) if ( - node.children[1].children[1].type != syms.power - and isinstance(node.children[1].children[0], Leaf) + isinstance(node.children[1].children[0], Leaf) and isinstance(node.children[1].children[-1], Leaf) + and ( + node.children[1].children[1].type != syms.power + or ( + node.children[1].children[1].type == syms.power + and node.children[1].children[1].children[0].type + == token.AWAIT + ) + ) ): - # Since await is an expression - # it is syntactically possible - # that someone would write `await (1 % 2)` - # (although realistically unlikely given that it's a runtime error). - # If we don't encounter a power being wrapped then we - # should not remove all brackets due to operator precedence. + # Since await is an expression we shouldn't remove + # brackets in cases where this would change + # the AST due to operator precedence. + # Therefore we only aim to remove brackets around + # power nodes that aren't also await expressions themselves. # https://peps.python.org/pep-0492/#updated-operator-precedence-table # N.B. We've still removed any redundant nested brackets though :) ensure_visible(node.children[1].children[0]) diff --git a/tests/data/remove_await_parens.py b/tests/data/remove_await_parens.py index a5cf09b4c36..dcf3973599d 100644 --- a/tests/data/remove_await_parens.py +++ b/tests/data/remove_await_parens.py @@ -57,6 +57,13 @@ async def main(): ))))))))))))) ) +# Keep brackets around non power operations and nested awaits +async def main(): + await (set_of_tasks | other_set) + +async def main(): + await (await asyncio.sleep(1)) + # output import asyncio @@ -120,3 +127,12 @@ async def main(): # Cr@zY Br@ck3Tz async def main(): await black(1) + + +# Keep brackets around non power operations and nested awaits +async def main(): + await (set_of_tasks | other_set) + + +async def main(): + await (await asyncio.sleep(1)) From 09da67790b88d16089f9f00f03f40b783db3df63 Mon Sep 17 00:00:00 2001 From: jpy-git Date: Mon, 4 Apr 2022 17:39:11 +0100 Subject: [PATCH 4/5] Use recursion to remove redundant brackets from nested awaits --- src/black/linegen.py | 76 ++++++++++++++++--------------- tests/data/remove_await_parens.py | 16 +++++++ 2 files changed, 55 insertions(+), 37 deletions(-) diff --git a/src/black/linegen.py b/src/black/linegen.py index 4eeb6ca8e29..76d2c976cf0 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -3,7 +3,7 @@ """ from functools import partial, wraps import sys -from typing import Collection, Iterator, List, Optional, Set, Union +from typing import Collection, Iterator, List, Optional, Set, Union, cast from black.nodes import WHITESPACE, RARROW, STATEMENT, STANDALONE_COMMENT from black.nodes import ASSIGNMENTS, OPENING_BRACKETS, CLOSING_BRACKETS @@ -226,42 +226,8 @@ def visit_power(self, node: Node) -> Iterator[Line]: ): wrap_in_parentheses(node, leaf) - if ( - Preview.remove_redundant_parens in self.mode - and node.children[0].type == token.AWAIT - and len(node.children) > 1 - ): - if ( - node.children[1].type == syms.atom - and node.children[1].children[0].type == token.LPAR - ): - if maybe_make_parens_invisible_in_atom( - node.children[1], - parent=node, - remove_brackets_around_comma=True, - ): - wrap_in_parentheses(node, node.children[1], visible=False) - if ( - isinstance(node.children[1].children[0], Leaf) - and isinstance(node.children[1].children[-1], Leaf) - and ( - node.children[1].children[1].type != syms.power - or ( - node.children[1].children[1].type == syms.power - and node.children[1].children[1].children[0].type - == token.AWAIT - ) - ) - ): - # Since await is an expression we shouldn't remove - # brackets in cases where this would change - # the AST due to operator precedence. - # Therefore we only aim to remove brackets around - # power nodes that aren't also await expressions themselves. - # https://peps.python.org/pep-0492/#updated-operator-precedence-table - # N.B. We've still removed any redundant nested brackets though :) - ensure_visible(node.children[1].children[0]) - ensure_visible(node.children[1].children[-1]) + if Preview.remove_redundant_parens in self.mode: + remove_await_parens(node) yield from self.visit_default(node) @@ -931,6 +897,42 @@ def normalize_invisible_parens( ) +def remove_await_parens(node: Node) -> None: + if node.children[0].type == token.AWAIT and len(node.children) > 1: + if ( + node.children[1].type == syms.atom + and node.children[1].children[0].type == token.LPAR + ): + if maybe_make_parens_invisible_in_atom( + node.children[1], + parent=node, + remove_brackets_around_comma=True, + ): + wrap_in_parentheses(node, node.children[1], visible=False) + + # Since await is an expression we shouldn't remove + # brackets in cases where this would change + # the AST due to operator precedence. + # Therefore we only aim to remove brackets around + # power nodes that aren't also await expressions themselves. + # https://peps.python.org/pep-0492/#updated-operator-precedence-table + # N.B. We've still removed any redundant nested brackets though :) + opening_bracket = cast(Leaf, node.children[1].children[0]) + closing_bracket = cast(Leaf, node.children[1].children[-1]) + bracket_contents = cast(Node, node.children[1].children[1]) + if bracket_contents.type != syms.power: + ensure_visible(opening_bracket) + ensure_visible(closing_bracket) + elif ( + bracket_contents.type == syms.power + and bracket_contents.children[0].type == token.AWAIT + ): + ensure_visible(opening_bracket) + ensure_visible(closing_bracket) + # If we are in a nested await then recurse down. + remove_await_parens(bracket_contents) + + 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 diff --git a/tests/data/remove_await_parens.py b/tests/data/remove_await_parens.py index dcf3973599d..7621d09c414 100644 --- a/tests/data/remove_await_parens.py +++ b/tests/data/remove_await_parens.py @@ -64,6 +64,13 @@ async def main(): async def main(): await (await asyncio.sleep(1)) +# It's brackets all the way down... +async def main(): + await (await (asyncio.sleep(1))) + +async def main(): + await (await (await (await (await (asyncio.sleep(1)))))) + # output import asyncio @@ -136,3 +143,12 @@ async def main(): async def main(): await (await asyncio.sleep(1)) + + +# It's brackets all the way down... +async def main(): + await (await asyncio.sleep(1)) + + +async def main(): + await (await (await (await (await asyncio.sleep(1))))) From d17092252c071e3ea2b6fe3772162f09e6d3dcf0 Mon Sep 17 00:00:00 2001 From: jpy-git Date: Tue, 5 Apr 2022 09:38:25 +0100 Subject: [PATCH 5/5] Extra unit tests --- tests/data/remove_await_parens.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/data/remove_await_parens.py b/tests/data/remove_await_parens.py index 7621d09c414..eb7dad340c3 100644 --- a/tests/data/remove_await_parens.py +++ b/tests/data/remove_await_parens.py @@ -64,7 +64,13 @@ async def main(): async def main(): await (await asyncio.sleep(1)) -# It's brackets all the way down... +# It's awaits all the way down... +async def main(): + await (await x) + +async def main(): + await (yield x) + async def main(): await (await (asyncio.sleep(1))) @@ -145,7 +151,15 @@ async def main(): await (await asyncio.sleep(1)) -# It's brackets all the way down... +# It's awaits all the way down... +async def main(): + await (await x) + + +async def main(): + await (yield x) + + async def main(): await (await asyncio.sleep(1))