Format named expressions (walrus operator) (#5642)

## Summary

Format named expressions (walrus operator) such a `value := f()`. 

Unlike tuples, named expression parentheses are not part of the range
even when mandatory, so mapping optional parentheses to always gives us
decent formatting without implementing all [PEP
572](https://peps.python.org/pep-0572/) rules on when we need
parentheses where other expressions wouldn't. We might want to revisit
this decision later and implement special cases, but for now this gives
us what we need.

## Test Plan

black fixtures, i added some fixtures and checked django and cpython for
stability.

Closes #5613
This commit is contained in:
konsti 2023-07-10 14:32:15 +02:00 committed by GitHub
parent 1e894f328c
commit bd8f65814c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 152 additions and 318 deletions

View file

@ -32,9 +32,9 @@ f(x, (a := b + c for c in range(10)), y=z, **q)
-x[a:=0]
-x[a:=0, b:=1]
-x[5, b:=0]
+x[NOT_YET_IMPLEMENTED_ExprNamedExpr]
+x[NOT_YET_IMPLEMENTED_ExprNamedExpr, NOT_YET_IMPLEMENTED_ExprNamedExpr]
+x[5, NOT_YET_IMPLEMENTED_ExprNamedExpr]
+x[a := 0]
+x[a := 0, b := 1]
+x[5, b := 0]
# Walruses are allowed inside generator expressions on function calls since 3.10.
-if any(match := pattern_error.match(s) for s in buffer):
@ -62,9 +62,9 @@ f(x, (a := b + c for c in range(10)), y=z, **q)
```py
# Unparenthesized walruses are now allowed in indices since Python 3.10.
x[NOT_YET_IMPLEMENTED_ExprNamedExpr]
x[NOT_YET_IMPLEMENTED_ExprNamedExpr, NOT_YET_IMPLEMENTED_ExprNamedExpr]
x[5, NOT_YET_IMPLEMENTED_ExprNamedExpr]
x[a := 0]
x[a := 0, b := 1]
x[5, b := 0]
# Walruses are allowed inside generator expressions on function calls since 3.10.
if any((NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])):

View file

@ -59,36 +59,20 @@ while x := f(x):
```diff
--- Black
+++ Ruff
@@ -1,47 +1,47 @@
-(a := 1)
-(a := a)
-if (match := pattern.search(data)) is None:
+(NOT_YET_IMPLEMENTED_ExprNamedExpr)
+(NOT_YET_IMPLEMENTED_ExprNamedExpr)
+if (NOT_YET_IMPLEMENTED_ExprNamedExpr) is None:
@@ -2,10 +2,10 @@
(a := a)
if (match := pattern.search(data)) is None:
pass
-if match := pattern.search(data):
+if NOT_YET_IMPLEMENTED_ExprNamedExpr:
+if (match := pattern.search(data)):
pass
-[y := f(x), y**2, y**3]
[y := f(x), y**2, y**3]
-filtered_data = [y for x in data if (y := f(x)) is None]
-(y := f(x))
-y0 = (y1 := f(x))
-foo(x=(y := f(x)))
+[NOT_YET_IMPLEMENTED_ExprNamedExpr, y**2, y**3]
+filtered_data = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []]
+(NOT_YET_IMPLEMENTED_ExprNamedExpr)
+y0 = NOT_YET_IMPLEMENTED_ExprNamedExpr
+foo(x=(NOT_YET_IMPLEMENTED_ExprNamedExpr))
-def foo(answer=(p := 42)):
+def foo(answer=(NOT_YET_IMPLEMENTED_ExprNamedExpr)):
pass
-def foo(answer: (p := 42) = 5):
+def foo(answer: (NOT_YET_IMPLEMENTED_ExprNamedExpr) = 5):
(y := f(x))
y0 = (y1 := f(x))
foo(x=(y := f(x)))
@@ -19,29 +19,29 @@
pass
@ -96,99 +80,89 @@ while x := f(x):
-(x := lambda: 1)
-(x := lambda: (y := 1))
-lambda line: (m := re.match(pattern, line)) and m.group(1)
-x = (y := 0)
-(z := (y := (x := 0)))
+lambda x: True
+(x := lambda x: True)
+(x := lambda x: True)
+lambda x: True
x = (y := 0)
(z := (y := (x := 0)))
-(info := (name, phone, *rest))
-(x := 1, 2)
-(total := total + tax)
-len(lines := f.readlines())
-foo(x := 3, cat="vector")
-foo(cat=(category := "vector"))
+(info := (name, phone, *NOT_YET_IMPLEMENTED_ExprStarred))
(x := 1, 2)
(total := total + tax)
len(lines := f.readlines())
foo(x := 3, cat="vector")
foo(cat=(category := "vector"))
-if any(len(longline := l) >= 100 for l in lines):
+lambda x: True
+(NOT_YET_IMPLEMENTED_ExprNamedExpr)
+(NOT_YET_IMPLEMENTED_ExprNamedExpr)
+lambda x: True
+x = NOT_YET_IMPLEMENTED_ExprNamedExpr
+(NOT_YET_IMPLEMENTED_ExprNamedExpr)
+(NOT_YET_IMPLEMENTED_ExprNamedExpr)
+(NOT_YET_IMPLEMENTED_ExprNamedExpr, 2)
+(NOT_YET_IMPLEMENTED_ExprNamedExpr)
+len(NOT_YET_IMPLEMENTED_ExprNamedExpr)
+foo(NOT_YET_IMPLEMENTED_ExprNamedExpr, cat="vector")
+foo(cat=(NOT_YET_IMPLEMENTED_ExprNamedExpr))
+if any((NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])):
print(longline)
-if env_base := os.environ.get("PYTHONUSERBASE", None):
+if NOT_YET_IMPLEMENTED_ExprNamedExpr:
+if (env_base := os.environ.get("PYTHONUSERBASE", None)):
return env_base
-if self._is_special and (ans := self._check_nans(context=context)):
+if self._is_special and (NOT_YET_IMPLEMENTED_ExprNamedExpr):
if self._is_special and (ans := self._check_nans(context=context)):
return ans
-foo(b := 2, a=1)
foo(b := 2, a=1)
-foo((b := 2), a=1)
-foo(c=(b := 2), a=1)
+foo(NOT_YET_IMPLEMENTED_ExprNamedExpr, a=1)
+foo(NOT_YET_IMPLEMENTED_ExprNamedExpr, a=1)
+foo(c=(NOT_YET_IMPLEMENTED_ExprNamedExpr), a=1)
+foo(b := 2, a=1)
foo(c=(b := 2), a=1)
-while x := f(x):
+while NOT_YET_IMPLEMENTED_ExprNamedExpr:
+while (x := f(x)):
pass
-while x := f(x):
+while NOT_YET_IMPLEMENTED_ExprNamedExpr:
+while (x := f(x)):
pass
```
## Ruff Output
```py
(NOT_YET_IMPLEMENTED_ExprNamedExpr)
(NOT_YET_IMPLEMENTED_ExprNamedExpr)
if (NOT_YET_IMPLEMENTED_ExprNamedExpr) is None:
(a := 1)
(a := a)
if (match := pattern.search(data)) is None:
pass
if NOT_YET_IMPLEMENTED_ExprNamedExpr:
if (match := pattern.search(data)):
pass
[NOT_YET_IMPLEMENTED_ExprNamedExpr, y**2, y**3]
[y := f(x), y**2, y**3]
filtered_data = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []]
(NOT_YET_IMPLEMENTED_ExprNamedExpr)
y0 = NOT_YET_IMPLEMENTED_ExprNamedExpr
foo(x=(NOT_YET_IMPLEMENTED_ExprNamedExpr))
(y := f(x))
y0 = (y1 := f(x))
foo(x=(y := f(x)))
def foo(answer=(NOT_YET_IMPLEMENTED_ExprNamedExpr)):
def foo(answer=(p := 42)):
pass
def foo(answer: (NOT_YET_IMPLEMENTED_ExprNamedExpr) = 5):
def foo(answer: (p := 42) = 5):
pass
lambda x: True
(NOT_YET_IMPLEMENTED_ExprNamedExpr)
(NOT_YET_IMPLEMENTED_ExprNamedExpr)
(x := lambda x: True)
(x := lambda x: True)
lambda x: True
x = NOT_YET_IMPLEMENTED_ExprNamedExpr
(NOT_YET_IMPLEMENTED_ExprNamedExpr)
(NOT_YET_IMPLEMENTED_ExprNamedExpr)
(NOT_YET_IMPLEMENTED_ExprNamedExpr, 2)
(NOT_YET_IMPLEMENTED_ExprNamedExpr)
len(NOT_YET_IMPLEMENTED_ExprNamedExpr)
foo(NOT_YET_IMPLEMENTED_ExprNamedExpr, cat="vector")
foo(cat=(NOT_YET_IMPLEMENTED_ExprNamedExpr))
x = (y := 0)
(z := (y := (x := 0)))
(info := (name, phone, *NOT_YET_IMPLEMENTED_ExprStarred))
(x := 1, 2)
(total := total + tax)
len(lines := f.readlines())
foo(x := 3, cat="vector")
foo(cat=(category := "vector"))
if any((NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])):
print(longline)
if NOT_YET_IMPLEMENTED_ExprNamedExpr:
if (env_base := os.environ.get("PYTHONUSERBASE", None)):
return env_base
if self._is_special and (NOT_YET_IMPLEMENTED_ExprNamedExpr):
if self._is_special and (ans := self._check_nans(context=context)):
return ans
foo(NOT_YET_IMPLEMENTED_ExprNamedExpr, a=1)
foo(NOT_YET_IMPLEMENTED_ExprNamedExpr, a=1)
foo(c=(NOT_YET_IMPLEMENTED_ExprNamedExpr), a=1)
foo(b := 2, a=1)
foo(b := 2, a=1)
foo(c=(b := 2), a=1)
while NOT_YET_IMPLEMENTED_ExprNamedExpr:
while (x := f(x)):
pass
while NOT_YET_IMPLEMENTED_ExprNamedExpr:
while (x := f(x)):
pass
```

View file

@ -22,15 +22,13 @@ x[(a := 1), (b := 3)]
@@ -1,7 +1,7 @@
# Unparenthesized walruses are now allowed in set literals & set comprehensions
# since Python 3.9
-{x := 1, 2, 3}
{x := 1, 2, 3}
-{x4 := x**5 for x in range(7)}
+{NOT_YET_IMPLEMENTED_ExprNamedExpr, 2, 3}
+{NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set}
# We better not remove the parentheses here (since it's a 3.10 feature)
-x[(a := 1)]
x[(a := 1)]
-x[(a := 1), (b := 3)]
+x[(NOT_YET_IMPLEMENTED_ExprNamedExpr)]
+x[((NOT_YET_IMPLEMENTED_ExprNamedExpr), (NOT_YET_IMPLEMENTED_ExprNamedExpr))]
+x[((a := 1), (b := 3))]
```
## Ruff Output
@ -38,11 +36,11 @@ x[(a := 1), (b := 3)]
```py
# Unparenthesized walruses are now allowed in set literals & set comprehensions
# since Python 3.9
{NOT_YET_IMPLEMENTED_ExprNamedExpr, 2, 3}
{x := 1, 2, 3}
{NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set}
# We better not remove the parentheses here (since it's a 3.10 feature)
x[(NOT_YET_IMPLEMENTED_ExprNamedExpr)]
x[((NOT_YET_IMPLEMENTED_ExprNamedExpr), (NOT_YET_IMPLEMENTED_ExprNamedExpr))]
x[(a := 1)]
x[((a := 1), (b := 3))]
```
## Black Output

