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:
github-actions[bot]: **diff-shades** results comparing this PR (9a2de75c974c1a24aefc3c7d0b66e9a110d5a6a5) to main (24c708eb374a856372284fb1a4f021fec292f713). The full diff is [available in the logs](https://github.com/psf/black/runs/5817065194?check_suite_focus=true#step:10:1) under the "Generate HTML diff report" step.
```text
╭─────────────────────── Summary ────────────────────────╮
│ 1 projects & 3 files changed / 2 changes [+1/-1]       │
│                                                        │
│ ... out of 2 173 850 lines, 10 479 files & 23 projects │
╰────────────────────────────────────────────────────────╯

Differences found.
```
[**What is this?**](https://black.readthedocs.io/en/latest/contributing/gauging_changes.html#diff-shades) | [Workflow run](https://github.com/psf/black/actions/runs/2090130066) | [diff-shades documentation](https://github.com/ichard26/diff-shades#readme)
ichard26: I should definitely color the "(allowed)" text next to the `sqlalchemy:test/orm/test_relationship_criteria.py` failure (since that one is known) as it's almost impossible to notice. The first two failures are new though. 
jpy-git: > The first two failures are new though.

```
1. test/ext/asyncio/test_engine_py3k.py: InvalidInput - Cannot parse: 985:31:                 result = await     
  await conn.stream_scalars(select(users)).all()                                                                   
  2. test/ext/asyncio/test_session_py3k.py: InvalidInput - Cannot parse: 1[13](https://github.com/psf/black/runs/5817065194?check_suite_focus=true#step:17:13):27:             result = await await  
  async_session.stream_scalars(stmt).all()
```
I see what's going on here, in the Python grammar `await` is an optional prefix for `power` so it is possible to call `await(await something(...))`. I'll just need to not remove the brackets in this case and add a unit test for this! 😄 
jpy-git: I've added the extra unit tests and handled the nested awaits, note that I also recurse down the nested awaits to remove any redundant parens within those 👍 

Also not sure why that lint check is failing, it's fine locally for me 🤷
JelleZijlstra: > Also not sure why that lint check is failing, it's fine locally for me 🤷

Looks like more click 8.1 fallout, I can look into it later. We need to either update our type ignores or tell the mypy check to use a different version of click.

> I've added the extra unit tests and handled the nested awaits, note that I also recurse down the nested awaits to remove any redundant parens within those 👍

Thanks!

JelleZijlstra: @ichard26 actually fixed the Click issue, thanks! I merged main in and hopefully CI will be green now.
jpy-git: > @ichard26 actually fixed the Click issue, thanks!

Thanks @ichard26 😄 

Patch:
diff --git a/CHANGES.md b/CHANGES.md
index c631aec..e168e24 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -14,6 +14,7 @@
 
 <!-- Changes that affect Black's preview style -->
 
+- Remove redundant parentheses around awaited objects (#2991)
 - Parentheses around return annotations are now managed (#2990)
 - Remove unnecessary parentheses from `with` statements (#2926)
 
diff --git a/src/black/linegen.py b/src/black/linegen.py
index c2b0616..caffbab 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
@@ -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
index 0000000..eb7dad3
--- /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
index 6f71617..51d8fb0 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",
     "return_annotation_brackets",
 ]