Respect parentheses for precedence in await (#7468)

## Summary

We were using `Parenthesize::IfBreaks` universally for `await`, but
dropping parentheses can change the AST due to precedence. It turns out
that Black's rules aren't _exactly_ the same as operator precedence
(e.g., they leave parentheses around `await ([1, 2, 3])`, although they
aren't strictly required).

Closes https://github.com/astral-sh/ruff/issues/7467.

## Test Plan

`cargo test`

No change in similarity.

Before:

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1632 |
| django | 0.99982 | 2760 | 37 |
| transformers | 0.99957 | 2587 | 398 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99983 | 3496 | 18 |
| warehouse | 0.99929 | 648 | 16 |
| zulip | 0.99962 | 1437 | 22 |

After:

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1632 |
| django | 0.99982 | 2760 | 37 |
| transformers | 0.99957 | 2587 | 398 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99983 | 3496 | 18 |
| warehouse | 0.99929 | 648 | 16 |
| zulip | 0.99962 | 1437 | 22 |
This commit is contained in:
Charlie Marsh 2023-09-18 09:56:41 -04:00 committed by GitHub
parent c4d85d6fb6
commit 8ab2519717
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 165 additions and 14 deletions

View file

@ -305,3 +305,21 @@ import os
import sys import sys
``` ```
### Parentheses around awaited collections are not preserved
Black preserves parentheses around awaited collections:
```python
await ([1, 2, 3])
```
Ruff will instead remove them:
```python
await [1, 2, 3]
```
This is more consistent to the formatting of other awaited expressions: Ruff and Black both
remove parentheses around, e.g., `await (1)`, only retaining them when syntactically required,
as in, e.g., `await (x := 1)`.

View file

@ -10,3 +10,41 @@ result = await (self.request(
result = await (1 + f(1, 2, 3,)) result = await (1 + f(1, 2, 3,))
result = (await (1 + f(1, 2, 3,))) result = (await (1 + f(1, 2, 3,)))
# Optional parentheses.
await foo
await (foo)
await foo()
await (foo())
await []()
await ([]())
await (foo + bar)()
await ((foo + bar)())
await foo.bar
await (foo.bar)
await foo['bar']
await (foo['bar'])
await 1
await (1)
await ""
await ("")
await f""
await (f"")
await [foo]
await ([foo])
await {foo}
await ({foo})
await (lambda foo: foo)
await (foo or bar)
await (foo * bar)
await (yield foo)
await (not foo)
await 1, 2, 3
await (1, 2, 3)
await ( # comment
[foo]
)
await (
# comment
[foo]
)

View file

@ -33,7 +33,7 @@ impl NeedsParentheses for ExprBinOp {
parent: AnyNodeRef, parent: AnyNodeRef,
context: &PyFormatContext, context: &PyFormatContext,
) -> OptionalParentheses { ) -> OptionalParentheses {
if parent.is_expr_await() && !self.op.is_pow() { if parent.is_expr_await() {
OptionalParentheses::Always OptionalParentheses::Always
} else if let Expr::Constant(constant) = self.left.as_ref() { } else if let Expr::Constant(constant) = self.left.as_ref() {
// Multiline strings are guaranteed to never fit, avoid adding unnecessary parentheses // Multiline strings are guaranteed to never fit, avoid adding unnecessary parentheses

View file

@ -19,10 +19,14 @@ impl FormatNodeRule<ExprBoolOp> for FormatExprBoolOp {
impl NeedsParentheses for ExprBoolOp { impl NeedsParentheses for ExprBoolOp {
fn needs_parentheses( fn needs_parentheses(
&self, &self,
_parent: AnyNodeRef, parent: AnyNodeRef,
_context: &PyFormatContext, _context: &PyFormatContext,
) -> OptionalParentheses { ) -> OptionalParentheses {
OptionalParentheses::Multiline if parent.is_expr_await() {
OptionalParentheses::Always
} else {
OptionalParentheses::Multiline
}
} }
} }

View file

@ -31,10 +31,12 @@ impl FormatNodeRule<ExprCompare> for FormatExprCompare {
impl NeedsParentheses for ExprCompare { impl NeedsParentheses for ExprCompare {
fn needs_parentheses( fn needs_parentheses(
&self, &self,
_parent: AnyNodeRef, parent: AnyNodeRef,
context: &PyFormatContext, context: &PyFormatContext,
) -> OptionalParentheses { ) -> OptionalParentheses {
if let Expr::Constant(constant) = self.left.as_ref() { if parent.is_expr_await() {
OptionalParentheses::Always
} else if let Expr::Constant(constant) = self.left.as_ref() {
// Multiline strings are guaranteed to never fit, avoid adding unnecessary parentheses // Multiline strings are guaranteed to never fit, avoid adding unnecessary parentheses
if !constant.value.is_implicit_concatenated() if !constant.value.is_implicit_concatenated()
&& is_multiline_string(constant, context.source()) && is_multiline_string(constant, context.source())

View file

@ -91,10 +91,14 @@ impl FormatNodeRule<ExprGeneratorExp> for FormatExprGeneratorExp {
impl NeedsParentheses for ExprGeneratorExp { impl NeedsParentheses for ExprGeneratorExp {
fn needs_parentheses( fn needs_parentheses(
&self, &self,
_parent: AnyNodeRef, parent: AnyNodeRef,
_context: &PyFormatContext, _context: &PyFormatContext,
) -> OptionalParentheses { ) -> OptionalParentheses {
OptionalParentheses::Never if parent.is_expr_await() {
OptionalParentheses::Always
} else {
OptionalParentheses::Never
}
} }
} }

View file

@ -85,10 +85,14 @@ impl FormatNodeRule<ExprIfExp> for FormatExprIfExp {
impl NeedsParentheses for ExprIfExp { impl NeedsParentheses for ExprIfExp {
fn needs_parentheses( fn needs_parentheses(
&self, &self,
_parent: AnyNodeRef, parent: AnyNodeRef,
_context: &PyFormatContext, _context: &PyFormatContext,
) -> OptionalParentheses { ) -> OptionalParentheses {
OptionalParentheses::Multiline if parent.is_expr_await() {
OptionalParentheses::Always
} else {
OptionalParentheses::Multiline
}
} }
} }

View file

@ -64,9 +64,13 @@ impl FormatNodeRule<ExprLambda> for FormatExprLambda {
impl NeedsParentheses for ExprLambda { impl NeedsParentheses for ExprLambda {
fn needs_parentheses( fn needs_parentheses(
&self, &self,
_parent: AnyNodeRef, parent: AnyNodeRef,
_context: &PyFormatContext, _context: &PyFormatContext,
) -> OptionalParentheses { ) -> OptionalParentheses {
OptionalParentheses::Multiline if parent.is_expr_await() {
OptionalParentheses::Always
} else {
OptionalParentheses::Multiline
}
} }
} }

View file

@ -72,11 +72,12 @@ impl FormatNodeRule<ExprUnaryOp> for FormatExprUnaryOp {
impl NeedsParentheses for ExprUnaryOp { impl NeedsParentheses for ExprUnaryOp {
fn needs_parentheses( fn needs_parentheses(
&self, &self,
_parent: AnyNodeRef, parent: AnyNodeRef,
context: &PyFormatContext, context: &PyFormatContext,
) -> OptionalParentheses { ) -> OptionalParentheses {
// We preserve the parentheses of the operand. It should not be necessary to break this expression. if parent.is_expr_await() {
if is_expression_parenthesized( OptionalParentheses::Always
} else if is_expression_parenthesized(
self.operand.as_ref().into(), self.operand.as_ref().into(),
context.comments().ranges(), context.comments().ranges(),
context.source(), context.source(),

View file

@ -16,6 +16,44 @@ result = await (self.request(
result = await (1 + f(1, 2, 3,)) result = await (1 + f(1, 2, 3,))
result = (await (1 + f(1, 2, 3,))) result = (await (1 + f(1, 2, 3,)))
# Optional parentheses.
await foo
await (foo)
await foo()
await (foo())
await []()
await ([]())
await (foo + bar)()
await ((foo + bar)())
await foo.bar
await (foo.bar)
await foo['bar']
await (foo['bar'])
await 1
await (1)
await ""
await ("")
await f""
await (f"")
await [foo]
await ([foo])
await {foo}
await ({foo})
await (lambda foo: foo)
await (foo or bar)
await (foo * bar)
await (yield foo)
await (not foo)
await 1, 2, 3
await (1, 2, 3)
await ( # comment
[foo]
)
await (
# comment
[foo]
)
``` ```
## Output ## Output
@ -46,6 +84,44 @@ result = await (
3, 3,
) )
) )
# Optional parentheses.
await foo
await foo
await foo()
await foo()
await []()
await []()
await (foo + bar)()
await (foo + bar)()
await foo.bar
await foo.bar
await foo["bar"]
await foo["bar"]
await 1
await 1
await ""
await ""
await f""
await f""
await [foo]
await [foo]
await {foo}
await {foo}
await (lambda foo: foo)
await (foo or bar)
await (foo * bar)
await (yield foo)
await (not foo)
await 1, 2, 3
await (1, 2, 3)
await ( # comment
[foo]
)
await (
# comment
[foo]
)
``` ```