View file

@ -32,14 +32,17 @@ def f():
@relaxed_decorator[0]
def f():
...
@@ -13,8 +12,6 @@
@@ -13,8 +12,10 @@
...
-@extremely_long_variable_name_that_doesnt_fit := complex.expression(
- with_long="arguments_value_that_wont_fit_at_the_end_of_the_line"
-)
+@NOT_YET_IMPLEMENTED_ExprNamedExpr
+@(
+ extremely_long_variable_name_that_doesnt_fit := complex.expression(
+ with_long="arguments_value_that_wont_fit_at_the_end_of_the_line"
+ )
)
def f():
...
```
@ -61,7 +64,11 @@ def f():
...
@NOT_YET_IMPLEMENTED_ExprNamedExpr
@(
extremely_long_variable_name_that_doesnt_fit := complex.expression(
with_long="arguments_value_that_wont_fit_at_the_end_of_the_line"
)
)
def f():
...
```

View file

@ -1,216 +0,0 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_39/remove_with_brackets.py
---
## Input
```py
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
# 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):
...
```
## Black Differences
```diff
--- Black
+++ Ruff
@@ -39,7 +39,7 @@
...
# Don't touch assignment expressions
-with (y := open("./test.py")) as f:
+with NOT_YET_IMPLEMENTED_ExprNamedExpr as f:
pass
# Deeply nested examples
```
## Ruff Output
```py
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 NOT_YET_IMPLEMENTED_ExprNamedExpr 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:
...
```
## Black Output
```py
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
# 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:
...
```

View file

@ -0,0 +1,38 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/named_expr.py
---
## Input
```py
y = 1
if (
# 1
x # 2
:= # 3
y # 4
):
pass
y0 = (y1 := f(x))
f(x:=y, z=True)
```
## Output
```py
y = 1
if (
# 1
x := y # 2 # 3 # 4
):
pass
y0 = (y1 := f(x))
f(x := y, z=True)
```