@@ -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
10921097def 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+
11821200def 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+
12181279def 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
0 commit comments