Repository: psf/black

Issue:
Should `await` automatically manage parentheses like `return`?
On a first glance, it would be better. On another though, `await` is an expression which can potentially complicate formattings that are now rather simple.

Alternatively, `await` should be ignored by an existing bracket pair for purposes of splitting on delimiters, enabling for example fluent interfaces.


Discussion:
Concrete example:
```
% black -c 'return (a)'   
return a
% black -c 'await (a)' 
await (a)
```

Resolving patch:
diff --git a/src/black/linegen.py b/src/black/linegen.py
--- 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
@@ -253,6 +253,9 @@ def visit_power(self, node: Node) -> Iterator[Line]:
             ):
                 wrap_in_parentheses(node, leaf)
 
+        if Preview.remove_redundant_parens in self.mode:
+            remove_await_parens(node)
+
         yield from self.visit_default(node)
 
     def visit_SEMI(self, leaf: Leaf) -> Iterator[Line]:
@@ -923,6 +926,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


Test patch:
diff --git a/tests/data/remove_await_parens.py b/tests/data/remove_await_parens.py
new file mode 100644
--- /dev/null
+++ b/tests/data/remove_await_parens.py
@@ -0,0 +1,168 @@
+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)))
+        )))        )))
+        )))         )))
+        )))         )))
+        )))        )))
+        )))))))))))))
+    )
+
+# 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))
+
+# 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)))
+
+async def main():
+    await (await (await (await (await (asyncio.sleep(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)
+
+
+# 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))
+
+
+# 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))
+
+
+async def main():
+    await (await (await (await (await asyncio.sleep(1)))))
diff --git a/tests/test_format.py b/tests/test_format.py
--- 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",
     "return_annotation_brackets",
 ]