Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 91e1e13

Browse files
authored
Wrap multiple context managers in parentheses when targeting Python 3.9+ (psf#3489)
1 parent 18fb884 commit 91e1e13

11 files changed

Lines changed: 416 additions & 21 deletions

File tree

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
- Fix two crashes in preview style involving edge cases with docstrings (#3451)
3636
- Exclude string type annotations from improved string processing; fix crash when the
3737
return type annotation is stringified and spans across multiple lines (#3462)
38+
- Wrap multiple context managers in parentheses when targeting Python 3.9+ (#3489)
3839
- Fix several crashes in preview style with walrus operators used in `with` statements
3940
or tuples (#3473)
4041

src/black/__init__.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1096,8 +1096,13 @@ def _format_str_once(src_contents: str, *, mode: Mode) -> str:
10961096
future_imports = get_future_imports(src_node)
10971097
versions = detect_target_versions(src_node, future_imports=future_imports)
10981098

1099+
context_manager_features = {
1100+
feature
1101+
for feature in {Feature.PARENTHESIZED_CONTEXT_MANAGERS}
1102+
if supports_feature(versions, feature)
1103+
}
10991104
normalize_fmt_off(src_node, preview=mode.preview)
1100-
lines = LineGenerator(mode=mode)
1105+
lines = LineGenerator(mode=mode, features=context_manager_features)
11011106
elt = EmptyLineTracker(mode=mode)
11021107
split_line_features = {
11031108
feature
@@ -1159,6 +1164,10 @@ def get_features_used( # noqa: C901
11591164
- relaxed decorator syntax;
11601165
- usage of __future__ flags (annotations);
11611166
- print / exec statements;
1167+
- parenthesized context managers;
1168+
- match statements;
1169+
- except* clause;
1170+
- variadic generics;
11621171
"""
11631172
features: Set[Feature] = set()
11641173
if future_imports:
@@ -1234,6 +1243,23 @@ def get_features_used( # noqa: C901
12341243
):
12351244
features.add(Feature.ANN_ASSIGN_EXTENDED_RHS)
12361245

1246+
elif (
1247+
n.type == syms.with_stmt
1248+
and len(n.children) > 2
1249+
and n.children[1].type == syms.atom
1250+
):
1251+
atom_children = n.children[1].children
1252+
if (
1253+
len(atom_children) == 3
1254+
and atom_children[0].type == token.LPAR
1255+
and atom_children[1].type == syms.testlist_gexp
1256+
and atom_children[2].type == token.RPAR
1257+
):
1258+
features.add(Feature.PARENTHESIZED_CONTEXT_MANAGERS)
1259+
1260+
elif n.type == syms.match_stmt:
1261+
features.add(Feature.PATTERN_MATCHING)
1262+
12371263
elif (
12381264
n.type == syms.except_clause
12391265
and len(n.children) >= 2

src/black/linegen.py

Lines changed: 81 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,9 @@ class LineGenerator(Visitor[Line]):
9090
in ways that will no longer stringify to valid Python code on the tree.
9191
"""
9292

93-
def __init__(self, mode: Mode) -> None:
93+
def __init__(self, mode: Mode, features: Collection[Feature]) -> None:
9494
self.mode = mode
95+
self.features = features
9596
self.current_line: Line
9697
self.__post_init__()
9798

@@ -191,7 +192,9 @@ def visit_stmt(
191192
`parens` holds a set of string leaf values immediately after which
192193
invisible parens should be put.
193194
"""
194-
normalize_invisible_parens(node, parens_after=parens, preview=self.mode.preview)
195+
normalize_invisible_parens(
196+
node, parens_after=parens, mode=self.mode, features=self.features
197+
)
195198
for child in node.children:
196199
if is_name_token(child) and child.value in keywords:
197200
yield from self.line()
@@ -244,7 +247,9 @@ def visit_funcdef(self, node: Node) -> Iterator[Line]:
244247

245248
def visit_match_case(self, node: Node) -> Iterator[Line]:
246249
"""Visit either a match or case statement."""
247-
normalize_invisible_parens(node, parens_after=set(), preview=self.mode.preview)
250+
normalize_invisible_parens(
251+
node, parens_after=set(), mode=self.mode, features=self.features
252+
)
248253

249254
yield from self.line()
250255
for child in node.children:
@@ -1090,7 +1095,7 @@ def normalize_prefix(leaf: Leaf, *, inside_brackets: bool) -> None:
10901095

10911096

10921097
def normalize_invisible_parens(
1093-
node: Node, parens_after: Set[str], *, preview: bool
1098+
node: Node, parens_after: Set[str], *, mode: Mode, features: Collection[Feature]
10941099
) -> None:
10951100
"""Make existing optional parentheses invisible or create new ones.
10961101
@@ -1100,17 +1105,24 @@ def normalize_invisible_parens(
11001105
Standardizes on visible parentheses for single-element tuples, and keeps
11011106
existing visible parentheses for other tuples and generator expressions.
11021107
"""
1103-
for pc in list_comments(node.prefix, is_endmarker=False, preview=preview):
1108+
for pc in list_comments(node.prefix, is_endmarker=False, preview=mode.preview):
11041109
if pc.value in FMT_OFF:
11051110
# This `node` has a prefix with `# fmt: off`, don't mess with parens.
11061111
return
1112+
1113+
# The multiple context managers grammar has a different pattern, thus this is
1114+
# separate from the for-loop below. This possibly wraps them in invisible parens,
1115+
# and later will be removed in remove_with_parens when needed.
1116+
if node.type == syms.with_stmt:
1117+
_maybe_wrap_cms_in_parens(node, mode, features)
1118+
11071119
check_lpar = False
11081120
for index, child in enumerate(list(node.children)):
11091121
# Fixes a bug where invisible parens are not properly stripped from
11101122
# assignment statements that contain type annotations.
11111123
if isinstance(child, Node) and child.type == syms.annassign:
11121124
normalize_invisible_parens(
1113-
child, parens_after=parens_after, preview=preview
1125+
child, parens_after=parens_after, mode=mode, features=features
11141126
)
11151127

11161128
# Add parentheses around long tuple unpacking in assignments.
@@ -1123,7 +1135,7 @@ def normalize_invisible_parens(
11231135

11241136
if check_lpar:
11251137
if (
1126-
preview
1138+
mode.preview
11271139
and child.type == syms.atom
11281140
and node.type == syms.for_stmt
11291141
and isinstance(child.prev_sibling, Leaf)
@@ -1136,7 +1148,9 @@ def normalize_invisible_parens(
11361148
remove_brackets_around_comma=True,
11371149
):
11381150
wrap_in_parentheses(node, child, visible=False)
1139-
elif preview and isinstance(child, Node) and node.type == syms.with_stmt:
1151+
elif (
1152+
mode.preview and isinstance(child, Node) and node.type == syms.with_stmt
1153+
):
11401154
remove_with_parens(child, node)
11411155
elif child.type == syms.atom:
11421156
if maybe_make_parens_invisible_in_atom(
@@ -1147,17 +1161,7 @@ def normalize_invisible_parens(
11471161
elif is_one_tuple(child):
11481162
wrap_in_parentheses(node, child, visible=True)
11491163
elif node.type == syms.import_from:
1150-
# "import from" nodes store parentheses directly as part of
1151-
# the statement
1152-
if is_lpar_token(child):
1153-
assert is_rpar_token(node.children[-1])
1154-
# make parentheses invisible
1155-
child.value = ""
1156-
node.children[-1].value = ""
1157-
elif child.type != token.STAR:
1158-
# insert invisible parentheses
1159-
node.insert_child(index, Leaf(token.LPAR, ""))
1160-
node.append_child(Leaf(token.RPAR, ""))
1164+
_normalize_import_from(node, child, index)
11611165
break
11621166
elif (
11631167
index == 1
@@ -1172,13 +1176,27 @@ def normalize_invisible_parens(
11721176
elif not (isinstance(child, Leaf) and is_multiline_string(child)):
11731177
wrap_in_parentheses(node, child, visible=False)
11741178

1175-
comma_check = child.type == token.COMMA if preview else False
1179+
comma_check = child.type == token.COMMA if mode.preview else False
11761180

11771181
check_lpar = isinstance(child, Leaf) and (
11781182
child.value in parens_after or comma_check
11791183
)
11801184

11811185

1186+
def _normalize_import_from(parent: Node, child: LN, index: int) -> None:
1187+
# "import from" nodes store parentheses directly as part of
1188+
# the statement
1189+
if is_lpar_token(child):
1190+
assert is_rpar_token(parent.children[-1])
1191+
# make parentheses invisible
1192+
child.value = ""
1193+
parent.children[-1].value = ""
1194+
elif child.type != token.STAR:
1195+
# insert invisible parentheses
1196+
parent.insert_child(index, Leaf(token.LPAR, ""))
1197+
parent.append_child(Leaf(token.RPAR, ""))
1198+
1199+
11821200
def remove_await_parens(node: Node) -> None:
11831201
if node.children[0].type == token.AWAIT and len(node.children) > 1:
11841202
if (
@@ -1215,6 +1233,49 @@ def remove_await_parens(node: Node) -> None:
12151233
remove_await_parens(bracket_contents)
12161234

12171235

1236+
def _maybe_wrap_cms_in_parens(
1237+
node: Node, mode: Mode, features: Collection[Feature]
1238+
) -> None:
1239+
"""When enabled and safe, wrap the multiple context managers in invisible parens.
1240+
1241+
It is only safe when `features` contain Feature.PARENTHESIZED_CONTEXT_MANAGERS.
1242+
"""
1243+
if (
1244+
Feature.PARENTHESIZED_CONTEXT_MANAGERS not in features
1245+
or Preview.wrap_multiple_context_managers_in_parens not in mode
1246+
or len(node.children) <= 2
1247+
# If it's an atom, it's already wrapped in parens.
1248+
or node.children[1].type == syms.atom
1249+
):
1250+
return
1251+
colon_index: Optional[int] = None
1252+
for i in range(2, len(node.children)):
1253+
if node.children[i].type == token.COLON:
1254+
colon_index = i
1255+
break
1256+
if colon_index is not None:
1257+
lpar = Leaf(token.LPAR, "")
1258+
rpar = Leaf(token.RPAR, "")
1259+
context_managers = node.children[1:colon_index]
1260+
for child in context_managers:
1261+
child.remove()
1262+
# After wrapping, the with_stmt will look like this:
1263+
# with_stmt
1264+
# NAME 'with'
1265+
# atom
1266+
# LPAR ''
1267+
# testlist_gexp
1268+
# ... <-- context_managers
1269+
# /testlist_gexp
1270+
# RPAR ''
1271+
# /atom
1272+
# COLON ':'
1273+
new_child = Node(
1274+
syms.atom, [lpar, Node(syms.testlist_gexp, context_managers), rpar]
1275+
)
1276+
node.insert_child(1, new_child)
1277+
1278+
12181279
def remove_with_parens(node: Node, parent: Node) -> None:
12191280
"""Recursively hide optional parens in `with` statements."""
12201281
# Removing all unnecessary parentheses in with statements in one pass is a tad

src/black/mode.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class Feature(Enum):
5050
EXCEPT_STAR = 14
5151
VARIADIC_GENERICS = 15
5252
DEBUG_F_STRINGS = 16
53+
PARENTHESIZED_CONTEXT_MANAGERS = 17
5354
FORCE_OPTIONAL_PARENTHESES = 50
5455

5556
# __future__ flags
@@ -106,6 +107,7 @@ class Feature(Enum):
106107
Feature.POS_ONLY_ARGUMENTS,
107108
Feature.UNPACKING_ON_FLOW,
108109
Feature.ANN_ASSIGN_EXTENDED_RHS,
110+
Feature.PARENTHESIZED_CONTEXT_MANAGERS,
109111
},
110112
TargetVersion.PY310: {
111113
Feature.F_STRINGS,
@@ -120,6 +122,7 @@ class Feature(Enum):
120122
Feature.POS_ONLY_ARGUMENTS,
121123
Feature.UNPACKING_ON_FLOW,
122124
Feature.ANN_ASSIGN_EXTENDED_RHS,
125+
Feature.PARENTHESIZED_CONTEXT_MANAGERS,
123126
Feature.PATTERN_MATCHING,
124127
},
125128
TargetVersion.PY311: {
@@ -135,6 +138,7 @@ class Feature(Enum):
135138
Feature.POS_ONLY_ARGUMENTS,
136139
Feature.UNPACKING_ON_FLOW,
137140
Feature.ANN_ASSIGN_EXTENDED_RHS,
141+
Feature.PARENTHESIZED_CONTEXT_MANAGERS,
138142
Feature.PATTERN_MATCHING,
139143
Feature.EXCEPT_STAR,
140144
Feature.VARIADIC_GENERICS,
@@ -164,6 +168,7 @@ class Preview(Enum):
164168
parenthesize_conditional_expressions = auto()
165169
skip_magic_trailing_comma_in_subscript = auto()
166170
wrap_long_dict_values_in_parens = auto()
171+
wrap_multiple_context_managers_in_parens = auto()
167172

168173

169174
class Deprecated(UserWarning):
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# This file uses pattern matching introduced in Python 3.10.
2+
3+
4+
match http_code:
5+
case 404:
6+
print("Not found")
7+
8+
9+
with \
10+
make_context_manager1() as cm1, \
11+
make_context_manager2() as cm2, \
12+
make_context_manager3() as cm3, \
13+
make_context_manager4() as cm4 \
14+
:
15+
pass
16+
17+
18+
# output
19+
20+
21+
# This file uses pattern matching introduced in Python 3.10.
22+
23+
24+
match http_code:
25+
case 404:
26+
print("Not found")
27+
28+
29+
with (
30+
make_context_manager1() as cm1,
31+
make_context_manager2() as cm2,
32+
make_context_manager3() as cm3,
33+
make_context_manager4() as cm4,
34+
):
35+
pass
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# This file uses except* clause in Python 3.11.
2+
3+
4+
try:
5+
some_call()
6+
except* Error as e:
7+
pass
8+
9+
10+
with \
11+
make_context_manager1() as cm1, \
12+
make_context_manager2() as cm2, \
13+
make_context_manager3() as cm3, \
14+
make_context_manager4() as cm4 \
15+
:
16+
pass
17+
18+
19+
# output
20+
21+
22+
# This file uses except* clause in Python 3.11.
23+
24+
25+
try:
26+
some_call()
27+
except* Error as e:
28+
pass
29+
30+
31+
with (
32+
make_context_manager1() as cm1,
33+
make_context_manager2() as cm2,
34+
make_context_manager3() as cm3,
35+
make_context_manager4() as cm4,
36+
):
37+
pass
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# This file doesn't use any Python 3.9+ only grammars.
2+
3+
4+
# Make sure parens around a single context manager don't get autodetected as
5+
# Python 3.9+.
6+
with (a):
7+
pass
8+
9+
10+
with \
11+
make_context_manager1() as cm1, \
12+
make_context_manager2() as cm2, \
13+
make_context_manager3() as cm3, \
14+
make_context_manager4() as cm4 \
15+
:
16+
pass
17+
18+
19+
# output
20+
# This file doesn't use any Python 3.9+ only grammars.
21+
22+
23+
# Make sure parens around a single context manager don't get autodetected as
24+
# Python 3.9+.
25+
with a:
26+
pass
27+
28+
29+
with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4:
30+
pass

0 commit comments

Comments
 (0)