Skip to content

Commit fc6cea0

Browse files
authored
Consistently format async statements similar to their non-async version. (#3609)
1 parent 71a2daa commit fc6cea0

File tree

7 files changed

+103
-8
lines changed

7 files changed

+103
-8
lines changed

CHANGES.md

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
- Add trailing commas to collection literals even if there's a comment after the last
1818
entry (#3393)
19+
- `async def`, `async for`, and `async with` statements are now formatted consistently
20+
compared to their non-async version. (#3609)
1921
- `with` statements that contain two context managers will be consistently wrapped in
2022
parentheses (#3589)
2123

src/black/linegen.py

+17-2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
Visitor,
3737
ensure_visible,
3838
is_arith_like,
39+
is_async_stmt_or_funcdef,
3940
is_atom_with_invisible_parens,
4041
is_docstring,
4142
is_empty_tuple,
@@ -110,6 +111,17 @@ def line(self, indent: int = 0) -> Iterator[Line]:
110111
self.current_line.depth += indent
111112
return # Line is empty, don't emit. Creating a new one unnecessary.
112113

114+
if (
115+
Preview.improved_async_statements_handling in self.mode
116+
and len(self.current_line.leaves) == 1
117+
and is_async_stmt_or_funcdef(self.current_line.leaves[0])
118+
):
119+
# Special case for async def/for/with statements. `visit_async_stmt`
120+
# adds an `ASYNC` leaf then visits the child def/for/with statement
121+
# nodes. Line yields from those nodes shouldn't treat the former
122+
# `ASYNC` leaf as a complete line.
123+
return
124+
113125
complete_line = self.current_line
114126
self.current_line = Line(mode=self.mode, depth=complete_line.depth + indent)
115127
yield complete_line
@@ -301,8 +313,11 @@ def visit_async_stmt(self, node: Node) -> Iterator[Line]:
301313
break
302314

303315
internal_stmt = next(children)
304-
for child in internal_stmt.children:
305-
yield from self.visit(child)
316+
if Preview.improved_async_statements_handling in self.mode:
317+
yield from self.visit(internal_stmt)
318+
else:
319+
for child in internal_stmt.children:
320+
yield from self.visit(child)
306321

307322
def visit_decorators(self, node: Node) -> Iterator[Line]:
308323
"""Visit decorators."""

src/black/lines.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
is_multiline_string,
2929
is_one_sequence_between,
3030
is_type_comment,
31-
is_with_stmt,
31+
is_with_or_async_with_stmt,
3232
replace_child,
3333
syms,
3434
whitespace,
@@ -124,9 +124,9 @@ def is_import(self) -> bool:
124124
return bool(self) and is_import(self.leaves[0])
125125

126126
@property
127-
def is_with_stmt(self) -> bool:
127+
def is_with_or_async_with_stmt(self) -> bool:
128128
"""Is this a with_stmt line?"""
129-
return bool(self) and is_with_stmt(self.leaves[0])
129+
return bool(self) and is_with_or_async_with_stmt(self.leaves[0])
130130

131131
@property
132132
def is_class(self) -> bool:
@@ -872,7 +872,7 @@ def can_omit_invisible_parens(
872872
if (
873873
Preview.wrap_multiple_context_managers_in_parens in line.mode
874874
and max_priority == COMMA_PRIORITY
875-
and rhs.head.is_with_stmt
875+
and rhs.head.is_with_or_async_with_stmt
876876
):
877877
# For two context manager with statements, the optional parentheses read
878878
# better. In this case, `rhs.body` is the context managers part of

src/black/mode.py

+1
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ class Preview(Enum):
155155

156156
add_trailing_comma_consistently = auto()
157157
hex_codes_in_unicode_sequences = auto()
158+
improved_async_statements_handling = auto()
158159
multiline_string_handling = auto()
159160
prefer_splitting_right_hand_side_of_assignments = auto()
160161
# NOTE: string_processing requires wrap_long_dict_values_in_parens

src/black/nodes.py

+19-2
Original file line numberDiff line numberDiff line change
@@ -789,13 +789,30 @@ def is_import(leaf: Leaf) -> bool:
789789
)
790790

791791

792-
def is_with_stmt(leaf: Leaf) -> bool:
793-
"""Return True if the given leaf starts a with statement."""
792+
def is_with_or_async_with_stmt(leaf: Leaf) -> bool:
793+
"""Return True if the given leaf starts a with or async with statement."""
794794
return bool(
795795
leaf.type == token.NAME
796796
and leaf.value == "with"
797797
and leaf.parent
798798
and leaf.parent.type == syms.with_stmt
799+
) or bool(
800+
leaf.type == token.ASYNC
801+
and leaf.next_sibling
802+
and leaf.next_sibling.type == syms.with_stmt
803+
)
804+
805+
806+
def is_async_stmt_or_funcdef(leaf: Leaf) -> bool:
807+
"""Return True if the given leaf starts an async def/for/with statement.
808+
809+
Note that `async def` can be either an `async_stmt` or `async_funcdef`,
810+
the latter is used when it has decorators.
811+
"""
812+
return bool(
813+
leaf.type == token.ASYNC
814+
and leaf.parent
815+
and leaf.parent.type in {syms.async_stmt, syms.async_funcdef}
799816
)
800817

801818

tests/data/preview/async_stmts.py

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
async def func() -> (int):
2+
return 0
3+
4+
5+
@decorated
6+
async def func() -> (int):
7+
return 0
8+
9+
10+
async for (item) in async_iter:
11+
pass
12+
13+
14+
# output
15+
16+
17+
async def func() -> int:
18+
return 0
19+
20+
21+
@decorated
22+
async def func() -> int:
23+
return 0
24+
25+
26+
async for item in async_iter:
27+
pass

tests/data/preview_context_managers/targeting_py39.py

+33
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,23 @@
6767
pass
6868

6969

70+
async def func():
71+
async with \
72+
make_context_manager1() as cm1, \
73+
make_context_manager2() as cm2, \
74+
make_context_manager3() as cm3, \
75+
make_context_manager4() as cm4 \
76+
:
77+
pass
78+
79+
async with some_function(
80+
argument1, argument2, argument3="some_value"
81+
) as some_cm, some_other_function(
82+
argument1, argument2, argument3="some_value"
83+
):
84+
pass
85+
86+
7087
# output
7188

7289

@@ -139,3 +156,19 @@
139156
]
140157
).another_method() as cmd:
141158
pass
159+
160+
161+
async def func():
162+
async with (
163+
make_context_manager1() as cm1,
164+
make_context_manager2() as cm2,
165+
make_context_manager3() as cm3,
166+
make_context_manager4() as cm4,
167+
):
168+
pass
169+
170+
async with (
171+
some_function(argument1, argument2, argument3="some_value") as some_cm,
172+
some_other_function(argument1, argument2, argument3="some_value"),
173+
):
174+
pass

0 commit comments

Comments
 (0)